1package git23import (4 "context"5 "io"6 "testing"7 "time"89 "github.com/stretchr/testify/assert"10)1112func 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 passed17 _, err := exec(ctx, "", []string{"version"}, nil)18 assert.Equal(t, ErrExecTimeout, err)19 })2021 t.Run("context deadline fires mid-execution", func(t *testing.T) {22 ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)23 defer cancel()2425 // Use cmd directly with a blocking stdin so the command starts successfully and26 // blocks reading until the context deadline fires.27 c, timeoutCancel := cmd(ctx, "", []string{"hash-object", "--stdin"}, nil)28 defer timeoutCancel()2930 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}3536// 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.EOF38// so that the stdin copy goroutine can exit cleanly, allowing cmd.Wait() to39// return.40type blockingReader struct {41 cancel <-chan struct{}42}4344func (r blockingReader) Read(p []byte) (int, error) {45 <-r.cancel46 return 0, io.EOF47}4849func TestCmd_ContextCancellation(t *testing.T) {50 ctx, cancel := context.WithCancel(context.Background())5152 // Cancel in the background after a short delay so the command is already running53 // 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 }()6061 c, timeoutCancel := cmd(ctx, "", []string{"hash-object", "--stdin"}, nil)62 defer timeoutCancel()6364 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}7071func TestExec_DefaultTimeoutApplied(t *testing.T) {72 // A plain context.Background() has no deadline. The command should still succeed73 // because DefaultTimeout is applied automatically and "git version" completes74 // 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}