jesseduffield.lazygit/pkg/tasks/read_request_queue_test.go
Stefan Haller dd2a1a634f Answer every read request, whether or not a task is still serving
A caller of ReadLines or ReadToEnd is told that the content it asked for
has been read by the request's Then being called. A task that reaches the
end of its input answers the requests still queued behind the one it was
serving, but a task that is stopped drops them, and their callers wait
for a callback that never comes. Pressing "/" in the focused main view
opens the search prompt from such a callback, so if a re-render replaces
the task at that moment the prompt never opens.

Answering them as the read loop ends would leave a request handed over
after that point unanswered, and there is a window for one. A caller
reads the channel to send on, and can reach the send itself only once the
loop has gone. So hand requests over through a queue instead. Asking
whether a task is there and giving it the request are one step, as are
taking the task away and handing back what it never answered; a request
made in between goes back to the caller to answer.

The queue is unbounded rather than a fixed-size channel, for the reasons
gocui's userEventQueue is. Requests are handed over from the UI thread,
where a blocking send would deadlock against the task waiting to be let
go, and a fixed channel that fills up leaves only blocking, dropping,
reordering or panicking to choose between.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 15:09:52 +02:00

59 lines
1.4 KiB
Go

package tasks
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestReadRequestQueueHandsBackWhatNoTaskWillServe(t *testing.T) {
queue := newReadRequestQueue()
// Nothing has begun serving, so the request comes straight back to its caller.
assert.False(t, queue.enqueue(LinesToRead{Total: 1}))
queue.beginServing()
assert.True(t, queue.enqueue(LinesToRead{Total: 1}))
assert.True(t, queue.enqueue(LinesToRead{Total: 2}))
request, ok := queue.dequeue()
assert.True(t, ok)
assert.Equal(t, 1, request.Total)
// What the task never got to comes back when it stops, and nothing is taken
// from a caller after that.
unanswered := queue.stopServing()
assert.Len(t, unanswered, 1)
assert.Equal(t, 2, unanswered[0].Total)
assert.False(t, queue.enqueue(LinesToRead{Total: 3}))
_, ok = queue.dequeue()
assert.False(t, ok)
}
func TestReadRequestQueueRingsTheDoorbell(t *testing.T) {
queue := newReadRequestQueue()
doorbell := queue.beginServing()
select {
case <-doorbell:
assert.Fail(t, "the doorbell rang before anything was queued")
default:
}
// A burst leaves one token, which stands for everything queued.
queue.enqueue(LinesToRead{Total: 1})
queue.enqueue(LinesToRead{Total: 2})
select {
case <-doorbell:
default:
assert.Fail(t, "the doorbell didn't ring")
}
select {
case <-doorbell:
assert.Fail(t, "the doorbell rang twice for one wake")
default:
}
}