1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-02-17 14:11:02 +02:00

add tests

This commit is contained in:
Jesse Duffield 2021-11-07 13:25:06 +11:00
parent 5d12a6bf99
commit 3fb478a30e
2 changed files with 195 additions and 63 deletions

View File

@ -19,18 +19,14 @@ const THROTTLE_TIME = time.Millisecond * 30
// we use this to check if the system is under stress right now. Hopefully this makes sense on other machines // we use this to check if the system is under stress right now. Hopefully this makes sense on other machines
const COMMAND_START_THRESHOLD = time.Millisecond * 10 const COMMAND_START_THRESHOLD = time.Millisecond * 10
type Task struct {
stop chan struct{}
stopped bool
stopMutex sync.Mutex
notifyStopped chan struct{}
Log *logrus.Entry
f func(chan struct{}) error
}
type ViewBufferManager struct { type ViewBufferManager struct {
writer io.Writer // this blocks until the task has been properly stopped
currentTask *Task stopCurrentTask func()
// this is what we write the output of the task to. It's typically a view
writer io.Writer
// this is for when we wait to get
waitingMutex sync.Mutex waitingMutex sync.Mutex
taskIDMutex sync.Mutex taskIDMutex sync.Mutex
Log *logrus.Entry Log *logrus.Entry
@ -80,8 +76,15 @@ func (m *ViewBufferManager) ReadLines(n int) {
}) })
} }
// note: onDone may be called twice
func (m *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), prefix string, linesToRead int, onDone func()) func(chan struct{}) error { func (m *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), prefix string, linesToRead int, onDone func()) func(chan struct{}) error {
return func(stop chan struct{}) error { return func(stop chan struct{}) error {
var once sync.Once
var onDoneWrapper func()
if onDone != nil {
onDoneWrapper = func() { once.Do(onDone) }
}
if m.throttle { if m.throttle {
m.Log.Info("throttling task") m.Log.Info("throttling task")
time.Sleep(THROTTLE_TIME) time.Sleep(THROTTLE_TIME)
@ -110,8 +113,9 @@ func (m *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), pref
} }
} }
if onDone != nil { // for pty's we need to call onDone here so that cmd.Wait() doesn't block forever
onDone() if onDoneWrapper != nil {
onDoneWrapper()
} }
}) })
@ -122,34 +126,42 @@ func (m *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), pref
done := make(chan struct{}) done := make(chan struct{})
scanner := bufio.NewScanner(r)
scanner.Split(bufio.ScanLines)
loaded := false
go utils.Safe(func() { go utils.Safe(func() {
scanner := bufio.NewScanner(r) ticker := time.NewTicker(time.Millisecond * 200)
scanner.Split(bufio.ScanLines) defer ticker.Stop()
select {
loaded := false case <-stop:
return
go utils.Safe(func() { case <-ticker.C:
ticker := time.NewTicker(time.Millisecond * 200) loadingMutex.Lock()
defer ticker.Stop() if !loaded {
select { m.beforeStart()
case <-ticker.C: _, _ = m.writer.Write([]byte("loading..."))
loadingMutex.Lock() m.refreshView()
if !loaded {
m.beforeStart()
_, _ = m.writer.Write([]byte("loading..."))
m.refreshView()
}
loadingMutex.Unlock()
case <-stop:
return
} }
}) loadingMutex.Unlock()
}
})
go utils.Safe(func() {
outer: outer:
for { for {
select { select {
case <-stop:
break outer
case linesToRead := <-m.readLines: case linesToRead := <-m.readLines:
for i := 0; i < linesToRead; i++ { for i := 0; i < linesToRead; i++ {
select {
case <-stop:
break outer
default:
}
ok := scanner.Scan() ok := scanner.Scan()
loadingMutex.Lock() loadingMutex.Lock()
if !loaded { if !loaded {
@ -161,11 +173,6 @@ func (m *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), pref
} }
loadingMutex.Unlock() loadingMutex.Unlock()
select {
case <-stop:
break outer
default:
}
if !ok { if !ok {
// if we're here then there's nothing left to scan from the source // if we're here then there's nothing left to scan from the source
// so we're at the EOF and can flush the stale content // so we're at the EOF and can flush the stale content
@ -175,8 +182,6 @@ func (m *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), pref
_, _ = m.writer.Write(append(scanner.Bytes(), '\n')) _, _ = m.writer.Write(append(scanner.Bytes(), '\n'))
} }
m.refreshView() m.refreshView()
case <-stop:
break outer
} }
} }
@ -189,8 +194,9 @@ func (m *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), pref
} }
} }
if onDone != nil { // calling onDoneWrapper here again in case the program ended on its own accord
onDone() if onDoneWrapper != nil {
onDoneWrapper()
} }
close(done) close(done)
@ -206,14 +212,14 @@ func (m *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), pref
// Close closes the task manager, killing whatever task may currently be running // Close closes the task manager, killing whatever task may currently be running
func (t *ViewBufferManager) Close() { func (t *ViewBufferManager) Close() {
if t.currentTask == nil { if t.stopCurrentTask == nil {
return return
} }
c := make(chan struct{}) c := make(chan struct{})
go utils.Safe(func() { go utils.Safe(func() {
t.currentTask.Stop() t.stopCurrentTask()
c <- struct{}{} c <- struct{}{}
}) })
@ -249,19 +255,20 @@ func (m *ViewBufferManager) NewTask(f func(stop chan struct{}) error, key string
return return
} }
if m.stopCurrentTask != nil {
m.stopCurrentTask()
}
stop := make(chan struct{}) stop := make(chan struct{})
notifyStopped := make(chan struct{}) notifyStopped := make(chan struct{})
if m.currentTask != nil { var once sync.Once
m.currentTask.Stop() onStop := func() {
close(stop)
<-notifyStopped
} }
m.currentTask = &Task{ m.stopCurrentTask = func() { once.Do(onStop) }
stop: stop,
notifyStopped: notifyStopped,
Log: m.Log,
f: f,
}
go utils.Safe(func() { go utils.Safe(func() {
if err := f(stop); err != nil { if err := f(stop); err != nil {
@ -274,14 +281,3 @@ func (m *ViewBufferManager) NewTask(f func(stop chan struct{}) error, key string
return nil return nil
} }
func (t *Task) Stop() {
t.stopMutex.Lock()
defer t.stopMutex.Unlock()
if t.stopped {
return
}
close(t.stop)
<-t.notifyStopped
t.stopped = true
}

136
pkg/tasks/tasks_test.go Normal file
View File

@ -0,0 +1,136 @@
package tasks
import (
"bytes"
"io"
"os/exec"
"sync"
"testing"
"time"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func getCounter() (func(), func() int) {
counter := 0
return func() { counter++ }, func() int { return counter }
}
func TestNewCmdTaskInstantStop(t *testing.T) {
writer := bytes.NewBuffer(nil)
beforeStart, getBeforeStartCallCount := getCounter()
refreshView, getRefreshViewCallCount := getCounter()
onEndOfInput, getOnEndOfInputCallCount := getCounter()
onNewKey, getOnNewKeyCallCount := getCounter()
onDone, getOnDoneCallCount := getCounter()
manager := NewViewBufferManager(
utils.NewDummyLog(),
writer,
beforeStart,
refreshView,
onEndOfInput,
onNewKey,
)
stop := make(chan struct{})
reader := bytes.NewBufferString("test")
start := func() (*exec.Cmd, io.Reader) {
// not actually starting this because it's not necessary
cmd := secureexec.Command("blah blah")
close(stop)
return cmd, reader
}
fn := manager.NewCmdTask(start, "prefix\n", 20, onDone)
_ = fn(stop)
callCountExpectations := []struct {
expected int
actual int
name string
}{
{0, getBeforeStartCallCount(), "beforeStart"},
{1, getRefreshViewCallCount(), "refreshView"},
{0, getOnEndOfInputCallCount(), "onEndOfInput"},
{0, getOnNewKeyCallCount(), "onNewKey"},
{1, getOnDoneCallCount(), "onDone"},
}
for _, expectation := range callCountExpectations {
if expectation.actual != expectation.expected {
t.Errorf("expected %s to be called %d times, got %d", expectation.name, expectation.expected, expectation.actual)
}
}
expectedContent := ""
actualContent := writer.String()
if actualContent != expectedContent {
t.Errorf("expected writer to receive the following content: \n%s\n. But instead it recevied: %s", expectedContent, actualContent)
}
}
func TestNewCmdTask(t *testing.T) {
writer := bytes.NewBuffer(nil)
beforeStart, getBeforeStartCallCount := getCounter()
refreshView, getRefreshViewCallCount := getCounter()
onEndOfInput, getOnEndOfInputCallCount := getCounter()
onNewKey, getOnNewKeyCallCount := getCounter()
onDone, getOnDoneCallCount := getCounter()
manager := NewViewBufferManager(
utils.NewDummyLog(),
writer,
beforeStart,
refreshView,
onEndOfInput,
onNewKey,
)
stop := make(chan struct{})
reader := bytes.NewBufferString("test")
start := func() (*exec.Cmd, io.Reader) {
// not actually starting this because it's not necessary
cmd := secureexec.Command("blah blah")
return cmd, reader
}
fn := manager.NewCmdTask(start, "prefix\n", 20, onDone)
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
time.Sleep(100 * time.Millisecond)
close(stop)
wg.Done()
}()
_ = fn(stop)
wg.Wait()
callCountExpectations := []struct {
expected int
actual int
name string
}{
{1, getBeforeStartCallCount(), "beforeStart"},
{1, getRefreshViewCallCount(), "refreshView"},
{1, getOnEndOfInputCallCount(), "onEndOfInput"},
{0, getOnNewKeyCallCount(), "onNewKey"},
{1, getOnDoneCallCount(), "onDone"},
}
for _, expectation := range callCountExpectations {
if expectation.actual != expectation.expected {
t.Errorf("expected %s to be called %d times, got %d", expectation.name, expectation.expected, expectation.actual)
}
}
expectedContent := "prefix\ntest\n"
actualContent := writer.String()
if actualContent != expectedContent {
t.Errorf("expected writer to receive the following content: \n%s\n. But instead it recevied: %s", expectedContent, actualContent)
}
}