1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-03-21 21:47:32 +02:00

Add busy count for integration tests

Integration tests need to be notified when Lazygit is idle so they can progress to the next assertion / user action.
This commit is contained in:
Jesse Duffield 2023-07-03 14:16:43 +10:00
parent 631cf1e873
commit 6c4e7ee972
23 changed files with 184 additions and 78 deletions

View File

@ -79,7 +79,7 @@ func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan stru
if self.pauseBackgroundThreads { if self.pauseBackgroundThreads {
continue continue
} }
_ = function() self.gui.c.OnWorker(func() { _ = function() })
case <-stop: case <-stop:
return return
} }

View File

@ -30,7 +30,7 @@ func NewSuggestionsContext(
c *ContextCommon, c *ContextCommon,
) *SuggestionsContext { ) *SuggestionsContext {
state := &SuggestionsContextState{ state := &SuggestionsContextState{
AsyncHandler: tasks.NewAsyncHandler(), AsyncHandler: tasks.NewAsyncHandler(c.OnWorker),
} }
getModel := func() []*types.Suggestion { getModel := func() []*types.Suggestion {
return state.Suggestions return state.Suggestions

View File

@ -4,7 +4,6 @@ import (
"time" "time"
"github.com/jesseduffield/lazygit/pkg/gui/status" "github.com/jesseduffield/lazygit/pkg/gui/status"
"github.com/jesseduffield/lazygit/pkg/utils"
) )
type AppStatusHelper struct { type AppStatusHelper struct {
@ -28,7 +27,7 @@ func (self *AppStatusHelper) Toast(message string) {
// withWaitingStatus wraps a function and shows a waiting status while the function is still executing // withWaitingStatus wraps a function and shows a waiting status while the function is still executing
func (self *AppStatusHelper) WithWaitingStatus(message string, f func() error) { func (self *AppStatusHelper) WithWaitingStatus(message string, f func() error) {
go utils.Safe(func() { self.c.OnWorker(func() {
self.statusMgr().WithWaitingStatus(message, func() { self.statusMgr().WithWaitingStatus(message, func() {
self.renderAppStatus() self.renderAppStatus()
@ -50,7 +49,7 @@ func (self *AppStatusHelper) GetStatusString() string {
} }
func (self *AppStatusHelper) renderAppStatus() { func (self *AppStatusHelper) renderAppStatus() {
go utils.Safe(func() { self.c.OnWorker(func() {
ticker := time.NewTicker(time.Millisecond * 50) ticker := time.NewTicker(time.Millisecond * 50)
defer ticker.Stop() defer ticker.Stop()
for range ticker.C { for range ticker.C {

View File

@ -90,7 +90,7 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) error {
wg.Add(1) wg.Add(1)
func() { func() {
if options.Mode == types.ASYNC { if options.Mode == types.ASYNC {
go utils.Safe(f) self.c.OnWorker(f)
} else { } else {
f() f()
} }
@ -206,7 +206,7 @@ func getModeName(mode types.RefreshMode) string {
func (self *RefreshHelper) refreshReflogCommitsConsideringStartup() { func (self *RefreshHelper) refreshReflogCommitsConsideringStartup() {
switch self.c.State().GetRepoState().GetStartupStage() { switch self.c.State().GetRepoState().GetStartupStage() {
case types.INITIAL: case types.INITIAL:
go utils.Safe(func() { self.c.OnWorker(func() {
_ = self.refreshReflogCommits() _ = self.refreshReflogCommits()
self.refreshBranches() self.refreshBranches()
self.c.State().GetRepoState().SetStartupStage(types.COMPLETE) self.c.State().GetRepoState().SetStartupStage(types.COMPLETE)

View File

@ -816,7 +816,7 @@ func (self *LocalCommitsController) GetOnFocus() func(types.OnFocusOpts) error {
context := self.context() context := self.context()
if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() { if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() {
context.SetLimitCommits(false) context.SetLimitCommits(false)
go utils.Safe(func() { self.c.OnWorker(func() {
if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}}); err != nil { if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}}); err != nil {
_ = self.c.Error(err) _ = self.c.Error(err)
} }

View File

@ -3,7 +3,6 @@ package controllers
import ( import (
"github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
) )
type SubCommitsController struct { type SubCommitsController struct {
@ -60,7 +59,7 @@ func (self *SubCommitsController) GetOnFocus() func(types.OnFocusOpts) error {
context := self.context() context := self.context()
if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() { if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() {
context.SetLimitCommits(false) context.SetLimitCommits(false)
go utils.Safe(func() { self.c.OnWorker(func() {
if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUB_COMMITS}}); err != nil { if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUB_COMMITS}}); err != nil {
_ = self.c.Error(err) _ = self.c.Error(err)
} }

View File

@ -120,7 +120,10 @@ func (gui *Gui) WatchFilesForChanges() {
} }
// only refresh if we're not already // only refresh if we're not already
if !gui.IsRefreshingFiles { if !gui.IsRefreshingFiles {
_ = gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) gui.c.OnUIThread(func() error {
// TODO: find out if refresh needs to be run on the UI thread
return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
})
} }
// watch for errors // watch for errors

View File

@ -130,6 +130,8 @@ type Gui struct {
c *helpers.HelperCommon c *helpers.HelperCommon
helpers *helpers.Helpers helpers *helpers.Helpers
integrationTest integrationTypes.IntegrationTest
} }
type StateAccessor struct { type StateAccessor struct {
@ -472,6 +474,7 @@ func NewGui(
func(message string, f func() error) { gui.helpers.AppStatus.WithWaitingStatus(message, f) }, func(message string, f func() error) { gui.helpers.AppStatus.WithWaitingStatus(message, f) },
func(message string) { gui.helpers.AppStatus.Toast(message) }, func(message string) { gui.helpers.AppStatus.Toast(message) },
func() string { return gui.Views.Confirmation.TextArea.GetContent() }, func() string { return gui.Views.Confirmation.TextArea.GetContent() },
func(f func()) { gui.c.OnWorker(f) },
) )
guiCommon := &guiCommon{gui: gui, IPopupHandler: gui.PopupHandler} guiCommon := &guiCommon{gui: gui, IPopupHandler: gui.PopupHandler}
@ -620,7 +623,8 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error {
gui.c.Log.Info("starting main loop") gui.c.Log.Info("starting main loop")
gui.handleTestMode(startArgs.IntegrationTest) // setting here so we can use it in layout.go
gui.integrationTest = startArgs.IntegrationTest
return gui.g.MainLoop() return gui.g.MainLoop()
} }
@ -779,16 +783,15 @@ func (gui *Gui) showInitialPopups(tasks []func(chan struct{}) error) {
gui.waitForIntro.Add(len(tasks)) gui.waitForIntro.Add(len(tasks))
done := make(chan struct{}) done := make(chan struct{})
go utils.Safe(func() { gui.c.OnWorker(func() {
for _, task := range tasks { for _, task := range tasks {
task := task if err := task(done); err != nil {
go utils.Safe(func() { _ = gui.c.Error(err)
if err := task(done); err != nil { }
_ = gui.c.Error(err)
}
})
gui.g.DecrementBusyCount()
<-done <-done
gui.g.IncrementBusyCount()
gui.waitForIntro.Done() gui.waitForIntro.Done()
} }
}) })
@ -796,9 +799,10 @@ func (gui *Gui) showInitialPopups(tasks []func(chan struct{}) error) {
func (gui *Gui) showIntroPopupMessage(done chan struct{}) error { func (gui *Gui) showIntroPopupMessage(done chan struct{}) error {
onConfirm := func() error { onConfirm := func() error {
done <- struct{}{}
gui.c.GetAppState().StartupPopupVersion = StartupPopupVersion gui.c.GetAppState().StartupPopupVersion = StartupPopupVersion
return gui.c.SaveAppState() err := gui.c.SaveAppState()
done <- struct{}{}
return err
} }
return gui.c.Confirm(types.ConfirmOpts{ return gui.c.Confirm(types.ConfirmOpts{
@ -828,6 +832,10 @@ func (gui *Gui) onUIThread(f func() error) {
}) })
} }
func (gui *Gui) onWorker(f func()) {
gui.g.OnWorker(f)
}
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions { func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
return gui.helpers.WindowArrangement.GetWindowDimensions(informationStr, appStatus) return gui.helpers.WindowArrangement.GetWindowDimensions(informationStr, appStatus)
} }

View File

@ -136,6 +136,10 @@ func (self *guiCommon) OnUIThread(f func() error) {
self.gui.onUIThread(f) self.gui.onUIThread(f)
} }
func (self *guiCommon) OnWorker(f func()) {
self.gui.onWorker(f)
}
func (self *guiCommon) RenderToMainViews(opts types.RefreshMainOpts) error { func (self *guiCommon) RenderToMainViews(opts types.RefreshMainOpts) error {
return self.gui.refreshMainViews(opts) return self.gui.refreshMainViews(opts)
} }

View File

@ -18,7 +18,8 @@ import (
// this gives our integration test a way of interacting with the gui for sending keypresses // this gives our integration test a way of interacting with the gui for sending keypresses
// and reading state. // and reading state.
type GuiDriver struct { type GuiDriver struct {
gui *Gui gui *Gui
isIdleChan chan struct{}
} }
var _ integrationTypes.GuiDriver = &GuiDriver{} var _ integrationTypes.GuiDriver = &GuiDriver{}
@ -40,6 +41,9 @@ func (self *GuiDriver) PressKey(keyStr string) {
tcell.NewEventKey(tcellKey, r, tcell.ModNone), tcell.NewEventKey(tcellKey, r, tcell.ModNone),
0, 0,
) )
// wait until lazygit is idle (i.e. all processing is done) before continuing
<-self.isIdleChan
} }
func (self *GuiDriver) Keys() config.KeybindingConfig { func (self *GuiDriver) Keys() config.KeybindingConfig {
@ -71,7 +75,10 @@ func (self *GuiDriver) Fail(message string) {
self.gui.g.Close() self.gui.g.Close()
// need to give the gui time to close // need to give the gui time to close
time.Sleep(time.Millisecond * 100) time.Sleep(time.Millisecond * 100)
fmt.Fprintln(os.Stderr, fullMessage) _, err := fmt.Fprintln(os.Stderr, fullMessage)
if err != nil {
panic("Test failed. Failed writing to stderr")
}
panic("Test failed") panic("Test failed")
} }

View File

@ -114,6 +114,8 @@ func (gui *Gui) layout(g *gocui.Gui) error {
return err return err
} }
gui.handleTestMode()
gui.ViewsSetup = true gui.ViewsSetup = true
} }

View File

@ -9,7 +9,6 @@ import (
gctx "github.com/jesseduffield/lazygit/pkg/gui/context" gctx "github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sasha-s/go-deadlock" "github.com/sasha-s/go-deadlock"
) )
@ -25,6 +24,7 @@ type PopupHandler struct {
withWaitingStatusFn func(message string, f func() error) withWaitingStatusFn func(message string, f func() error)
toastFn func(message string) toastFn func(message string)
getPromptInputFn func() string getPromptInputFn func() string
onWorker func(func())
} }
var _ types.IPopupHandler = &PopupHandler{} var _ types.IPopupHandler = &PopupHandler{}
@ -39,6 +39,7 @@ func NewPopupHandler(
withWaitingStatusFn func(message string, f func() error), withWaitingStatusFn func(message string, f func() error),
toastFn func(message string), toastFn func(message string),
getPromptInputFn func() string, getPromptInputFn func() string,
onWorker func(func()),
) *PopupHandler { ) *PopupHandler {
return &PopupHandler{ return &PopupHandler{
Common: common, Common: common,
@ -51,6 +52,7 @@ func NewPopupHandler(
withWaitingStatusFn: withWaitingStatusFn, withWaitingStatusFn: withWaitingStatusFn,
toastFn: toastFn, toastFn: toastFn,
getPromptInputFn: getPromptInputFn, getPromptInputFn: getPromptInputFn,
onWorker: onWorker,
} }
} }
@ -141,7 +143,7 @@ func (self *PopupHandler) WithLoaderPanel(message string, f func() error) error
return nil return nil
} }
go utils.Safe(func() { self.onWorker(func() {
if err := f(); err != nil { if err := f(); err != nil {
self.Log.Error(err) self.Log.Error(err)
} }

View File

@ -48,7 +48,7 @@ func (gui *Gui) newStringTask(view *gocui.View, str string) error {
func (gui *Gui) newStringTaskWithoutScroll(view *gocui.View, str string) error { func (gui *Gui) newStringTaskWithoutScroll(view *gocui.View, str string) error {
manager := gui.getManager(view) manager := gui.getManager(view)
f := func(stop chan struct{}) error { f := func(tasks.TaskOpts) error {
gui.c.SetViewContent(view, str) gui.c.SetViewContent(view, str)
return nil return nil
} }
@ -65,7 +65,7 @@ func (gui *Gui) newStringTaskWithoutScroll(view *gocui.View, str string) error {
func (gui *Gui) newStringTaskWithScroll(view *gocui.View, str string, originX int, originY int) error { func (gui *Gui) newStringTaskWithScroll(view *gocui.View, str string, originX int, originY int) error {
manager := gui.getManager(view) manager := gui.getManager(view)
f := func(stop chan struct{}) error { f := func(tasks.TaskOpts) error {
gui.c.SetViewContent(view, str) gui.c.SetViewContent(view, str)
_ = view.SetOrigin(originX, originY) _ = view.SetOrigin(originX, originY)
return nil return nil
@ -81,7 +81,7 @@ func (gui *Gui) newStringTaskWithScroll(view *gocui.View, str string, originX in
func (gui *Gui) newStringTaskWithKey(view *gocui.View, str string, key string) error { func (gui *Gui) newStringTaskWithKey(view *gocui.View, str string, key string) error {
manager := gui.getManager(view) manager := gui.getManager(view)
f := func(stop chan struct{}) error { f := func(tasks.TaskOpts) error {
gui.c.ResetViewOrigin(view) gui.c.ResetViewOrigin(view)
gui.c.SetViewContent(view, str) gui.c.SetViewContent(view, str)
return nil return nil
@ -130,6 +130,8 @@ func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager {
func() { func() {
_ = view.SetOrigin(0, 0) _ = view.SetOrigin(0, 0)
}, },
gui.c.GocuiGui().IncrementBusyCount,
gui.c.GocuiGui().DecrementBusyCount,
) )
gui.viewBufferManagerMap[view.Name()] = manager gui.viewBufferManagerMap[view.Name()] = manager
} }

View File

@ -7,29 +7,39 @@ import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/integration/components" "github.com/jesseduffield/lazygit/pkg/integration/components"
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
) )
type IntegrationTest interface { type IntegrationTest interface {
Run(guiAdapter *GuiDriver) Run(*GuiDriver)
} }
func (gui *Gui) handleTestMode(test integrationTypes.IntegrationTest) { func (gui *Gui) handleTestMode() {
test := gui.integrationTest
if os.Getenv(components.SANDBOX_ENV_VAR) == "true" { if os.Getenv(components.SANDBOX_ENV_VAR) == "true" {
return return
} }
if test != nil { if test != nil {
go func() { isIdleChan := make(chan struct{})
time.Sleep(time.Millisecond * 100)
test.Run(&GuiDriver{gui: gui}) gui.c.GocuiGui().AddIdleListener(isIdleChan)
waitUntilIdle := func() {
<-isIdleChan
}
go func() {
waitUntilIdle()
test.Run(&GuiDriver{gui: gui, isIdleChan: isIdleChan})
gui.g.Update(func(*gocui.Gui) error { gui.g.Update(func(*gocui.Gui) error {
return gocui.ErrQuit return gocui.ErrQuit
}) })
waitUntilIdle()
time.Sleep(time.Second * 1) time.Sleep(time.Second * 1)
log.Fatal("gocui should have already exited") log.Fatal("gocui should have already exited")

View File

@ -77,6 +77,9 @@ type IGuiCommon interface {
// Only necessary to call if you're not already on the UI thread i.e. you're inside a goroutine. // Only necessary to call if you're not already on the UI thread i.e. you're inside a goroutine.
// All controller handlers are executed on the UI thread. // All controller handlers are executed on the UI thread.
OnUIThread(f func() error) OnUIThread(f func() error)
// Runs a function in a goroutine. Use this whenever you want to run a goroutine and keep track of the fact
// that lazygit is still busy.
OnWorker(f func())
// returns the gocui Gui struct. There is a good chance you don't actually want to use // returns the gocui Gui struct. There is a good chance you don't actually want to use
// this struct and instead want to use another method above // this struct and instead want to use another method above

View File

@ -13,6 +13,8 @@ type assertionHelper struct {
// milliseconds we'll wait when an assertion fails. // milliseconds we'll wait when an assertion fails.
func retryWaitTimes() []int { func retryWaitTimes() []int {
return []int{0}
if os.Getenv("LONG_WAIT_BEFORE_FAIL") == "true" { if os.Getenv("LONG_WAIT_BEFORE_FAIL") == "true" {
// CI has limited hardware, may be throttled, runs tests in parallel, etc, so we // CI has limited hardware, may be throttled, runs tests in parallel, etc, so we
// give it more leeway compared to when we're running things locally. // give it more leeway compared to when we're running things locally.

View File

@ -61,6 +61,7 @@ var Reword = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Commits(). t.Views().Commits().
Lines( Lines(
Contains(wipCommitMessage), Contains(wipCommitMessage),
Contains(commitMessage),
) )
}, },
}) })

View File

@ -62,7 +62,7 @@ var DiffAndApplyPatch = NewIntegrationTest(NewIntegrationTestArgs{
Tap(func() { Tap(func() {
t.ExpectPopup().Menu().Title(Equals("Diffing")).Select(Contains("Exit diff mode")).Confirm() t.ExpectPopup().Menu().Title(Equals("Diffing")).Select(Contains("Exit diff mode")).Confirm()
t.Views().Information().Content(DoesNotContain("Building patch")) t.Views().Information().Content(Contains("Building patch"))
}). }).
Press(keys.Universal.CreatePatchOptionsMenu) Press(keys.Universal.CreatePatchOptionsMenu)

View File

@ -30,7 +30,7 @@ var SquashFixupsAboveFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{
Content(Contains("Are you sure you want to create a fixup! commit for commit")). Content(Contains("Are you sure you want to create a fixup! commit for commit")).
Confirm() Confirm()
}). }).
NavigateToLine(Contains("commit 01")). NavigateToLine(Contains("commit 01").DoesNotContain("fixup!")).
Press(keys.Commits.SquashAboveCommits). Press(keys.Commits.SquashAboveCommits).
Tap(func() { Tap(func() {
t.ExpectPopup().Confirmation(). t.ExpectPopup().Confirmation().

View File

@ -1,7 +1,6 @@
package tasks package tasks
import ( import (
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sasha-s/go-deadlock" "github.com/sasha-s/go-deadlock"
) )
@ -18,11 +17,13 @@ type AsyncHandler struct {
lastId int lastId int
mutex deadlock.Mutex mutex deadlock.Mutex
onReject func() onReject func()
onWorker func(func())
} }
func NewAsyncHandler() *AsyncHandler { func NewAsyncHandler(onWorker func(func())) *AsyncHandler {
return &AsyncHandler{ return &AsyncHandler{
mutex: deadlock.Mutex{}, mutex: deadlock.Mutex{},
onWorker: onWorker,
} }
} }
@ -32,7 +33,7 @@ func (self *AsyncHandler) Do(f func() func()) {
id := self.currentId id := self.currentId
self.mutex.Unlock() self.mutex.Unlock()
go utils.Safe(func() { self.onWorker(func() {
after := f() after := f()
self.handle(after, id) self.handle(after, id)
}) })

View File

@ -12,7 +12,10 @@ func TestAsyncHandler(t *testing.T) {
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
wg.Add(2) wg.Add(2)
handler := NewAsyncHandler() onWorker := func(f func()) {
go f()
}
handler := NewAsyncHandler(onWorker)
handler.onReject = func() { handler.onReject = func() {
wg.Done() wg.Done()
} }

View File

@ -48,6 +48,10 @@ type ViewBufferManager struct {
refreshView func() refreshView func()
onEndOfInput func() onEndOfInput func()
// see docs/dev/Busy.md
incrementBusyCount func()
decrementBusyCount func()
// if the user flicks through a heap of items, with each one // if the user flicks through a heap of items, with each one
// spawning a process to render something to the main view, // spawning a process to render something to the main view,
// it can slow things down quite a bit. In these situations we // it can slow things down quite a bit. In these situations we
@ -76,15 +80,19 @@ func NewViewBufferManager(
refreshView func(), refreshView func(),
onEndOfInput func(), onEndOfInput func(),
onNewKey func(), onNewKey func(),
incrementBusyCount func(),
decrementBusyCount func(),
) *ViewBufferManager { ) *ViewBufferManager {
return &ViewBufferManager{ return &ViewBufferManager{
Log: log, Log: log,
writer: writer, writer: writer,
beforeStart: beforeStart, beforeStart: beforeStart,
refreshView: refreshView, refreshView: refreshView,
onEndOfInput: onEndOfInput, onEndOfInput: onEndOfInput,
readLines: make(chan LinesToRead, 1024), readLines: make(chan LinesToRead, 1024),
onNewKey: onNewKey, onNewKey: onNewKey,
incrementBusyCount: incrementBusyCount,
decrementBusyCount: decrementBusyCount,
} }
} }
@ -94,13 +102,22 @@ func (self *ViewBufferManager) ReadLines(n int) {
}) })
} }
// note: onDone may be called twice func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), prefix string, linesToRead LinesToRead, onDoneFn func()) func(TaskOpts) error {
func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), prefix string, linesToRead LinesToRead, onDone func()) func(chan struct{}) error { return func(opts TaskOpts) error {
return func(stop chan struct{}) error { var onDoneOnce sync.Once
var once sync.Once var onFirstPageShownOnce sync.Once
var onDoneWrapper func()
if onDone != nil { onFirstPageShown := func() {
onDoneWrapper = func() { once.Do(onDone) } onFirstPageShownOnce.Do(func() {
opts.InitialContentLoaded()
})
}
onDone := func() {
if onDoneFn != nil {
onDoneOnce.Do(onDoneFn)
}
onFirstPageShown()
} }
if self.throttle { if self.throttle {
@ -109,7 +126,8 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p
} }
select { select {
case <-stop: case <-opts.Stop:
onDone()
return nil return nil
default: default:
} }
@ -119,7 +137,7 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p
timeToStart := time.Since(startTime) timeToStart := time.Since(startTime)
go utils.Safe(func() { go utils.Safe(func() {
<-stop <-opts.Stop
// we use the time it took to start the program as a way of checking if things // we use the time it took to start the program as a way of checking if things
// are running slow at the moment. This is admittedly a crude estimate, but // are running slow at the moment. This is admittedly a crude estimate, but
// the point is that we only want to throttle when things are running slow // the point is that we only want to throttle when things are running slow
@ -132,9 +150,7 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p
} }
// for pty's we need to call onDone here so that cmd.Wait() doesn't block forever // for pty's we need to call onDone here so that cmd.Wait() doesn't block forever
if onDoneWrapper != nil { onDone()
onDoneWrapper()
}
}) })
loadingMutex := deadlock.Mutex{} loadingMutex := deadlock.Mutex{}
@ -153,7 +169,7 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p
ticker := time.NewTicker(time.Millisecond * 200) ticker := time.NewTicker(time.Millisecond * 200)
defer ticker.Stop() defer ticker.Stop()
select { select {
case <-stop: case <-opts.Stop:
return return
case <-ticker.C: case <-ticker.C:
loadingMutex.Lock() loadingMutex.Lock()
@ -182,12 +198,12 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p
outer: outer:
for { for {
select { select {
case <-stop: case <-opts.Stop:
break outer break outer
case linesToRead := <-self.readLines: case linesToRead := <-self.readLines:
for i := 0; i < linesToRead.Total; i++ { for i := 0; i < linesToRead.Total; i++ {
select { select {
case <-stop: case <-opts.Stop:
break outer break outer
default: default:
} }
@ -219,6 +235,7 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p
} }
} }
refreshViewIfStale() refreshViewIfStale()
onFirstPageShown()
} }
} }
@ -231,10 +248,8 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p
} }
} }
// calling onDoneWrapper here again in case the program ended on its own accord // calling this here again in case the program ended on its own accord
if onDoneWrapper != nil { onDone()
onDoneWrapper()
}
close(done) close(done)
}) })
@ -272,8 +287,30 @@ func (self *ViewBufferManager) Close() {
// 1) command based, where the manager can be asked to read more lines, but the command can be killed // 1) command based, where the manager can be asked to read more lines, but the command can be killed
// 2) string based, where the manager can also be asked to read more lines // 2) string based, where the manager can also be asked to read more lines
func (self *ViewBufferManager) NewTask(f func(stop chan struct{}) error, key string) error { type TaskOpts struct {
// Channel that tells the task to stop, because another task wants to run.
Stop chan struct{}
// Only for tasks which are long-running, where we read more lines sporadically.
// We use this to keep track of when a user's action is complete (i.e. all views
// have been refreshed to display the results of their action)
InitialContentLoaded func()
}
func (self *ViewBufferManager) NewTask(f func(TaskOpts) error, key string) error {
self.incrementBusyCount()
var decrementCounterOnce sync.Once
decrementCounter := func() {
decrementCounterOnce.Do(func() {
self.decrementBusyCount()
})
}
go utils.Safe(func() { go utils.Safe(func() {
defer decrementCounter()
self.taskIDMutex.Lock() self.taskIDMutex.Lock()
self.newTaskID++ self.newTaskID++
taskID := self.newTaskID taskID := self.newTaskID
@ -286,9 +323,9 @@ func (self *ViewBufferManager) NewTask(f func(stop chan struct{}) error, key str
self.taskIDMutex.Unlock() self.taskIDMutex.Unlock()
self.waitingMutex.Lock() self.waitingMutex.Lock()
defer self.waitingMutex.Unlock()
if taskID < self.newTaskID { if taskID < self.newTaskID {
self.waitingMutex.Unlock()
return return
} }
@ -307,13 +344,13 @@ func (self *ViewBufferManager) NewTask(f func(stop chan struct{}) error, key str
self.stopCurrentTask = func() { once.Do(onStop) } self.stopCurrentTask = func() { once.Do(onStop) }
go utils.Safe(func() { self.waitingMutex.Unlock()
if err := f(stop); err != nil {
self.Log.Error(err) // might need an onError callback
}
close(notifyStopped) if err := f(TaskOpts{Stop: stop, InitialContentLoaded: decrementCounter}); err != nil {
}) self.Log.Error(err) // might need an onError callback
}
close(notifyStopped)
}) })
return nil return nil

View File

@ -19,6 +19,11 @@ func getCounter() (func(), func() int) {
return func() { counter++ }, func() int { return counter } return func() { counter++ }, func() int { return counter }
} }
func getIncDecCounter(initialValue int) (func(), func(), func() int) {
counter := initialValue
return func() { counter++ }, func() { counter-- }, func() int { return counter }
}
func TestNewCmdTaskInstantStop(t *testing.T) { func TestNewCmdTaskInstantStop(t *testing.T) {
writer := bytes.NewBuffer(nil) writer := bytes.NewBuffer(nil)
beforeStart, getBeforeStartCallCount := getCounter() beforeStart, getBeforeStartCallCount := getCounter()
@ -26,6 +31,7 @@ func TestNewCmdTaskInstantStop(t *testing.T) {
onEndOfInput, getOnEndOfInputCallCount := getCounter() onEndOfInput, getOnEndOfInputCallCount := getCounter()
onNewKey, getOnNewKeyCallCount := getCounter() onNewKey, getOnNewKeyCallCount := getCounter()
onDone, getOnDoneCallCount := getCounter() onDone, getOnDoneCallCount := getCounter()
incBusyCount, decBusyCount, getBusyCount := getIncDecCounter(1)
manager := NewViewBufferManager( manager := NewViewBufferManager(
utils.NewDummyLog(), utils.NewDummyLog(),
@ -34,6 +40,8 @@ func TestNewCmdTaskInstantStop(t *testing.T) {
refreshView, refreshView,
onEndOfInput, onEndOfInput,
onNewKey, onNewKey,
incBusyCount,
decBusyCount,
) )
stop := make(chan struct{}) stop := make(chan struct{})
@ -49,7 +57,7 @@ func TestNewCmdTaskInstantStop(t *testing.T) {
fn := manager.NewCmdTask(start, "prefix\n", LinesToRead{20, -1}, onDone) fn := manager.NewCmdTask(start, "prefix\n", LinesToRead{20, -1}, onDone)
_ = fn(stop) _ = fn(TaskOpts{Stop: stop, InitialContentLoaded: decBusyCount})
callCountExpectations := []struct { callCountExpectations := []struct {
expected int expected int
@ -68,6 +76,10 @@ func TestNewCmdTaskInstantStop(t *testing.T) {
} }
} }
if getBusyCount() != 0 {
t.Errorf("expected busy count to be 0, got %d", getBusyCount())
}
expectedContent := "" expectedContent := ""
actualContent := writer.String() actualContent := writer.String()
if actualContent != expectedContent { if actualContent != expectedContent {
@ -82,6 +94,7 @@ func TestNewCmdTask(t *testing.T) {
onEndOfInput, getOnEndOfInputCallCount := getCounter() onEndOfInput, getOnEndOfInputCallCount := getCounter()
onNewKey, getOnNewKeyCallCount := getCounter() onNewKey, getOnNewKeyCallCount := getCounter()
onDone, getOnDoneCallCount := getCounter() onDone, getOnDoneCallCount := getCounter()
incBusyCount, decBusyCount, getBusyCount := getIncDecCounter(1)
manager := NewViewBufferManager( manager := NewViewBufferManager(
utils.NewDummyLog(), utils.NewDummyLog(),
@ -90,6 +103,8 @@ func TestNewCmdTask(t *testing.T) {
refreshView, refreshView,
onEndOfInput, onEndOfInput,
onNewKey, onNewKey,
incBusyCount,
decBusyCount,
) )
stop := make(chan struct{}) stop := make(chan struct{})
@ -109,7 +124,7 @@ func TestNewCmdTask(t *testing.T) {
close(stop) close(stop)
wg.Done() wg.Done()
}() }()
_ = fn(stop) _ = fn(TaskOpts{Stop: stop, InitialContentLoaded: decBusyCount})
wg.Wait() wg.Wait()
@ -130,6 +145,10 @@ func TestNewCmdTask(t *testing.T) {
} }
} }
if getBusyCount() != 0 {
t.Errorf("expected busy count to be 0, got %d", getBusyCount())
}
expectedContent := "prefix\ntest\n" expectedContent := "prefix\ntest\n"
actualContent := writer.String() actualContent := writer.String()
if actualContent != expectedContent { if actualContent != expectedContent {
@ -208,6 +227,8 @@ func TestNewCmdTaskRefresh(t *testing.T) {
lineCountsOnRefresh = append(lineCountsOnRefresh, strings.Count(writer.String(), "\n")) lineCountsOnRefresh = append(lineCountsOnRefresh, strings.Count(writer.String(), "\n"))
} }
decBusyCount := func() {}
manager := NewViewBufferManager( manager := NewViewBufferManager(
utils.NewDummyLog(), utils.NewDummyLog(),
writer, writer,
@ -215,6 +236,8 @@ func TestNewCmdTaskRefresh(t *testing.T) {
refreshView, refreshView,
func() {}, func() {},
func() {}, func() {},
func() {},
decBusyCount,
) )
stop := make(chan struct{}) stop := make(chan struct{})
@ -234,7 +257,7 @@ func TestNewCmdTaskRefresh(t *testing.T) {
close(stop) close(stop)
wg.Done() wg.Done()
}() }()
_ = fn(stop) _ = fn(TaskOpts{Stop: stop, InitialContentLoaded: decBusyCount})
wg.Wait() wg.Wait()