git-module

git clone git://git.lin.moe/fork/git-module.git

 1package git
 2
 3import (
 4	"context"
 5	"io"
 6	"testing"
 7	"time"
 8
 9	"github.com/stretchr/testify/assert"
10)
11
12func TestExec_ContextTimeout(t *testing.T) {
13	t.Run("context already expired before start", func(t *testing.T) {
14		ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
15		defer cancel()
16		time.Sleep(time.Millisecond) // ensure deadline has passed
17		_, err := exec(ctx, "", []string{"version"}, nil)
18		assert.Equal(t, ErrExecTimeout, err)
19	})
20
21	t.Run("context deadline fires mid-execution", func(t *testing.T) {
22		ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
23		defer cancel()
24
25		// Use cmd directly with a blocking stdin so the command starts successfully and
26		// blocks reading until the context deadline fires.
27		c, timeoutCancel := cmd(ctx, "", []string{"hash-object", "--stdin"}, nil)
28		defer timeoutCancel()
29
30		err := c.Input(blockingReader{cancel: ctx.Done()}).StdOut().Run().Stream(io.Discard)
31		err = mapContextError(err, ctx)
32		assert.Equal(t, ErrExecTimeout, err)
33	})
34}
35
36// blockingReader is an io.Reader that blocks until its cancel channel is closed,
37// simulating a stdin that never provides data. When canceled it returns io.EOF
38// so that the stdin copy goroutine can exit cleanly, allowing cmd.Wait() to
39// return.
40type blockingReader struct {
41	cancel <-chan struct{}
42}
43
44func (r blockingReader) Read(p []byte) (int, error) {
45	<-r.cancel
46	return 0, io.EOF
47}
48
49func TestCmd_ContextCancellation(t *testing.T) {
50	ctx, cancel := context.WithCancel(context.Background())
51
52	// Cancel in the background after a short delay so the command is already running
53	// when cancellation arrives. Close done to unblock the reader.
54	done := make(chan struct{})
55	go func() {
56		time.Sleep(50 * time.Millisecond)
57		cancel()
58		close(done)
59	}()
60
61	c, timeoutCancel := cmd(ctx, "", []string{"hash-object", "--stdin"}, nil)
62	defer timeoutCancel()
63
64	err := c.Input(blockingReader{cancel: done}).StdOut().Run().Stream(io.Discard)
65	err = mapContextError(err, ctx)
66	assert.ErrorIs(t, err, context.Canceled)
67	// Must NOT be ErrExecTimeout — cancellation is distinct from deadline.
68	assert.NotEqual(t, ErrExecTimeout, err)
69}
70
71func TestExec_DefaultTimeoutApplied(t *testing.T) {
72	// A plain context.Background() has no deadline. The command should still succeed
73	// because DefaultTimeout is applied automatically and "git version" completes
74	// well within that.
75	ctx := context.Background()
76	stdout, err := exec(ctx, "", []string{"version"}, nil)
77	assert.NoError(t, err)
78	assert.Contains(t, string(stdout), "git version")
79}