Files
zzet--gortex/internal/gitcmd/gitcmd_test.go
T
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

359 lines
10 KiB
Go

package gitcmd
import (
"context"
"errors"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
)
// withFakeGit installs a shell script named "git" at the front of PATH
// for the duration of the test and returns the directory it lives in.
// The script body is provided by the caller.
func withFakeGit(t *testing.T, body string) string {
t.Helper()
if runtime.GOOS == "windows" {
t.Skip("fake-git shim is a POSIX shell script")
}
dir := t.TempDir()
script := "#!/bin/sh\n" + body
gitPath := filepath.Join(dir, "git")
if err := os.WriteFile(gitPath, []byte(script), 0o755); err != nil {
t.Fatalf("write fake git: %v", err)
}
oldPath := os.Getenv("PATH")
t.Setenv("PATH", dir+string(os.PathListSeparator)+oldPath)
return dir
}
// TestConcurrencyCapNeverExceeded launches N+k concurrent Run calls
// against a fake git that signals on start, blocks on a barrier, then
// exits. It asserts the number simultaneously in-flight never exceeds
// the configured concurrency cap.
func TestConcurrencyCapNeverExceeded(t *testing.T) {
const cap = 3
const callers = cap + 4
// The fake git appends a "start" line to $TRACK, then spins until
// $RELEASE exists, then appends a "stop" line. This lets the test
// hold every spawned process open simultaneously and watch the
// live count.
dir := withFakeGit(t, `
echo "start $$" >> "$GITCMD_TRACK"
while [ ! -f "$GITCMD_RELEASE" ]; do
sleep 0.005
done
echo "stop $$" >> "$GITCMD_TRACK"
exit 0
`)
_ = dir
trackFile := filepath.Join(t.TempDir(), "track")
releaseFile := filepath.Join(t.TempDir(), "release")
t.Setenv("GITCMD_TRACK", trackFile)
t.Setenv("GITCMD_RELEASE", releaseFile)
if err := os.WriteFile(trackFile, nil, 0o644); err != nil {
t.Fatalf("seed track file: %v", err)
}
// Reset the limiter to a known cap, restore the default afterwards.
SetConcurrency(cap)
t.Cleanup(func() { SetConcurrency(int(defaultConcurrency())) })
var inFlight int64
var maxInFlight int64
var wg sync.WaitGroup
for i := 0; i < callers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// Run will block inside the fake git until releaseFile
// appears; while blocked it holds a semaphore slot.
_, _ = Run(context.Background(), "", "status")
}()
}
// Sampler: while callers are blocked in the fake git, count the
// processes that have logged "start" but not yet "stop" and track
// the peak. We wait until cap processes are concurrently running
// (proving the cap is reached) then release everyone.
deadline := time.Now().Add(10 * time.Second)
released := false
for time.Now().Before(deadline) {
starts, stops := countTrack(t, trackFile)
cur := int64(starts - stops)
atomic.StoreInt64(&inFlight, cur)
if cur > atomic.LoadInt64(&maxInFlight) {
atomic.StoreInt64(&maxInFlight, cur)
}
if !released && starts >= cap {
// Cap-many processes are concurrently alive; release.
if err := os.WriteFile(releaseFile, nil, 0o644); err != nil {
t.Fatalf("write release file: %v", err)
}
released = true
}
if released && stops >= callers {
break
}
time.Sleep(2 * time.Millisecond)
}
if !released {
// Make sure no goroutine leaks if we timed out before reaching cap.
_ = os.WriteFile(releaseFile, nil, 0o644)
}
wg.Wait()
peak := atomic.LoadInt64(&maxInFlight)
if peak > cap {
t.Fatalf("concurrency cap exceeded: peak in-flight %d > cap %d", peak, cap)
}
if peak < 1 {
t.Fatalf("expected at least one in-flight git process, saw %d", peak)
}
}
// countTrack returns (#start lines, #stop lines) in the track file.
func countTrack(t *testing.T, path string) (int, int) {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read track file: %v", err)
}
var starts, stops int
for _, line := range strings.Split(string(data), "\n") {
switch {
case strings.HasPrefix(line, "start "):
starts++
case strings.HasPrefix(line, "stop "):
stops++
}
}
return starts, stops
}
// TestCancelBeforeAcquireDoesNotSpawn asserts that a Run with an
// already-cancelled context returns ctx.Err() and never spawns git.
func TestCancelBeforeAcquireDoesNotSpawn(t *testing.T) {
var spawned int32
withFakeGit(t, `
echo spawned >> "$GITCMD_SPAWN_LOG"
exit 0
`)
spawnLog := filepath.Join(t.TempDir(), "spawn.log")
t.Setenv("GITCMD_SPAWN_LOG", spawnLog)
ctx, cancel := context.WithCancel(context.Background())
cancel() // cancel before calling Run
out, err := Run(ctx, "", "status")
if !errors.Is(err, context.Canceled) {
t.Fatalf("expected context.Canceled, got %v", err)
}
if len(out) != 0 {
t.Fatalf("expected empty stdout on pre-cancel, got %q", out)
}
// The fake git, if it ran, would have created the spawn log.
if _, statErr := os.Stat(spawnLog); statErr == nil {
t.Fatalf("git was spawned despite cancelled context")
}
_ = atomic.LoadInt32(&spawned)
}
// TestCancelWhileWaitingForSlot asserts that when the limiter is
// saturated, a Run whose ctx is cancelled while waiting for a slot
// returns ctx.Err() and never spawns its own git.
func TestCancelWhileWaitingForSlot(t *testing.T) {
withFakeGit(t, `
echo "start $$" >> "$GITCMD_TRACK"
while [ ! -f "$GITCMD_RELEASE" ]; do
sleep 0.005
done
exit 0
`)
trackFile := filepath.Join(t.TempDir(), "track")
releaseFile := filepath.Join(t.TempDir(), "release")
t.Setenv("GITCMD_TRACK", trackFile)
t.Setenv("GITCMD_RELEASE", releaseFile)
if err := os.WriteFile(trackFile, nil, 0o644); err != nil {
t.Fatalf("seed track file: %v", err)
}
SetConcurrency(1)
t.Cleanup(func() { SetConcurrency(int(defaultConcurrency())) })
// Saturate the single slot.
blockerDone := make(chan struct{})
go func() {
defer close(blockerDone)
_, _ = Run(context.Background(), "", "status")
}()
// Wait until the blocker is actually running git.
waitForStarts(t, trackFile, 1, 5*time.Second)
// Now a waiter whose ctx we cancel while it blocks on Acquire.
ctx, cancel := context.WithCancel(context.Background())
waiterErr := make(chan error, 1)
go func() {
_, err := Run(ctx, "", "status")
waiterErr <- err
}()
// Give the waiter a moment to enter Acquire, then cancel.
time.Sleep(50 * time.Millisecond)
cancel()
select {
case err := <-waiterErr:
if !errors.Is(err, context.Canceled) {
t.Fatalf("waiter: expected context.Canceled, got %v", err)
}
case <-time.After(5 * time.Second):
t.Fatal("waiter did not return after ctx cancel")
}
// Only the blocker should have spawned git; the waiter must not.
if starts, _ := countTrack(t, trackFile); starts != 1 {
t.Fatalf("expected exactly 1 spawned git (the blocker), saw %d", starts)
}
// Release the blocker.
if err := os.WriteFile(releaseFile, nil, 0o644); err != nil {
t.Fatalf("write release file: %v", err)
}
<-blockerDone
}
func waitForStarts(t *testing.T, path string, want int, timeout time.Duration) {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if starts, _ := countTrack(t, path); starts >= want {
return
}
time.Sleep(2 * time.Millisecond)
}
t.Fatalf("timed out waiting for %d git starts in %s", want, path)
}
// TestFailingGitSurfacesStderr asserts the wrapped error carries git's
// stderr and the failing subcommand name.
func TestFailingGitSurfacesStderr(t *testing.T) {
withFakeGit(t, `
echo "fatal: not a git repository" 1>&2
exit 128
`)
out, err := Run(context.Background(), "", "rev-parse", "HEAD")
if err == nil {
t.Fatal("expected error from failing git, got nil")
}
msg := err.Error()
if !strings.Contains(msg, "fatal: not a git repository") {
t.Fatalf("stderr not surfaced in error: %q", msg)
}
if !strings.Contains(msg, "git rev-parse") {
t.Fatalf("subcommand name not in error: %q", msg)
}
// stdout is still returned (empty here).
_ = out
}
// TestOutputTrimsAndCarriesError verifies Output trims stdout and
// propagates the wrapped error.
func TestOutputTrimsAndCarriesError(t *testing.T) {
withFakeGit(t, `
if [ "$1" = "ok" ]; then
printf ' deadbeef\n\n'
exit 0
fi
echo "boom" 1>&2
exit 1
`)
got, err := Output(context.Background(), "", "ok")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != "deadbeef" {
t.Fatalf("Output did not trim: %q", got)
}
_, err = Output(context.Background(), "", "fail")
if err == nil || !strings.Contains(err.Error(), "boom") {
t.Fatalf("Output should surface stderr, got %v", err)
}
}
// TestRunPassesDirAsDashC ensures the -C dir flag is prepended when a
// dir is supplied and omitted when it is empty.
func TestRunPassesDirAsDashC(t *testing.T) {
withFakeGit(t, `
printf '%s\n' "$@" > "$GITCMD_ARGS"
exit 0
`)
argsFile := filepath.Join(t.TempDir(), "args")
t.Setenv("GITCMD_ARGS", argsFile)
if _, err := Run(context.Background(), "/some/dir", "status"); err != nil {
t.Fatalf("Run error: %v", err)
}
data, err := os.ReadFile(argsFile)
if err != nil {
t.Fatalf("read args file: %v", err)
}
lines := splitNonEmpty(string(data))
want := []string{"-C", "/some/dir", "status"}
if len(lines) != len(want) {
t.Fatalf("args = %v, want %v", lines, want)
}
for i := range want {
if lines[i] != want[i] {
t.Fatalf("arg[%d] = %q, want %q (full %v)", i, lines[i], want[i], lines)
}
}
// Empty dir: no -C prefix.
if _, err := Run(context.Background(), "", "status"); err != nil {
t.Fatalf("Run error: %v", err)
}
data, _ = os.ReadFile(argsFile)
lines = splitNonEmpty(string(data))
if len(lines) != 1 || lines[0] != "status" {
t.Fatalf("empty-dir args = %v, want [status]", lines)
}
}
func splitNonEmpty(s string) []string {
var out []string
for _, p := range strings.Split(s, "\n") {
if p != "" {
out = append(out, p)
}
}
return out
}
// TestSetConcurrencyClamps verifies SetConcurrency clamps sub-1 values
// to 1 and that defaultConcurrency stays within [1,8].
func TestSetConcurrencyClamps(t *testing.T) {
SetConcurrency(0)
if got := currentSem(); got == nil {
t.Fatal("limiter is nil after SetConcurrency(0)")
}
SetConcurrency(-5)
if got := currentSem(); got == nil {
t.Fatal("limiter is nil after SetConcurrency(-5)")
}
// Restore default for other tests.
SetConcurrency(int(defaultConcurrency()))
d := defaultConcurrency()
if d < 1 || d > 8 {
t.Fatalf("defaultConcurrency() = %d, want within [1,8]", d)
}
}