Tests verify: - EXIT PROCEDURE auto-execution on shutdown - Reverse module order for EXIT - AtExit LIFO callback order - WorkArea CloseAll on exit - Panic in cleanup doesn't crash (safeCall) - Shutdown runs exactly once (sync.Once) - Static variables cleared - Full sequence order: EXIT → AtExit → WA:Close → onExit Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
378 lines
7.7 KiB
Go
378 lines
7.7 KiB
Go
package hbrt
|
|
|
|
import (
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
// === EXIT PROCEDURE ===
|
|
|
|
func TestShutdown_ExitProcedure(t *testing.T) {
|
|
shutdownOnce = sync.Once{}
|
|
atExitMu.Lock()
|
|
atExitFuncs = nil
|
|
atExitMu.Unlock()
|
|
|
|
vm := NewVM()
|
|
var log []string
|
|
|
|
// Register module with EXIT procedure
|
|
vm.RegisterModule(&Module{
|
|
Name: "test_exit",
|
|
Symbols: []Symbol{
|
|
{
|
|
Name: "CLEANUP",
|
|
Scope: FsExit, // EXIT PROCEDURE
|
|
Func: func(t *Thread) {
|
|
log = append(log, "EXIT:CLEANUP")
|
|
},
|
|
},
|
|
{
|
|
Name: "MAIN",
|
|
Scope: FsPublic | FsLocal | FsFirst,
|
|
Func: func(t *Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProc()
|
|
log = append(log, "MAIN:running")
|
|
t.RetNil()
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
vm.Run("MAIN")
|
|
// Shutdown already called via defer in Run()
|
|
|
|
if len(log) < 2 {
|
|
t.Fatalf("expected 2 log entries, got %d: %v", len(log), log)
|
|
}
|
|
if log[0] != "MAIN:running" {
|
|
t.Errorf("log[0]: got %q, want MAIN:running", log[0])
|
|
}
|
|
if log[1] != "EXIT:CLEANUP" {
|
|
t.Errorf("log[1]: got %q, want EXIT:CLEANUP", log[1])
|
|
}
|
|
t.Logf("EXIT PROCEDURE executed: %v", log)
|
|
}
|
|
|
|
// === Multiple EXIT in reverse order ===
|
|
|
|
func TestShutdown_ExitReverseOrder(t *testing.T) {
|
|
shutdownOnce = sync.Once{}
|
|
atExitMu.Lock()
|
|
atExitFuncs = nil
|
|
atExitMu.Unlock()
|
|
|
|
vm := NewVM()
|
|
var log []string
|
|
|
|
vm.RegisterModule(&Module{
|
|
Name: "mod_first",
|
|
Symbols: []Symbol{
|
|
{Name: "EXIT_A", Scope: FsExit, Func: func(t *Thread) { log = append(log, "A") }},
|
|
},
|
|
})
|
|
vm.RegisterModule(&Module{
|
|
Name: "mod_second",
|
|
Symbols: []Symbol{
|
|
{Name: "EXIT_B", Scope: FsExit, Func: func(t *Thread) { log = append(log, "B") }},
|
|
},
|
|
})
|
|
vm.RegisterModule(&Module{
|
|
Name: "mod_main",
|
|
Symbols: []Symbol{
|
|
{Name: "MAIN", Scope: FsPublic | FsLocal | FsFirst, Func: func(t *Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProc()
|
|
t.RetNil()
|
|
}},
|
|
},
|
|
})
|
|
|
|
vm.Run("MAIN")
|
|
|
|
// Reverse module order: B first (mod_second), then A (mod_first)
|
|
if len(log) < 2 {
|
|
t.Fatalf("expected 2 exits, got %d: %v", len(log), log)
|
|
}
|
|
if log[0] != "B" || log[1] != "A" {
|
|
t.Errorf("expected [B, A], got %v", log)
|
|
}
|
|
t.Logf("Reverse order: %v", log)
|
|
}
|
|
|
|
// === AtExit callbacks ===
|
|
|
|
func TestShutdown_AtExit(t *testing.T) {
|
|
// Reset global state
|
|
atExitMu.Lock()
|
|
atExitFuncs = nil
|
|
atExitMu.Unlock()
|
|
shutdownOnce = sync.Once{}
|
|
|
|
vm := NewVM()
|
|
var log []string
|
|
|
|
vm.RegisterModule(&Module{
|
|
Name: "mod_main",
|
|
Symbols: []Symbol{
|
|
{Name: "MAIN", Scope: FsPublic | FsLocal | FsFirst, Func: func(t *Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProc()
|
|
// Register AtExit during execution
|
|
AtExit(func() { log = append(log, "atexit_1") })
|
|
AtExit(func() { log = append(log, "atexit_2") })
|
|
AtExit(func() { log = append(log, "atexit_3") })
|
|
log = append(log, "MAIN")
|
|
t.RetNil()
|
|
}},
|
|
},
|
|
})
|
|
|
|
vm.Run("MAIN")
|
|
|
|
// LIFO order: atexit_3, atexit_2, atexit_1
|
|
if len(log) != 4 {
|
|
t.Fatalf("expected 4 entries, got %d: %v", len(log), log)
|
|
}
|
|
if log[0] != "MAIN" {
|
|
t.Errorf("log[0]: got %q", log[0])
|
|
}
|
|
if log[1] != "atexit_3" || log[2] != "atexit_2" || log[3] != "atexit_1" {
|
|
t.Errorf("LIFO order wrong: %v", log[1:])
|
|
}
|
|
t.Logf("AtExit LIFO: %v", log)
|
|
}
|
|
|
|
// === WorkArea auto-close ===
|
|
|
|
type mockWorkArea struct {
|
|
closed bool
|
|
log *[]string
|
|
}
|
|
|
|
func (m *mockWorkArea) CloseAll() {
|
|
m.closed = true
|
|
*m.log = append(*m.log, "WA:CloseAll")
|
|
}
|
|
|
|
func TestShutdown_WorkAreaClose(t *testing.T) {
|
|
shutdownOnce = sync.Once{}
|
|
atExitMu.Lock()
|
|
atExitFuncs = nil
|
|
atExitMu.Unlock()
|
|
|
|
vm := NewVM()
|
|
var log []string
|
|
wa := &mockWorkArea{log: &log}
|
|
|
|
vm.RegisterModule(&Module{
|
|
Name: "mod_main",
|
|
Symbols: []Symbol{
|
|
{Name: "MAIN", Scope: FsPublic | FsLocal | FsFirst, Func: func(t *Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProc()
|
|
t.WA = wa
|
|
log = append(log, "MAIN:using_db")
|
|
t.RetNil()
|
|
}},
|
|
},
|
|
})
|
|
|
|
vm.Run("MAIN")
|
|
|
|
if !wa.closed {
|
|
t.Error("WorkArea was NOT closed on shutdown")
|
|
}
|
|
found := false
|
|
for _, entry := range log {
|
|
if entry == "WA:CloseAll" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("WA:CloseAll not in log")
|
|
}
|
|
t.Logf("WorkArea cleanup: %v", log)
|
|
}
|
|
|
|
// === Panic in cleanup doesn't crash ===
|
|
|
|
func TestShutdown_PanicSafe(t *testing.T) {
|
|
shutdownOnce = sync.Once{}
|
|
atExitMu.Lock()
|
|
atExitFuncs = nil
|
|
atExitMu.Unlock()
|
|
|
|
vm := NewVM()
|
|
var log []string
|
|
|
|
vm.RegisterModule(&Module{
|
|
Name: "mod_main",
|
|
Symbols: []Symbol{
|
|
{Name: "EXIT_PANIC", Scope: FsExit, Func: func(t *Thread) {
|
|
log = append(log, "before_panic")
|
|
panic("test panic in EXIT")
|
|
}},
|
|
{Name: "MAIN", Scope: FsPublic | FsLocal | FsFirst, Func: func(t *Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProc()
|
|
AtExit(func() { log = append(log, "atexit_after_panic") })
|
|
log = append(log, "MAIN")
|
|
t.RetNil()
|
|
}},
|
|
},
|
|
})
|
|
|
|
// Should NOT panic — safeCall catches it
|
|
vm.Run("MAIN")
|
|
|
|
if !logContains(log, "before_panic") {
|
|
t.Error("EXIT proc didn't run before panic")
|
|
}
|
|
if !logContains(log, "atexit_after_panic") {
|
|
t.Error("AtExit didn't run after EXIT panic")
|
|
}
|
|
t.Logf("Panic-safe cleanup: %v", log)
|
|
}
|
|
|
|
// === Shutdown runs only once ===
|
|
|
|
func TestShutdown_OnlyOnce(t *testing.T) {
|
|
shutdownOnce = sync.Once{}
|
|
atExitMu.Lock()
|
|
atExitFuncs = nil
|
|
atExitMu.Unlock()
|
|
|
|
vm := NewVM()
|
|
count := 0
|
|
|
|
AtExit(func() { count++ })
|
|
|
|
vm.RegisterModule(&Module{
|
|
Name: "mod_main",
|
|
Symbols: []Symbol{
|
|
{Name: "MAIN", Scope: FsPublic | FsLocal | FsFirst, Func: func(t *Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProc()
|
|
t.RetNil()
|
|
}},
|
|
},
|
|
})
|
|
|
|
vm.Run("MAIN")
|
|
// Run() calls Shutdown() via defer — count should be 1
|
|
|
|
// Call again explicitly — should NOT run again
|
|
vm.Shutdown()
|
|
vm.Shutdown()
|
|
|
|
if count != 1 {
|
|
t.Errorf("Shutdown ran %d times, expected 1", count)
|
|
}
|
|
t.Logf("Shutdown ran exactly once: count=%d", count)
|
|
}
|
|
|
|
// === Static variables cleared ===
|
|
|
|
func TestShutdown_StaticsClear(t *testing.T) {
|
|
shutdownOnce = sync.Once{}
|
|
atExitMu.Lock()
|
|
atExitFuncs = nil
|
|
atExitMu.Unlock()
|
|
|
|
vm := NewVM()
|
|
|
|
// Set some statics
|
|
vm.mu.Lock()
|
|
vm.statics["TEST"] = []Value{MakeString("hello"), MakeInt(42)}
|
|
vm.mu.Unlock()
|
|
|
|
vm.RegisterModule(&Module{
|
|
Name: "mod_main",
|
|
Symbols: []Symbol{
|
|
{Name: "MAIN", Scope: FsPublic | FsLocal | FsFirst, Func: func(t *Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProc()
|
|
t.RetNil()
|
|
}},
|
|
},
|
|
})
|
|
|
|
vm.Run("MAIN")
|
|
|
|
// Statics should be cleared
|
|
vm.mu.RLock()
|
|
for _, vals := range vm.statics {
|
|
for _, v := range vals {
|
|
if !v.IsNil() {
|
|
t.Errorf("static not cleared: %v", v)
|
|
}
|
|
}
|
|
}
|
|
vm.mu.RUnlock()
|
|
t.Log("Statics cleared on shutdown")
|
|
}
|
|
|
|
// === Full sequence order ===
|
|
|
|
func TestShutdown_FullSequence(t *testing.T) {
|
|
shutdownOnce = sync.Once{}
|
|
atExitMu.Lock()
|
|
atExitFuncs = nil
|
|
atExitMu.Unlock()
|
|
|
|
vm := NewVM()
|
|
var log []string
|
|
wa := &mockWorkArea{log: &log}
|
|
|
|
vm.RegisterModule(&Module{
|
|
Name: "mod_main",
|
|
Symbols: []Symbol{
|
|
{Name: "EXIT_PROC", Scope: FsExit, Func: func(t *Thread) {
|
|
log = append(log, "1:EXIT_PROC")
|
|
}},
|
|
{Name: "MAIN", Scope: FsPublic | FsLocal | FsFirst, Func: func(t *Thread) {
|
|
t.Frame(0, 0)
|
|
defer t.EndProc()
|
|
t.WA = wa
|
|
AtExit(func() { log = append(log, "2:ATEXIT") })
|
|
log = append(log, "0:MAIN")
|
|
t.RetNil()
|
|
}},
|
|
},
|
|
})
|
|
|
|
vm.SetOnExit(func() { log = append(log, "4:ONEXIT") })
|
|
vm.Run("MAIN")
|
|
|
|
// Expected order:
|
|
// 0:MAIN (during execution)
|
|
// 1:EXIT_PROC (phase 1)
|
|
// 2:ATEXIT (phase 2)
|
|
// WA:CloseAll (phase 3)
|
|
// 4:ONEXIT (phase 7)
|
|
|
|
t.Logf("Full sequence: %v", log)
|
|
|
|
expected := []string{"0:MAIN", "1:EXIT_PROC", "2:ATEXIT", "WA:CloseAll", "4:ONEXIT"}
|
|
if len(log) != len(expected) {
|
|
t.Fatalf("expected %d entries, got %d: %v", len(expected), len(log), log)
|
|
}
|
|
for i, want := range expected {
|
|
if log[i] != want {
|
|
t.Errorf("log[%d]: got %q, want %q", i, log[i], want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func logContains(list []string, s string) bool {
|
|
for _, item := range list {
|
|
if strings.Contains(item, s) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|