Wait for the event loop to exit in integration tests

The test harness enqueued ErrQuit after a test finished, waited for the
program to go idle, then slept a fixed second and declared "gocui should
have already exited" if it hadn't. That fixed grace is fragile: under the
race detector the shutdown legitimately takes longer than a second, so
nearly every test failed with that message even though nothing was wrong.

Wait for the main loop to actually return instead. gocui now closes a
loopExited channel when MainLoop exits, and the harness blocks on it; the
existing 40s watchdog still fails a test whose loop genuinely never quits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-07-09 10:25:13 +02:00
parent 435e02efa8
commit 59ed1517bc
2 changed files with 14 additions and 5 deletions

View file

@ -148,6 +148,10 @@ type Gui struct {
maxX, maxY int
outputMode OutputMode
stop chan struct{}
// loopExited is closed when MainLoop returns, so callers (e.g. the
// integration-test harness) can wait for the event loop to actually finish
// rather than polling or sleeping a fixed interval.
loopExited chan struct{}
// BgColor and FgColor allow to configure the background and foreground
// colors of the GUI.
@ -260,6 +264,7 @@ func NewGui(opts NewGuiOpts) (*Gui, error) {
g.outputMode = opts.OutputMode
g.stop = make(chan struct{})
g.loopExited = make(chan struct{})
g.gEvents = make(chan GocuiEvent, 20)
g.userEvents = newUserEventQueue()
@ -348,6 +353,11 @@ func (g *Gui) Close() {
Screen.Fini()
}
// LoopExited returns a channel that is closed once MainLoop has returned.
func (g *Gui) LoopExited() <-chan struct{} {
return g.loopExited
}
// Size returns the terminal's size.
func (g *Gui) Size() (x, y int) {
return g.maxX, g.maxY
@ -965,6 +975,8 @@ func (g *Gui) SetManagerFunc(manager func(*Gui) error) {
// MainLoop runs the main loop until an error is returned. A successful
// finish should return ErrQuit.
func (g *Gui) MainLoop() error {
defer close(g.loopExited)
g.uiThreadID.Store(goid.Get())
go func() {

View file

@ -40,11 +40,8 @@ func (gui *Gui) handleTestMode() {
return gocui.ErrQuit
})
waitUntilIdle()
time.Sleep(time.Second * 1)
log.Fatal("gocui should have already exited")
// Wait for the event loop to actually exit.
<-gui.g.LoopExited()
}()
if os.Getenv(components.WAIT_FOR_DEBUGGER_ENV_VAR) == "" {