mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Track replayed test input as busy from the moment it is submitted
Integration tests synchronize with lazygit through the task manager: after submitting an input event, the test driver waits until the program goes idle before asserting. But a submitted event only got its task once the main loop picked it up from the events channel; while it was still in flight (handed to the poller goroutine, or sitting in the channel), no task existed for it, so the program could look idle even though input was still pending. The edge-triggered idle protocol mostly papers over this: each wait is satisfied by the *next* busy-to-idle transition, which in practice is the one produced by processing the submitted event. It only goes wrong when some other task (e.g. a background refresh) completes in that window, producing an edge the waiting test mistakes for its own — a rare source of test flakes. The next commit replaces that protocol with a level-triggered one, for which the window would be fatal rather than rare: a wait falling into the gap would return immediately. Close the gap by creating the task on the test goroutine before the event is submitted, and carrying it through the poller into the main loop, which uses it instead of creating its own. The new Replay* methods own this invariant, and the replayed-events channels are no longer exported, so tests can't submit an untracked event. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
7e1073a0ee
commit
664a65d584
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue