mirror of
https://github.com/rclone/rclone.git
synced 2025-04-19 01:59:00 +08:00
cmd/gitannex: Add explicit timeout for mock stdout reads in tests
It seems like (*testState).readLine() hangs indefinitely when it's waiting for a line that will never be written [1]. This commit adds an explicit 30-second timeout when reading from the internal mock stdout. Given that we integrate with fstest, this timeout needs to be sufficiently long that it accommodates slow-but-successful operations on real remotes. [1]: https://github.com/rclone/rclone/pull/8423#issuecomment-2701601290
This commit is contained in:
parent
ccef29bbff
commit
128489a9ec
@ -252,6 +252,9 @@ type testState struct {
|
||||
server *server
|
||||
mockStdinW *io.PipeWriter
|
||||
mockStdoutReader *bufio.Reader
|
||||
// readLineTimeout is the maximum duration of time to wait for [server] to
|
||||
// write a line to be written to the mock stdout.
|
||||
readLineTimeout time.Duration
|
||||
|
||||
fstestRun *fstest.Run
|
||||
remoteName string
|
||||
@ -270,6 +273,11 @@ func makeTestState(t *testing.T) testState {
|
||||
},
|
||||
mockStdinW: stdinW,
|
||||
mockStdoutReader: bufio.NewReader(stdoutR),
|
||||
|
||||
// The default readLineTimeout must be large enough to accommodate slow
|
||||
// operations on real remotes. Without a timeout, attempts to read a
|
||||
// line that's never written would block indefinitely.
|
||||
readLineTimeout: time.Second * 30,
|
||||
}
|
||||
}
|
||||
|
||||
@ -277,18 +285,52 @@ func (h *testState) requireRemoteIsEmpty() {
|
||||
h.fstestRun.CheckRemoteItems(h.t)
|
||||
}
|
||||
|
||||
func (h *testState) requireReadLineExact(line string) {
|
||||
receivedLine, err := h.mockStdoutReader.ReadString('\n')
|
||||
require.NoError(h.t, err)
|
||||
require.Equal(h.t, line+"\n", receivedLine)
|
||||
// readLineWithTimeout attempts to read a line from the mock stdout. Returns an
|
||||
// error if the read operation times out or fails for any reason.
|
||||
func (h *testState) readLineWithTimeout() (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), h.readLineTimeout)
|
||||
defer cancel()
|
||||
|
||||
lineChan := make(chan string)
|
||||
errChan := make(chan error)
|
||||
|
||||
go func() {
|
||||
line, err := h.mockStdoutReader.ReadString('\n')
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
} else {
|
||||
lineChan <- line
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case line := <-lineChan:
|
||||
return line, nil
|
||||
case err := <-errChan:
|
||||
return "", err
|
||||
case <-ctx.Done():
|
||||
return "", fmt.Errorf("attempt to read line timed out: %w", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
// requireReadLineExact requires that a line matching wantLine can be read from
|
||||
// the mock stdout.
|
||||
func (h *testState) requireReadLineExact(wantLine string) {
|
||||
receivedLine, err := h.readLineWithTimeout()
|
||||
require.NoError(h.t, err)
|
||||
require.Equal(h.t, wantLine+"\n", receivedLine)
|
||||
}
|
||||
|
||||
// requireReadLine requires that a line can be read from the mock stdout and
|
||||
// returns the line.
|
||||
func (h *testState) requireReadLine() string {
|
||||
receivedLine, err := h.mockStdoutReader.ReadString('\n')
|
||||
receivedLine, err := h.readLineWithTimeout()
|
||||
require.NoError(h.t, err)
|
||||
return receivedLine
|
||||
}
|
||||
|
||||
// requireWriteLine requires that the given line is successfully written to the
|
||||
// mock stdin.
|
||||
func (h *testState) requireWriteLine(line string) {
|
||||
_, err := h.mockStdinW.Write([]byte(line + "\n"))
|
||||
require.NoError(h.t, err)
|
||||
@ -1281,6 +1323,46 @@ var fstestTestCases = []testCase{
|
||||
},
|
||||
}
|
||||
|
||||
// TestReadLineHasShortDeadline verifies that [testState.readLineWithTimeout]
|
||||
// does not block indefinitely when a line is never written.
|
||||
func TestReadLineHasShortDeadline(t *testing.T) {
|
||||
const timeoutForRead = time.Millisecond * 50
|
||||
const timeoutForTest = time.Millisecond * 100
|
||||
const tickDuration = time.Millisecond * 10
|
||||
|
||||
type readLineResult struct {
|
||||
line string
|
||||
err error
|
||||
}
|
||||
|
||||
resultChan := make(chan readLineResult)
|
||||
|
||||
go func() {
|
||||
defer close(resultChan)
|
||||
|
||||
h := makeTestState(t)
|
||||
h.readLineTimeout = timeoutForRead
|
||||
|
||||
line, err := h.readLineWithTimeout()
|
||||
resultChan <- readLineResult{line, err}
|
||||
}()
|
||||
|
||||
// This closure will be run periodically until time runs out or until all of
|
||||
// its assertions pass.
|
||||
idempotentConditionFunc := func(c *assert.CollectT) {
|
||||
result, ok := <-resultChan
|
||||
require.True(c, ok, "The goroutine should send a result")
|
||||
|
||||
require.Empty(c, result.line, "No line should be read")
|
||||
require.ErrorIs(c, result.err, context.DeadlineExceeded)
|
||||
|
||||
_, ok = <-resultChan
|
||||
require.False(c, ok, "The channel should be closed")
|
||||
}
|
||||
|
||||
require.EventuallyWithT(t, idempotentConditionFunc, timeoutForTest, tickDuration)
|
||||
}
|
||||
|
||||
// TestMain drives the tests
|
||||
func TestMain(m *testing.M) {
|
||||
fstest.TestMain(m)
|
||||
|
Loading…
x
Reference in New Issue
Block a user