Ctrl K
ComunidadGuías…

pkg/tasks/tasks_test.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package tasks

import (
	"bytes"
	"io"
	"os/exec"
	"reflect"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/jesseduffield/lazygit/pkg/gocui"
	"github.com/jesseduffield/lazygit/pkg/utils"
	"github.com/stretchr/testify/assert"
)

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()
	resetOrigin, getResetOriginCallCount := getCounter()
	beginRender, getBeginRenderCallCount := getCounter()
	swapInRender, getSwapInRenderCallCount := getCounter()
	onDone, getOnDoneCallCount := getCounter()
	task := gocui.NewFakeTask()
	newTask := func() gocui.Task {
		return task
	}

	manager := NewViewBufferManager(
		utils.NewDummyLog(),
		writer,
		beforeStart,
		refreshView,
		onEndOfInput,
		resetOrigin,
		beginRender,
		swapInRender,
		newTask,
		// no UI thread in the test; run the view mutations inline
		func(f func()) error { f(); return nil },
	)

	stop := make(chan struct{})
	reader := bytes.NewBufferString("test")
	start := func() (Cmd, io.Reader) {
		// not actually starting this because it's not necessary
		cmd := exec.Command("blah")

		close(stop)

		return ExecCmd{Cmd: cmd}, reader
	}

	fn := manager.NewCmdTask(start, func() string { return "prefix\n" }, LinesToRead{Total: 20, InitialRefreshAfter: -1}, onDone)

	_ = fn(TaskOpts{Stop: stop, InitialContentLoaded: func() { task.Done() }})

	callCountExpectations := []struct {
		expected int
		actual   int
		name     string
	}{
		{0, getBeforeStartCallCount(), "beforeStart"},
		{1, getRefreshViewCallCount(), "refreshView"},
		{0, getOnEndOfInputCallCount(), "onEndOfInput"},
		{0, getResetOriginCallCount(), "resetOrigin"},
		{0, getBeginRenderCallCount(), "beginRender"},
		{0, getSwapInRenderCallCount(), "swapInRender"},
		{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)
		}
	}

	if task.Status() != gocui.TaskStatusDone {
		t.Errorf("expected task status to be 'done', got '%s'", task.FormatStatus())
	}

	expectedContent := ""
	actualContent := writer.String()
	if actualContent != expectedContent {
		t.Errorf("expected writer to receive the following content: \n%s\n. But instead it received: %s", expectedContent, actualContent)
	}
}

func TestNewCmdTask(t *testing.T) {
	writer := bytes.NewBuffer(nil)
	beforeStart, getBeforeStartCallCount := getCounter()
	refreshView, getRefreshViewCallCount := getCounter()
	onEndOfInput, getOnEndOfInputCallCount := getCounter()
	resetOrigin, getResetOriginCallCount := getCounter()
	beginRender, getBeginRenderCallCount := getCounter()
	swapInRender, getSwapInRenderCallCount := getCounter()
	onDone, getOnDoneCallCount := getCounter()
	task := gocui.NewFakeTask()
	newTask := func() gocui.Task {
		return task
	}

	manager := NewViewBufferManager(
		utils.NewDummyLog(),
		writer,
		beforeStart,
		refreshView,
		onEndOfInput,
		resetOrigin,
		beginRender,
		swapInRender,
		newTask,
		// no UI thread in the test; run the view mutations inline
		func(f func()) error { f(); return nil },
	)

	stop := make(chan struct{})
	reader := bytes.NewBufferString("test")
	start := func() (Cmd, io.Reader) {
		// not actually starting this because it's not necessary
		cmd := exec.Command("blah")

		return ExecCmd{Cmd: cmd}, reader
	}

	fn := manager.NewCmdTask(start, func() string { return "prefix\n" }, LinesToRead{Total: 20, InitialRefreshAfter: -1}, onDone)
	wg := sync.WaitGroup{}
	wg.Go(func() {
		time.Sleep(100 * time.Millisecond)
		close(stop)
	})
	_ = fn(TaskOpts{Stop: stop, InitialContentLoaded: func() { task.Done() }})

	wg.Wait()

	callCountExpectations := []struct {
		expected int
		actual   int
		name     string
	}{
		{0, getBeforeStartCallCount(), "beforeStart"},
		{1, getRefreshViewCallCount(), "refreshView"},
		{1, getOnEndOfInputCallCount(), "onEndOfInput"},
		{0, getResetOriginCallCount(), "resetOrigin"},
		{1, getBeginRenderCallCount(), "beginRender"},
		{1, getSwapInRenderCallCount(), "swapInRender"},
		{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)
		}
	}

	if task.Status() != gocui.TaskStatusDone {
		t.Errorf("expected task status to be 'done', got '%s'", task.FormatStatus())
	}

	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 received: %s", expectedContent, actualContent)
	}
}

// A prefix is produced before the command starts, so a task stopped while it
// produces its prefix never gets as far as starting the command.
func TestNewCmdTaskStoppedWhileProducingItsPrefix(t *testing.T) {
	noop := func() {}
	task := gocui.NewFakeTask()
	writer := bytes.NewBuffer(nil)

	manager := NewViewBufferManager(
		utils.NewDummyLog(), writer, noop, noop, noop, noop, noop, noop,
		func() gocui.Task { return task },
		func(f func()) error { f(); return nil },
	)

	stop := make(chan struct{})
	started := false
	start := func() (Cmd, io.Reader) {
		started = true
		return ExecCmd{Cmd: exec.Command("true")}, bytes.NewBufferString("output")
	}
	prefix := func() string {
		close(stop)
		return "prefix\n"
	}
	onDone, getOnDoneCallCount := getCounter()

	fn := manager.NewCmdTask(start, prefix, LinesToRead{Total: 20, InitialRefreshAfter: -1}, onDone)
	_ = fn(TaskOpts{Stop: stop, InitialContentLoaded: func() { task.Done() }})

	assert.False(t, started)
	assert.Equal(t, 1, getOnDoneCallCount())
	assert.Empty(t, writer.String())
}

// A dummy reader that simply yields as many blank lines as requested. The only
// thing we want to do with the output is count the number of lines.
type BlankLineReader struct {
	totalLinesToYield int
	linesYielded      int
}

func (d *BlankLineReader) Read(p []byte) (n int, err error) {
	if d.totalLinesToYield == d.linesYielded {
		return 0, io.EOF
	}

	d.linesYielded++
	p[0] = '\n'
	return 1, nil
}

// A dummy reader that yields the given number of blank lines and then blocks
// until unblock is closed, at which point it reports EOF. This lets a test hold
// a task in its "still loading" state for as long as it needs to.
type BlockingLineReader struct {
	linesToYield int
	linesYielded int
	reachedEnd   bool
	blocked      chan struct{}
	unblock      chan struct{}
}

func (d *BlockingLineReader) Read(p []byte) (n int, err error) {
	if d.linesYielded == d.linesToYield {
		if !d.reachedEnd {
			d.reachedEnd = true
			close(d.blocked)
		}
		<-d.unblock
		return 0, io.EOF
	}

	d.linesYielded++
	p[0] = '\n'
	return 1, nil
}

func TestNewCmdTaskQueuedReadAtEndOfInput(t *testing.T) {
	writer := bytes.NewBuffer(nil)
	task := gocui.NewFakeTask()

	manager := NewViewBufferManager(
		utils.NewDummyLog(),
		writer,
		func() {},
		func() {},
		func() {},
		func() {},
		func() {},
		func() {},
		func() gocui.Task { return task },
		// no UI thread in the test; run the view mutations inline
		func(f func()) error { f(); return nil },
	)

	reader := BlockingLineReader{
		linesToYield: 5,
		blocked:      make(chan struct{}),
		unblock:      make(chan struct{}),
	}
	start := func() (Cmd, io.Reader) {
		// not actually starting this because it's not necessary
		return ExecCmd{Cmd: exec.Command("blah")}, &reader
	}

	// The initial request asks for far more lines than the reader has, so the
	// task reaches EOF while that request is still the one being served.
	fn := manager.NewCmdTask(start, nil, LinesToRead{Total: 100, InitialRefreshAfter: -1}, func() {})

	thenCalled := false
	wg := sync.WaitGroup{}
	wg.Go(func() {
		_ = fn(TaskOpts{Stop: make(chan struct{}), InitialContentLoaded: func() { task.Done() }})
	})

	<-reader.blocked
	// The request is queued by the time this returns, so it is outstanding when we
	// let the task reach EOF below.
	manager.ReadToEnd(func() { thenCalled = true })
	close(reader.unblock)

	wg.Wait()

	assert.True(t, thenCalled)
}

// A task rendering content the view wasn't already showing resets the scroll
// position to the top, at its first paint. If it is stopped and replaced before
// it ever paints — a background refresh landing just after the user clicked a
// different item, say — the replacement renders the same content and so decides
// on no reset of its own; it has to perform the one the stopped task was owed,
// or the view keeps the scroll position of the content it showed before.
func TestResetOriginSurvivesTaskReplacement(t *testing.T) {
	resetOrigin, getResetOriginCallCount := getCounter()

	manager := NewViewBufferManager(
		utils.NewDummyLog(),
		bytes.NewBuffer(nil),
		func() {},
		func() {},
		func() {},
		resetOrigin,
		func() {},
		func() {},
		func() gocui.Task { return gocui.NewFakeTask() },
		// no UI thread in the test; run the view mutations inline
		func(f func()) error { f(); return nil },
	)

	startTask := func(key string, reader io.Reader, onDone func()) {
		start := func() (Cmd, io.Reader) {
			// not actually starting this because it's not necessary
			return ExecCmd{Cmd: exec.Command("blah")}, reader
		}
		// The first-paint point is far beyond what any of these readers yield, so
		// only reaching EOF paints.
		_ = manager.NewTask(manager.NewCmdTask(start, nil, LinesToRead{Total: 100, InitialRefreshAfter: 50}, onDone), key)
	}
	runTaskToCompletion := func(key string) {
		done := make(chan struct{})
		startTask(key, &BlankLineReader{totalLinesToYield: 3}, func() { close(done) })
		<-done
	}

	// A render of content the view wasn't showing resets the scroll position.
	runTaskToCompletion("cmd1")
	assert.Equal(t, 1, getResetOriginCallCount())

	// Different content again, but this task stalls before it can paint.
	stalled := BlockingLineReader{
		linesToYield: 3,
		blocked:      make(chan struct{}),
		unblock:      make(chan struct{}),
	}
	defer close(stalled.unblock)
	startTask("cmd2", &stalled, nil)
	<-stalled.blocked

	// The replacement shows the same content as the stalled task, so it has no
	// reset of its own to do — but it must still do that task's.
	runTaskToCompletion("cmd2")
	assert.Equal(t, 2, getResetOriginCallCount())
}

// A render that takes long enough to start takes the view over to say
// "loading...", which means blanking whatever it was showing. That is only worth
// doing when the content coming is different from what's on screen: re-rendering
// the same content (a background refresh, say) would otherwise blank the view and
// paint the same thing back, a visible flicker for nothing.
func TestLoadingIndicatorOnlyTakesOverForNewContent(t *testing.T) {
	var beforeStartCount atomic.Int32

	manager := NewViewBufferManager(
		utils.NewDummyLog(),
		io.Discard,
		func() { beforeStartCount.Add(1) },
		func() {},
		func() {},
		func() {},
		func() {},
		func() {},
		func() gocui.Task { return gocui.NewFakeTask() },
		// no UI thread in the test; run the view mutations inline
		func(f func()) error { f(); return nil },
	)

	startTask := func(key string, reader io.Reader, onDone func()) {
		start := func() (Cmd, io.Reader) {
			// not actually starting this because it's not necessary
			return ExecCmd{Cmd: exec.Command("blah")}, reader
		}
		_ = manager.NewTask(manager.NewCmdTask(start, nil, LinesToRead{Total: 100, InitialRefreshAfter: 50}, onDone), key)
	}
	// Starts a task whose command produces nothing at all, so that it is still
	// waiting for its first line when the loading indicator falls due. Returns
	// the reader so the caller can let it finish.
	startStalledTask := func(key string) *BlockingLineReader {
		reader := &BlockingLineReader{
			blocked: make(chan struct{}),
			unblock: make(chan struct{}),
		}
		startTask(key, reader, nil)
		<-reader.blocked
		return reader
	}

	// Get some content on screen first: the indicator is only due when a render
	// is slow, and this one isn't.
	done := make(chan struct{})
	startTask("cmd1", &BlankLineReader{totalLinesToYield: 3}, func() { close(done) })
	<-done
	assert.EqualValues(t, 0, beforeStartCount.Load())

	// A slow re-render of that same content must leave the view alone however
	// long it takes. The indicator is due 200ms in, so give it well past that.
	sameContent := startStalledTask("cmd1")
	defer close(sameContent.unblock)
	time.Sleep(500 * time.Millisecond)
	assert.EqualValues(t, 0, beforeStartCount.Load())

	// Different content, though, is worth taking the view over for.
	newContent := startStalledTask("cmd2")
	defer close(newContent.unblock)
	assert.Eventually(t,
		func() bool { return beforeStartCount.Load() == 1 },
		2*time.Second, 10*time.Millisecond)
}

// A pending restore takes the first paint over: it says when enough of the new
// content has arrived to show the position it remembers, and does the swap itself so
// that it can look for that position while the previous content is still displayed.
// The scroll reset that new content is owed happens before it runs, so that where it
// puts the view is where the view stays.
func TestNewCmdTaskRestore(t *testing.T) {
	writer := bytes.NewBuffer(nil)
	linesWritten := func() int { return strings.Count(writer.String(), "\n") }
	resetOrigin, getResetOriginCallCount := getCounter()

	swapped := false
	applyCount := 0
	applyAtLines := -1
	swappedBeforeApply := false
	swappedByApply := false
	resetsBeforeApply := -1

	manager := NewViewBufferManager(
		utils.NewDummyLog(),
		writer,
		func() {}, // beforeStart
		func() {}, // refreshView
		func() {}, // onEndOfInput
		resetOrigin,
		func() {},                 // beginRender
		func() { swapped = true }, // swapInRender
		func() gocui.Task { return gocui.NewFakeTask() },
		// no UI thread in the test; run the view mutations inline
		func(f func()) error { f(); return nil },
	)

	manager.SetRestoreForNextTask(&RenderRestore{
		// Ready once five lines have loaded — well before the view is filled (30).
		FirstPaintReady: func() bool { return linesWritten() >= 5 },
		Apply: func(swapIn func()) {
			applyCount++
			applyAtLines = linesWritten()
			swappedBeforeApply = swappedBeforeApply || swapped
			resetsBeforeApply = getResetOriginCallCount()
			swapIn()
			swappedByApply = swapped
		},
	})

	done := make(chan struct{})
	start := func() (Cmd, io.Reader) {
		// not actually starting this because it's not necessary
		return ExecCmd{Cmd: exec.Command("blah")}, &BlankLineReader{totalLinesToYield: 50}
	}
	_ = manager.NewTask(manager.NewCmdTask(start, nil, LinesToRead{Total: 100, InitialRefreshAfter: 30}, func() { close(done) }), "cmd")
	<-done

	assert.Equal(t, 1, applyCount, "Apply should run exactly once")
	assert.False(t, swappedBeforeApply, "the off-screen render should not be swapped in before Apply runs")
	assert.True(t, swappedByApply, "Apply should swap the off-screen render in via swapIn")
	// The first paint was driven by the restore, not by having read enough lines to
	// fill the view.
	assert.GreaterOrEqual(t, applyAtLines, 5)
	assert.Less(t, applyAtLines, 30)
	assert.Equal(t, 1, resetsBeforeApply, "new content should be put at the top before the restore places it")
	assert.Equal(t, 1, getResetOriginCallCount(), "and not reset again afterwards, over the restore")
}

// A restore that never finds what it is looking for keeps the task reading to the
// end of its input, since the line might have been anywhere in it. Once there is no
// more content to hope for, the render is revealed with the scroll reset that new
// content is owed.
func TestNewCmdTaskRestoreThatFindsNothing(t *testing.T) {
	writer := bytes.NewBuffer(nil)
	linesWritten := func() int { return strings.Count(writer.String(), "\n") }
	resetOrigin, getResetOriginCallCount := getCounter()

	applyCount := 0
	swappedAtLines := -1

	manager := NewViewBufferManager(
		utils.NewDummyLog(),
		writer,
		func() {}, // beforeStart
		func() {}, // refreshView
		func() {}, // onEndOfInput
		resetOrigin,
		func() {}, // beginRender
		func() { swappedAtLines = linesWritten() },
		func() gocui.Task { return gocui.NewFakeTask() },
		// no UI thread in the test; run the view mutations inline
		func(f func()) error { f(); return nil },
	)

	manager.SetRestoreForNextTask(&RenderRestore{
		FirstPaintReady: func() bool { return false },
		Apply: func(swapIn func()) {
			applyCount++
			swapIn()
		},
	})

	done := make(chan struct{})
	start := func() (Cmd, io.Reader) {
		// not actually starting this because it's not necessary
		return ExecCmd{Cmd: exec.Command("blah")}, &BlankLineReader{totalLinesToYield: 50}
	}
	_ = manager.NewTask(manager.NewCmdTask(start, nil, LinesToRead{Total: 100, InitialRefreshAfter: 30}, func() { close(done) }), "cmd")
	<-done

	assert.Equal(t, 1, applyCount, "Apply should still run, to swap the render in")
	assert.Equal(t, 50, swappedAtLines, "the whole input should be read before giving up on the restore")
	assert.Equal(t, 1, getResetOriginCallCount(), "new content the restore couldn't place starts at the top")
}

// The task a restore was installed for can be stopped and replaced before it ever
// paints — a background refresh landing right after the key was pressed. The
// replacement renders the same content, so it is the one that owes the user their
// position.
func TestRestoreSurvivesTaskReplacement(t *testing.T) {
	var applyCount atomic.Int32

	manager := NewViewBufferManager(
		utils.NewDummyLog(),
		io.Discard,
		func() {},
		func() {},
		func() {},
		func() {},
		func() {},
		func() {},
		func() gocui.Task { return gocui.NewFakeTask() },
		// no UI thread in the test; run the view mutations inline
		func(f func()) error { f(); return nil },
	)

	manager.SetRestoreForNextTask(&RenderRestore{
		FirstPaintReady: func() bool { return false },
		Apply: func(swapIn func()) {
			applyCount.Add(1)
			swapIn()
		},
	})

	startTask := func(reader io.Reader, onDone func()) {
		start := func() (Cmd, io.Reader) {
			// not actually starting this because it's not necessary
			return ExecCmd{Cmd: exec.Command("blah")}, reader
		}
		_ = manager.NewTask(manager.NewCmdTask(start, nil, LinesToRead{Total: 100, InitialRefreshAfter: 50}, onDone), "cmd")
	}

	// The task the restore was installed for stalls before it can paint.
	stalled := BlockingLineReader{
		linesToYield: 3,
		blocked:      make(chan struct{}),
		unblock:      make(chan struct{}),
	}
	defer close(stalled.unblock)
	startTask(&stalled, nil)
	<-stalled.blocked

	done := make(chan struct{})
	startTask(&BlankLineReader{totalLinesToYield: 3}, func() { close(done) })
	<-done

	assert.EqualValues(t, 1, applyCount.Load(), "the replacement should apply the restore the stopped task couldn't")
}

// A task told to keep the scroll position renders the content the view is showing
// under another command — the same diff with more context around it, say — so it
// neither resets the scroll nor blanks the view to say "loading...", both of which are
// for content the user hasn't seen.
func TestKeepScrollPositionForNextTask(t *testing.T) {
	var beforeStartCount atomic.Int32
	resetOrigin, getResetOriginCallCount := getCounter()

	manager := NewViewBufferManager(
		utils.NewDummyLog(),
		io.Discard,
		func() { beforeStartCount.Add(1) },
		func() {}, // refreshView
		func() {}, // onEndOfInput
		resetOrigin,
		func() {}, // beginRender
		func() {}, // swapInRender
		func() gocui.Task { return gocui.NewFakeTask() },
		// no UI thread in the test; run the view mutations inline
		func(f func()) error { f(); return nil },
	)

	startTask := func(key string, reader io.Reader, onDone func()) {
		start := func() (Cmd, io.Reader) {
			// not actually starting this because it's not necessary
			return ExecCmd{Cmd: exec.Command("blah")}, reader
		}
		_ = manager.NewTask(manager.NewCmdTask(start, nil, LinesToRead{Total: 100, InitialRefreshAfter: 50}, onDone), key)
	}
	runTaskToCompletion := func(key string) {
		done := make(chan struct{})
		startTask(key, &BlankLineReader{totalLinesToYield: 3}, func() { close(done) })
		<-done
	}

	// Content the view wasn't showing, to have something to keep the position in.
	runTaskToCompletion("cmd1")
	assert.Equal(t, 1, getResetOriginCallCount())

	manager.SetKeepScrollPositionForNextTask()
	runTaskToCompletion("cmd2")
	assert.Equal(t, 1, getResetOriginCallCount(), "the same content under another command keeps its position")

	// And the request rides one task only: the next different command is a different
	// diff as far as anyone knows.
	runTaskToCompletion("cmd3")
	assert.Equal(t, 2, getResetOriginCallCount())

	// The loading indicator goes by the same question, so it stays out of the way too.
	manager.SetKeepScrollPositionForNextTask()
	stalled := BlockingLineReader{
		blocked: make(chan struct{}),
		unblock: make(chan struct{}),
	}
	defer close(stalled.unblock)
	startTask("cmd4", &stalled, nil)
	<-stalled.blocked
	time.Sleep(500 * time.Millisecond)
	assert.EqualValues(t, 0, beforeStartCount.Load())
}

// A view that has been emptied is showing nothing, so the render it was showing is no
// longer the one to compare the next task against: running the same command again is
// putting content into the view that isn't there any more, and starts from the top.
func TestForgetRenderedContent(t *testing.T) {
	resetOrigin, getResetOriginCallCount := getCounter()

	manager := NewViewBufferManager(
		utils.NewDummyLog(),
		io.Discard,
		func() {}, // beforeStart
		func() {}, // refreshView
		func() {}, // onEndOfInput
		resetOrigin,
		func() {}, // beginRender
		func() {}, // swapInRender
		func() gocui.Task { return gocui.NewFakeTask() },
		// no UI thread in the test; run the view mutations inline
		func(f func()) error { f(); return nil },
	)

	runTaskToCompletion := func(key string) {
		start := func() (Cmd, io.Reader) {
			// not actually starting this because it's not necessary
			return ExecCmd{Cmd: exec.Command("blah")}, &BlankLineReader{totalLinesToYield: 3}
		}
		done := make(chan struct{})
		_ = manager.NewTask(
			manager.NewCmdTask(start, nil, LinesToRead{Total: 100, InitialRefreshAfter: 50}, func() { close(done) }), key)
		<-done
	}

	runTaskToCompletion("cmd1")
	assert.Equal(t, 1, getResetOriginCallCount())

	// Rendering the same command's output again leaves the view where it is, that being
	// what it already shows.
	runTaskToCompletion("cmd1")
	assert.Equal(t, 1, getResetOriginCallCount())

	manager.ForgetRenderedContent()
	runTaskToCompletion("cmd1")
	assert.Equal(t, 2, getResetOriginCallCount(), "an emptied view is shown its content afresh")
}

// A render of the content the view is showing keeps the view where it is, so it reads
// the lines above the view as well, both before it first paints and in all. A render
// of content the view isn't showing yet is shown from the top, so the lines above the
// view are no use to it. Reading them would only hold up the first paint of a diff
// selected while the view is scrolled far down another one.
func TestOnlyARenderInPlaceReadsTheLinesAboveTheView(t *testing.T) {
	writer := bytes.NewBuffer(nil)
	linesWritten := func() int { return strings.Count(writer.String(), "\n") }
	linesWrittenAtFirstPaint := -1

	manager := NewViewBufferManager(
		utils.NewDummyLog(),
		writer,
		func() {}, // beforeStart
		func() {}, // refreshView
		func() {}, // onEndOfInput
		func() {}, // resetOrigin
		writer.Reset,
		func() { linesWrittenAtFirstPaint = linesWritten() },
		func() gocui.Task { return gocui.NewFakeTask() },
		// no UI thread in the test; run the view mutations inline
		func(f func()) error { f(); return nil },
	)
	defer manager.Close()

	// Renders the given command's output, which is longer than any of the reading
	// asked for here, and returns once the task has read the lines it was asked for.
	render := func(key string) {
		start := func() (Cmd, io.Reader) {
			// not actually starting this because it's not necessary
			return ExecCmd{Cmd: exec.Command("blah")}, &BlankLineReader{totalLinesToYield: 1000}
		}
		served := make(chan struct{})
		linesToRead := LinesToRead{
			Total:               20,
			InitialRefreshAfter: 15,
			LinesAboveView:      100,
			Then:                func() { close(served) },
		}
		_ = manager.NewTask(manager.NewCmdTask(start, nil, linesToRead, nil), key)
		<-served
	}

	render("cmd1")
	assert.Equal(t, 15, linesWrittenAtFirstPaint)
	assert.Equal(t, 20, linesWritten())

	render("cmd1")
	assert.Equal(t, 115, linesWrittenAtFirstPaint)
	assert.Equal(t, 120, linesWritten())

	render("cmd2")
	assert.Equal(t, 15, linesWrittenAtFirstPaint)
	assert.Equal(t, 20, linesWritten())
}

func TestNewCmdTaskRefresh(t *testing.T) {
	type scenario struct {
		name                        string
		totalTaskLines              int
		linesToRead                 LinesToRead
		expectedLineCountsOnRefresh []int
	}

	scenarios := []scenario{
		{
			"total < initialRefreshAfter",
			150,
			LinesToRead{Total: 100, InitialRefreshAfter: 120},
			[]int{100},
		},
		{
			"total == initialRefreshAfter",
			150,
			LinesToRead{Total: 100, InitialRefreshAfter: 100},
			[]int{100},
		},
		{
			"total > initialRefreshAfter",
			150,
			LinesToRead{Total: 100, InitialRefreshAfter: 50},
			[]int{50, 100},
		},
		{
			"initialRefreshAfter == -1",
			150,
			LinesToRead{Total: 100, InitialRefreshAfter: -1},
			[]int{100},
		},
		{
			"totalTaskLines < initialRefreshAfter",
			25,
			LinesToRead{Total: 100, InitialRefreshAfter: 50},
			[]int{25},
		},
		{
			"totalTaskLines between total and initialRefreshAfter",
			75,
			LinesToRead{Total: 100, InitialRefreshAfter: 50},
			[]int{50, 75},
		},
	}

	for _, s := range scenarios {
		writer := bytes.NewBuffer(nil)
		lineCountsOnRefresh := []int{}
		refreshView := func() {
			lineCountsOnRefresh = append(lineCountsOnRefresh, strings.Count(writer.String(), "\n"))
		}

		task := gocui.NewFakeTask()
		newTask := func() gocui.Task {
			return task
		}

		manager := NewViewBufferManager(
			utils.NewDummyLog(),
			writer,
			func() {},
			refreshView,
			func() {},
			func() {},
			func() {},
			func() {},
			newTask,
			// no UI thread in the test; run the view mutations inline
			func(f func()) error { f(); return nil },
		)

		stop := make(chan struct{})
		reader := BlankLineReader{totalLinesToYield: s.totalTaskLines}
		start := func() (Cmd, io.Reader) {
			// not actually starting this because it's not necessary
			cmd := exec.Command("blah")

			return ExecCmd{Cmd: cmd}, &reader
		}

		fn := manager.NewCmdTask(start, nil, s.linesToRead, func() {})
		wg := sync.WaitGroup{}
		wg.Go(func() {
			time.Sleep(100 * time.Millisecond)
			close(stop)
		})
		_ = fn(TaskOpts{Stop: stop, InitialContentLoaded: func() { task.Done() }})

		wg.Wait()

		if !reflect.DeepEqual(lineCountsOnRefresh, s.expectedLineCountsOnRefresh) {
			t.Errorf("%s: expected line counts on refresh: %v, got %v",
				s.name, s.expectedLineCountsOnRefresh, lineCountsOnRefresh)
		}
	}
}

// A caller of ReadToEnd acts on the whole of what the view shows once it is answered,
// such as listing the files of the diff in a menu. If a re-render replaces the task
// that is reading the content, the content is the re-render's, so whoever is waiting
// is answered once that has been read to the end, whether the replaced task was
// already reading to the end for them or hadn't got to their request yet.
func TestReadToEndWaitingWhenARerenderReplacesTheTask(t *testing.T) {
	writer := bytes.NewBuffer(nil)
	linesWritten := func() int { return strings.Count(writer.String(), "\n") }
	firstPaint := make(chan struct{}, 1)

	manager := NewViewBufferManager(
		utils.NewDummyLog(),
		writer,
		func() {}, // beforeStart
		func() {}, // refreshView
		func() {}, // onEndOfInput
		func() {}, // resetOrigin
		writer.Reset,
		func() { firstPaint <- struct{}{} },
		func() gocui.Task { return gocui.NewFakeTask() },
		// no UI thread in the test; run the view mutations inline
		func(f func()) error { f(); return nil },
	)
	defer manager.Close()

	startTask := func(reader io.Reader) {
		start := func() (Cmd, io.Reader) {
			// not actually starting this because it's not necessary
			return ExecCmd{Cmd: exec.Command("blah")}, reader
		}
		linesToRead := LinesToRead{Total: 1, InitialRefreshAfter: 1}
		_ = manager.NewTask(manager.NewCmdTask(start, nil, linesToRead, nil), "cmd")
	}

	var mutex sync.Mutex
	linesWrittenWhenAnswered := []int{}
	var answered sync.WaitGroup
	readToEnd := func() {
		answered.Add(1)
		manager.ReadToEnd(func() {
			mutex.Lock()
			linesWrittenWhenAnswered = append(linesWrittenWhenAnswered, linesWritten())
			mutex.Unlock()
			answered.Done()
		})
	}

	// The render reads the line it was asked for, and stalls while reading to the end
	// for the first caller. The second one's request waits behind it.
	stalled := BlockingLineReader{linesToYield: 3, blocked: make(chan struct{}), unblock: make(chan struct{})}
	defer close(stalled.unblock)
	startTask(&stalled)
	<-firstPaint
	readToEnd()
	<-stalled.blocked
	readToEnd()

	// A re-render of the same content, as a refresh does.
	startTask(&BlankLineReader{totalLinesToYield: 50})
	answered.Wait()

	assert.Equal(t, []int{50, 50}, linesWrittenWhenAnswered)
}

// A task that reads no command's output, such as one showing a message, has given the
// view all of its content once it returns. So when it replaces a task that was reading
// for someone, it answers the requests that task leaves it, after showing its content.
func TestReadRequestsLeftToAMessageAreAnsweredOnceItIsShown(t *testing.T) {
	noop := func() {}
	manager := NewViewBufferManager(
		utils.NewDummyLog(), bytes.NewBuffer(nil), noop, noop, noop, noop, noop, noop,
		func() gocui.Task { return gocui.NewFakeTask() },
		// no UI thread in the test; run the view mutations inline
		func(f func()) error { f(); return nil },
	)
	defer manager.Close()

	stalled := BlockingLineReader{linesToYield: 1, blocked: make(chan struct{}), unblock: make(chan struct{})}
	defer close(stalled.unblock)
	start := func() (Cmd, io.Reader) {
		// not actually starting this because it's not necessary
		return ExecCmd{Cmd: exec.Command("blah")}, &stalled
	}
	_ = manager.NewTask(manager.NewCmdTask(start, nil, LinesToRead{Total: 1, InitialRefreshAfter: 1}, nil), "cmd")
	<-stalled.blocked

	var messageShown atomic.Bool
	answeredAfterTheMessage := make(chan bool, 2)
	for range 2 {
		manager.ReadToEnd(func() { answeredAfterTheMessage <- messageShown.Load() })
	}

	_ = manager.NewTask(func(TaskOpts) error {
		messageShown.Store(true)
		return nil
	}, "message")

	assert.True(t, <-answeredAfterTheMessage)
	assert.True(t, <-answeredAfterTheMessage)
}

// A render whose task is created after the layout is the one whose content a request
// made in the meantime is about, so the request waits for that task to read it, even
// though the task before it had read all of its own.
func TestReadToEndWaitsForATaskAskedForButNotCreatedYet(t *testing.T) {
	writer := bytes.NewBuffer(nil)
	linesWritten := func() int { return strings.Count(writer.String(), "\n") }
	noop := func() {}
	manager := NewViewBufferManager(
		utils.NewDummyLog(), writer, noop, noop, noop, noop, writer.Reset, noop,
		func() gocui.Task { return gocui.NewFakeTask() },
		// no UI thread in the test; run the view mutations inline
		func(f func()) error { f(); return nil },
	)
	defer manager.Close()

	newCmdTask := func(lines int, onDone func()) func(TaskOpts) error {
		start := func() (Cmd, io.Reader) {
			// not actually starting this because it's not necessary
			return ExecCmd{Cmd: exec.Command("blah")}, &BlankLineReader{totalLinesToYield: lines}
		}
		return manager.NewCmdTask(start, nil, LinesToRead{Total: 100, InitialRefreshAfter: 1}, onDone)
	}

	done := make(chan struct{})
	_ = manager.NewTask(newCmdTask(3, func() { close(done) }), "cmd1")
	<-done

	reservation := manager.ReserveTask()
	linesWrittenWhenAnswered := make(chan int, 1)
	manager.ReadToEnd(func() { linesWrittenWhenAnswered <- linesWritten() })

	_ = manager.NewReservedTask(reservation, newCmdTask(50, nil), "cmd2")
	assert.Equal(t, 50, <-linesWrittenWhenAnswered)
}

// Once the view's tasks are closed, no task is going to read anything, so whoever is
// waiting for content to be read is answered.
func TestCloseAnswersWaitingReadRequests(t *testing.T) {
	noop := func() {}
	manager := NewViewBufferManager(
		utils.NewDummyLog(), bytes.NewBuffer(nil), noop, noop, noop, noop, noop, noop,
		func() gocui.Task { return gocui.NewFakeTask() },
		// no UI thread in the test; run the view mutations inline
		func(f func()) error { f(); return nil },
	)

	stalled := BlockingLineReader{linesToYield: 1, blocked: make(chan struct{}), unblock: make(chan struct{})}
	defer close(stalled.unblock)
	start := func() (Cmd, io.Reader) {
		// not actually starting this because it's not necessary
		return ExecCmd{Cmd: exec.Command("blah")}, &stalled
	}
	_ = manager.NewTask(manager.NewCmdTask(start, nil, LinesToRead{Total: 1, InitialRefreshAfter: 1}, nil), "cmd")
	<-stalled.blocked

	answered := atomic.Int32{}
	manager.ReadToEnd(func() { answered.Add(1) })
	manager.ReadToEnd(func() { answered.Add(1) })

	manager.Close()
	assert.EqualValues(t, 2, answered.Load())
}

// The callers of ReadToEnd hand their follow-up work to the UI thread from then, and
// that work holds a task of its own from the moment it is enqueued. If the task that
// ReadToEnd holds were done before then runs, lazygit would count as idle for a moment
// in between, and an integration test would carry on before the follow-up work is done.
func TestReadToEndHoldsItsTaskUntilThenReturns(t *testing.T) {
	noop := func() {}
	task := gocui.NewFakeTask()

	manager := NewViewBufferManager(
		utils.NewDummyLog(), bytes.NewBuffer(nil), noop, noop, noop, noop, noop, noop,
		func() gocui.Task { return task },
		func(f func()) error { f(); return nil },
	)

	// With no command task serving read requests, ReadToEnd answers right away.
	var statusDuringThen gocui.TaskStatus
	manager.ReadToEnd(func() { statusDuringThen = task.Status() })

	assert.Equal(t, gocui.TaskStatusBusy, statusDuringThen)
	assert.Equal(t, gocui.TaskStatusDone, task.Status())
}

// doneSignallingTask lets a test wait for the tasks of a view to finish, including
// those that never get to run.
type doneSignallingTask struct {
	*gocui.FakeTask
	done func()
}

func (self *doneSignallingTask) Done() {
	self.FakeTask.Done()
	self.done()
}

// A render whose task is only created after the layout takes its place among the
// view's tasks when it is asked for. A message asked for after it, before the
// layout, is created first, and the render's task mustn't replace it.
func TestReservedTaskDoesntReplaceATaskAskedForLater(t *testing.T) {
	var tasksDone sync.WaitGroup
	manager := NewViewBufferManager(
		utils.NewDummyLog(),
		bytes.NewBuffer(nil),
		func() {},
		func() {},
		func() {},
		func() {},
		func() {},
		func() {},
		func() gocui.Task {
			tasksDone.Add(1)
			return &doneSignallingTask{FakeTask: gocui.NewFakeTask(), done: tasksDone.Done}
		},
		// no UI thread in the test; run the view mutations inline
		func(f func()) error { f(); return nil },
	)

	var mutex sync.Mutex
	var tasksRun []string
	task := func(name string) func(TaskOpts) error {
		return func(TaskOpts) error {
			mutex.Lock()
			defer mutex.Unlock()
			tasksRun = append(tasksRun, name)
			return nil
		}
	}

	reservation := manager.ReserveTask()
	assert.False(t, manager.IsSuperseded(reservation))

	_ = manager.NewTask(task("message"), "message")
	assert.True(t, manager.IsSuperseded(reservation))

	_ = manager.NewReservedTask(reservation, task("render"), "render")
	tasksDone.Wait()

	assert.Equal(t, []string{"message"}, tasksRun)
}

// A message that replaces a command task while the task is still reading its
// output leaves nothing loading content into the view.
func TestMessageEndsTheLoadingOfTheTaskItReplaces(t *testing.T) {
	manager := NewViewBufferManager(
		utils.NewDummyLog(),
		bytes.NewBuffer(nil),
		func() {},
		func() {},
		func() {},
		func() {},
		func() {},
		func() {},
		func() gocui.Task { return gocui.NewFakeTask() },
		// no UI thread in the test; run the view mutations inline
		func(f func()) error { f(); return nil },
	)

	stalled := BlockingLineReader{
		linesToYield: 3,
		blocked:      make(chan struct{}),
		unblock:      make(chan struct{}),
	}
	defer close(stalled.unblock)
	start := func() (Cmd, io.Reader) {
		// not actually starting this because it's not necessary
		return ExecCmd{Cmd: exec.Command("blah")}, &stalled
	}

	reservation := manager.ReserveTask()
	manager.StartLoading()
	_ = manager.NewReservedTask(reservation, manager.NewCmdTask(start, nil, LinesToRead{Total: 100, InitialRefreshAfter: 50}, nil), "cmd")
	<-stalled.blocked
	assert.True(t, manager.IsLoading())

	_ = manager.NewTask(func(TaskOpts) error { return nil }, "message")
	assert.False(t, manager.IsLoading())
}

// The view is loading the content of the command task asked for last. A task
// asked for before it reaching the end of its input doesn't end that.
func TestEarlierTaskEndingLeavesALaterTaskLoading(t *testing.T) {
	manager := NewViewBufferManager(
		utils.NewDummyLog(),
		bytes.NewBuffer(nil),
		func() {},
		func() {},
		func() {},
		func() {},
		func() {},
		func() {},
		func() gocui.Task { return gocui.NewFakeTask() },
		// no UI thread in the test; run the view mutations inline
		func(f func()) error { f(); return nil },
	)

	stalled := BlockingLineReader{
		linesToYield: 3,
		blocked:      make(chan struct{}),
		unblock:      make(chan struct{}),
	}
	start := func() (Cmd, io.Reader) {
		// not actually starting this because it's not necessary
		return ExecCmd{Cmd: exec.Command("blah")}, &stalled
	}

	earlierDone := make(chan struct{})
	reservation := manager.ReserveTask()
	manager.StartLoading()
	_ = manager.NewReservedTask(reservation,
		manager.NewCmdTask(start, nil, LinesToRead{Total: 100, InitialRefreshAfter: 50}, func() { close(earlierDone) }), "earlier")
	<-stalled.blocked

	// The later task is asked for, but not created yet, as for a render whose task
	// is created after the layout.
	manager.ReserveTask()
	manager.StartLoading()

	close(stalled.unblock)
	<-earlierDone
	assert.True(t, manager.IsLoading())
}