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 }