diff --git a/pkg/gocui/gui.go b/pkg/gocui/gui.go index ceb570c59..e5588e262 100644 --- a/pkg/gocui/gui.go +++ b/pkg/gocui/gui.go @@ -125,8 +125,11 @@ type clickInfo struct { // and keybindings. type Gui struct { RecordingConfig - // ReplayedEvents is for passing pre-recorded input events, for the purposes of testing - ReplayedEvents replayedEvents + // replayedEvents is for passing simulated input events, for the purposes + // of testing. Events must be submitted through the Replay* methods, which + // attach a task to each event; pushing into the channels directly would + // bypass the busy-tracking that integration tests rely on. + replayedEvents replayedEvents playRecording bool tabClickBindings []*tabClickBinding @@ -255,7 +258,7 @@ func NewGui(opts NewGuiOpts) (*Gui, error) { g.taskManager = newTaskManager() if opts.PlayRecording { - g.ReplayedEvents = replayedEvents{ + g.replayedEvents = replayedEvents{ Keys: make(chan *TcellKeyEventWrapper), Resizes: make(chan *TcellResizeEventWrapper), MouseEvents: make(chan *TcellMouseEventWrapper), @@ -291,6 +294,30 @@ func (g *Gui) NewBackgroundTask() *TaskImpl { return g.taskManager.NewTask(true) } +// ReplayKeyEvent simulates a key press, as if the user had typed it. It's used +// by integration tests. The event carries a task, so that the program counts +// as busy from before the event is submitted until the main loop has fully +// processed it; the test driver relies on this when it waits for the program +// to go idle after submitting an event. (If the task were only created once +// the main loop picks the event up, there would be a window in which the event +// is still in flight but nothing counts as busy.) +func (g *Gui) ReplayKeyEvent(ev *TcellKeyEventWrapper) { + ev.task = g.NewTask() + g.replayedEvents.Keys <- ev +} + +// ReplayMouseEvent is like ReplayKeyEvent, but for mouse events. +func (g *Gui) ReplayMouseEvent(ev *TcellMouseEventWrapper) { + ev.task = g.NewTask() + g.replayedEvents.MouseEvents <- ev +} + +// ReplayFocusEvent is like ReplayKeyEvent, but for focus events. +func (g *Gui) ReplayFocusEvent(ev *TcellFocusEventWrapper) { + ev.task = g.NewTask() + g.replayedEvents.FocusEvents <- ev +} + // Busy reports whether any foreground work is in flight, ignoring the event // currently being processed on the main goroutine (see currentTask). Background // routines (auto-fetch etc.) don't count. It's used to decide whether it's safe @@ -948,7 +975,12 @@ func (g *Gui) processEvent() error { // are always the primary event here. select { case ev := <-g.gEvents: - task := g.NewTask() + // Replayed test events already carry their task (see ReplayKeyEvent); + // organic events get theirs here. + task := ev.task + if task == nil { + task = g.NewTask() + } g.currentTask = task defer func() { g.currentTask = nil; task.Done() }() @@ -992,7 +1024,11 @@ func (g *Gui) processRemainingEvents() (bool, error) { select { case ev := <-g.gEvents: contentOnly = false - if err := g.handleError(g.handleEvent(&ev)); err != nil { + err := g.handleError(g.handleEvent(&ev)) + if ev.task != nil { + ev.task.Done() + } + if err != nil { return false, err } default: diff --git a/pkg/gocui/tcell_driver.go b/pkg/gocui/tcell_driver.go index 857e35bfc..885bcbabb 100644 --- a/pkg/gocui/tcell_driver.go +++ b/pkg/gocui/tcell_driver.go @@ -172,6 +172,12 @@ type GocuiEvent struct { Focused bool Start bool N int + + // task tracks the processing of this event for idle detection. Events + // replayed by integration tests carry a task from the moment they are + // submitted (see Gui.ReplayKeyEvent); for organic events it is nil, and + // the main loop creates a task when it picks the event up. + task Task } // Event types. @@ -208,6 +214,8 @@ type TcellKeyEventWrapper struct { Mod tcell.ModMask Key tcell.Key Ch string + + task Task // see GocuiEvent.task } func NewTcellKeyEventWrapper(event *tcell.EventKey, timestamp int64) *TcellKeyEventWrapper { @@ -229,6 +237,8 @@ type TcellMouseEventWrapper struct { Y int ButtonMask tcell.ButtonMask ModMask tcell.ModMask + + task Task // see GocuiEvent.task } func NewTcellMouseEventWrapper(event *tcell.EventMouse, timestamp int64) *TcellMouseEventWrapper { @@ -269,6 +279,8 @@ func (wrapper TcellResizeEventWrapper) toTcellEvent() tcell.Event { type TcellFocusEventWrapper struct { Timestamp int64 Focused bool + + task Task // see GocuiEvent.task } func NewTcellFocusEventWrapper(event *tcell.EventFocus, timestamp int64) *TcellFocusEventWrapper { @@ -285,22 +297,28 @@ func (wrapper TcellFocusEventWrapper) toTcellEvent() tcell.Event { // pollEvent get tcell.Event and transform it into gocuiEvent func (g *Gui) pollEvent() GocuiEvent { var tev tcell.Event + var task Task if g.playRecording { select { - case ev := <-g.ReplayedEvents.Keys: + case ev := <-g.replayedEvents.Keys: tev = (ev).toTcellEvent() - case ev := <-g.ReplayedEvents.Resizes: + task = ev.task + case ev := <-g.replayedEvents.Resizes: tev = (ev).toTcellEvent() - case ev := <-g.ReplayedEvents.MouseEvents: + case ev := <-g.replayedEvents.MouseEvents: tev = (ev).toTcellEvent() - case ev := <-g.ReplayedEvents.FocusEvents: + task = ev.task + case ev := <-g.replayedEvents.FocusEvents: tev = (ev).toTcellEvent() + task = ev.task } } else { tev = <-Screen.EventQ() } - return gocuiEventFromTcellEvent(tev) + event := gocuiEventFromTcellEvent(tev) + event.task = task + return event } func gocuiEventFromTcellEvent(tev tcell.Event) GocuiEvent { diff --git a/pkg/gui/gui_driver.go b/pkg/gui/gui_driver.go index fef33bf66..a54530791 100644 --- a/pkg/gui/gui_driver.go +++ b/pkg/gui/gui_driver.go @@ -33,10 +33,10 @@ func (self *GuiDriver) PressKey(keyStr string) { self.Fail("Unrecognized key: " + keyStr) } - self.gui.g.ReplayedEvents.Keys <- gocui.NewTcellKeyEventWrapper( + self.gui.g.ReplayKeyEvent(gocui.NewTcellKeyEventWrapper( tcell.NewEventKey(tcell.Key(key.KeyName()), key.Str(), tcell.ModMask(key.Mod())), 0, - ) + )) self.waitTillIdle() } @@ -44,15 +44,15 @@ func (self *GuiDriver) PressKey(keyStr string) { func (self *GuiDriver) Click(x, y int) { self.CheckAllToastsAcknowledged() - self.gui.g.ReplayedEvents.MouseEvents <- gocui.NewTcellMouseEventWrapper( + self.gui.g.ReplayMouseEvent(gocui.NewTcellMouseEventWrapper( tcell.NewEventMouse(x, y, tcell.ButtonPrimary, 0), 0, - ) + )) self.waitTillIdle() - self.gui.g.ReplayedEvents.MouseEvents <- gocui.NewTcellMouseEventWrapper( + self.gui.g.ReplayMouseEvent(gocui.NewTcellMouseEventWrapper( tcell.NewEventMouse(x, y, tcell.ButtonNone, 0), 0, - ) + )) self.waitTillIdle() } @@ -60,10 +60,10 @@ func (self *GuiDriver) Click(x, y int) { // learns to reload changed config files. Tests use it to exercise the live // config-reload path. func (self *GuiDriver) FocusIn() { - self.gui.g.ReplayedEvents.FocusEvents <- gocui.NewTcellFocusEventWrapper( + self.gui.g.ReplayFocusEvent(gocui.NewTcellFocusEventWrapper( tcell.NewEventFocus(true), 0, - ) + )) self.waitTillIdle() }