Skip to content

Commit 2dddd90

Browse files
Track busy/idle state for integration tests (#2765)
2 parents 585ea36 + 16ed3c2 commit 2dddd90

File tree

91 files changed

+1081
-402
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+1081
-402
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,8 @@ jobs:
6868
restore-keys: |
6969
${{runner.os}}-go-
7070
- name: Test code
71-
# LONG_WAIT_BEFORE_FAIL means that for a given test assertion, we'll wait longer before failing
7271
run: |
73-
LONG_WAIT_BEFORE_FAIL=true go test pkg/integration/clients/*.go
72+
go test pkg/integration/clients/*.go
7473
build:
7574
runs-on: ubuntu-latest
7675
env:

docs/Integration_Tests.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
* [Keybindings](./keybindings)
77
* [Undo/Redo](./Undoing.md)
88
* [Searching/Filtering](./Searching.md)
9+
* [Dev docs](./dev)

docs/dev/Busy.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Knowing when Lazygit is busy/idle
2+
3+
## The use-case
4+
5+
This topic deserves its own doc because there there are a few touch points for it. We have a use-case for knowing when Lazygit is idle or busy because integration tests follow the following process:
6+
1) press a key
7+
2) wait until Lazygit is idle
8+
3) run assertion / press another key
9+
4) repeat
10+
11+
In the past the process was:
12+
1) press a key
13+
2) run assertion
14+
3) if assertion fails, wait a bit and retry
15+
4) repeat
16+
17+
The old process was problematic because an assertion may give a false positive due to the contents of some view not yet having changed since the last key was pressed.
18+
19+
## The solution
20+
21+
First, it's important to distinguish three different types of goroutines:
22+
* The UI goroutine, of which there is only one, which infinitely processes a queue of events
23+
* Worker goroutines, which do some work and then typically enqueue an event in the UI goroutine to display the results
24+
* Background goroutines, which periodically spawn worker goroutines (e.g. doing a git fetch every minute)
25+
26+
The point of distinguishing worker goroutines from background goroutines is that when any worker goroutine is running, we consider Lazygit to be 'busy', whereas this is not the case with background goroutines. It would be pointless to have background goroutines be considered 'busy' because then Lazygit would be considered busy for the entire duration of the program!
27+
28+
In gocui, the underlying package we use for managing the UI and events, we keep track of how many busy goroutines there are using the `Task` type. A task represents some work being done by lazygit. The gocui Gui struct holds a map of tasks and allows creating a new task (which adds it to the map), pausing/continuing a task, and marking a task as done (which removes it from the map). Lazygit is considered to be busy so long as there is at least one busy task in the map; otherwise it's considered idle. When Lazygit goes from busy to idle, it notifies the integration test.
29+
30+
It's important that we play by the rules below to ensure that after the user does anything, all the processing that follows happens in a contiguous block of busy-ness with no gaps.
31+
32+
### Spawning a worker goroutine
33+
34+
Here's the basic implementation of `OnWorker` (using the same flow as `WaitGroup`s):
35+
36+
```go
37+
func (g *Gui) OnWorker(f func(*Task)) {
38+
task := g.NewTask()
39+
go func() {
40+
f(task)
41+
task.Done()
42+
}()
43+
}
44+
```
45+
46+
The crucial thing here is that we create the task _before_ spawning the goroutine, because it means that we'll have at least one busy task in the map until the completion of the goroutine. If we created the task within the goroutine, the current function could exit and Lazygit would be considered idle before the goroutine starts, leading to our integration test prematurely progressing.
47+
48+
You typically invoke this with `self.c.OnWorker(f)`. Note that the callback function receives the task. This allows the callback to pause/continue the task (see below).
49+
50+
### Spawning a background goroutine
51+
52+
Spawning a background goroutine is as simple as:
53+
54+
```go
55+
go utils.Safe(f)
56+
```
57+
58+
Where `utils.Safe` is a helper function that ensures we clean up the gui if the goroutine panics.
59+
60+
### Programmatically enqueing a UI event
61+
62+
This is invoked with `self.c.OnUIThread(f)`. Internally, it creates a task before enqueuing the function as an event (including the task in the event struct) and once that event is processed by the event queue (and any other pending events are processed) the task is removed from the map by calling `task.Done()`.
63+
64+
### Pressing a key
65+
66+
If the user presses a key, an event will be enqueued automatically and a task will be created before (and `Done`'d after) the event is processed.
67+
68+
## Special cases
69+
70+
There are a couple of special cases where we manually pause/continue the task directly in the client code. These are subject to change but for the sake of completeness:
71+
72+
### Writing to the main view(s)
73+
74+
If the user focuses a file in the files panel, we run a `git diff` command for that file and write the output to the main view. But we only read enough of the command's output to fill the view's viewport: further loading only happens if the user scrolls. Given that we have a background goroutine for running the command and writing more output upon scrolling, we create our own task and call `Done` on it as soon as the viewport is filled.
75+
76+
### Requesting credentials from a git command
77+
78+
Some git commands (e.g. git push) may request credentials. This is the same deal as above; we use a worker goroutine and manually pause continue its task as we go from waiting on the git command to waiting on user input. This requires passing the task through to the `Push` method so that it can be paused/continued.

docs/dev/Integration_Tests.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
see new docs [here](../../pkg/integration/README.md)

docs/dev/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Dev Documentation Overview
2+
3+
* [Busy/Idle tracking](./Busy.md).
4+
* [Integration Tests](../../pkg/integration/README.md)

go.mod

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ require (
1818
github.com/integrii/flaggy v1.4.0
1919
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
2020
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
21-
github.com/jesseduffield/gocui v0.3.1-0.20230702054502-d6c452fc12ce
21+
github.com/jesseduffield/gocui v0.3.1-0.20230710004407-9bbfd873713b
2222
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
2323
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
2424
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
@@ -67,8 +67,8 @@ require (
6767
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
6868
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8 // indirect
6969
golang.org/x/net v0.7.0 // indirect
70-
golang.org/x/sys v0.9.0 // indirect
71-
golang.org/x/term v0.9.0 // indirect
72-
golang.org/x/text v0.10.0 // indirect
70+
golang.org/x/sys v0.10.0 // indirect
71+
golang.org/x/term v0.10.0 // indirect
72+
golang.org/x/text v0.11.0 // indirect
7373
gopkg.in/warnings.v0 v0.1.2 // indirect
7474
)

go.sum

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
7272
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
7373
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
7474
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
75-
github.com/jesseduffield/gocui v0.3.1-0.20230702054502-d6c452fc12ce h1:Xgm21B1an/outcRxnkDfMT6wKb6SKBR05jXOyfPA8WQ=
76-
github.com/jesseduffield/gocui v0.3.1-0.20230702054502-d6c452fc12ce/go.mod h1:dJ/BEUt3OWtaRg/PmuJWendRqREhre9JQ1SLvqrVJ8s=
75+
github.com/jesseduffield/gocui v0.3.1-0.20230710004407-9bbfd873713b h1:8FmmdaYHes1m3oNyNdS+VIgkgkFpNZAWuwTnvp0tG14=
76+
github.com/jesseduffield/gocui v0.3.1-0.20230710004407-9bbfd873713b/go.mod h1:dJ/BEUt3OWtaRg/PmuJWendRqREhre9JQ1SLvqrVJ8s=
7777
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
7878
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
7979
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
@@ -207,21 +207,21 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
207207
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
208208
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
209209
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
210-
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
211-
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
210+
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
211+
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
212212
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
213213
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
214214
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
215215
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
216-
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
217-
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
216+
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
217+
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
218218
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
219219
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
220220
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
221221
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
222222
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
223-
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
224-
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
223+
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
224+
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
225225
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
226226
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
227227
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

pkg/commands/git_cmd_obj_runner.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
package commands
22

33
import (
4+
"strings"
5+
"time"
6+
47
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
58
"github.com/sirupsen/logrus"
69
)
710

811
// here we're wrapping the default command runner in some git-specific stuff e.g. retry logic if we get an error due to the presence of .git/index.lock
912

13+
const (
14+
WaitTime = 50 * time.Millisecond
15+
RetryCount = 5
16+
)
17+
1018
type gitCmdObjRunner struct {
1119
log *logrus.Entry
1220
innerRunner oscommands.ICmdObjRunner
@@ -18,13 +26,44 @@ func (self *gitCmdObjRunner) Run(cmdObj oscommands.ICmdObj) error {
1826
}
1927

2028
func (self *gitCmdObjRunner) RunWithOutput(cmdObj oscommands.ICmdObj) (string, error) {
21-
return self.innerRunner.RunWithOutput(cmdObj)
29+
var output string
30+
var err error
31+
for i := 0; i < RetryCount; i++ {
32+
newCmdObj := cmdObj.Clone()
33+
output, err = self.innerRunner.RunWithOutput(newCmdObj)
34+
35+
if err == nil || !strings.Contains(output, ".git/index.lock") {
36+
return output, err
37+
}
38+
39+
// if we have an error based on the index lock, we should wait a bit and then retry
40+
self.log.Warn("index.lock prevented command from running. Retrying command after a small wait")
41+
time.Sleep(WaitTime)
42+
}
43+
44+
return output, err
2245
}
2346

2447
func (self *gitCmdObjRunner) RunWithOutputs(cmdObj oscommands.ICmdObj) (string, string, error) {
25-
return self.innerRunner.RunWithOutputs(cmdObj)
48+
var stdout, stderr string
49+
var err error
50+
for i := 0; i < RetryCount; i++ {
51+
newCmdObj := cmdObj.Clone()
52+
stdout, stderr, err = self.innerRunner.RunWithOutputs(newCmdObj)
53+
54+
if err == nil || !strings.Contains(stdout+stderr, ".git/index.lock") {
55+
return stdout, stderr, err
56+
}
57+
58+
// if we have an error based on the index lock, we should wait a bit and then retry
59+
self.log.Warn("index.lock prevented command from running. Retrying command after a small wait")
60+
time.Sleep(WaitTime)
61+
}
62+
63+
return stdout, stderr, err
2664
}
2765

66+
// Retry logic not implemented here, but these commands typically don't need to obtain a lock.
2867
func (self *gitCmdObjRunner) RunAndProcessLines(cmdObj oscommands.ICmdObj, onLine func(line string) (bool, error)) error {
2968
return self.innerRunner.RunAndProcessLines(cmdObj, onLine)
3069
}

pkg/commands/git_commands/remote.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package git_commands
22

33
import (
44
"fmt"
5+
6+
"github.com/jesseduffield/gocui"
57
)
68

79
type RemoteCommands struct {
@@ -46,12 +48,12 @@ func (self *RemoteCommands) UpdateRemoteUrl(remoteName string, updatedUrl string
4648
return self.cmd.New(cmdArgs).Run()
4749
}
4850

49-
func (self *RemoteCommands) DeleteRemoteBranch(remoteName string, branchName string) error {
51+
func (self *RemoteCommands) DeleteRemoteBranch(task gocui.Task, remoteName string, branchName string) error {
5052
cmdArgs := NewGitCmd("push").
5153
Arg(remoteName, "--delete", branchName).
5254
ToArgv()
5355

54-
return self.cmd.New(cmdArgs).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
56+
return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).WithMutex(self.syncMutex).Run()
5557
}
5658

5759
// CheckRemoteBranchExists Returns remote branch

0 commit comments

Comments
 (0)