package main import ( "bytes" "os" "strings" "sync" "sync/atomic" "testing" ) func TestCpuWorkers(t *testing.T) { t.Setenv("BOOTSTRAP_PARALLELISM", "4") if n := cpuWorkers(); n != 4 { t.Errorf("expected 4 workers via env, got %d", n) } t.Setenv("BOOTSTRAP_PARALLELISM", "") if n := cpuWorkers(); n < 1 { t.Errorf("expected at least 1 worker, got %d", n) } } func TestHttpWorkersRespectsCap(t *testing.T) { t.Setenv("BOOTSTRAP_PARALLELISM", "32") defer func() { githubTokenSet = false }() githubTokenSet = false if n := httpWorkers(); n != httpWorkersCap { t.Errorf("expected http workers capped at %d without token, got %d", httpWorkersCap, n) } githubTokenSet = true if n := httpWorkers(); n != 32 { t.Errorf("expected http workers uncapped to 32 with token, got %d", n) } } func TestParallelDoConcurrency(t *testing.T) { const items = 16 var inFlight, peak int32 work := make([]int, items) for i := range work { work[i] = i } parallelDo(work, 4, func(_ int, _ int) { now := atomic.AddInt32(&inFlight, 1) for { cur := atomic.LoadInt32(&peak) if now <= cur || atomic.CompareAndSwapInt32(&peak, cur, now) { break } } // brief busy spin to keep multiple workers overlapping for i := 0; i < 50000; i++ { _ = i * i } atomic.AddInt32(&inFlight, -1) }) if peak < 2 { t.Errorf("expected at least 2 concurrent workers, observed peak %d", peak) } if peak > 4 { t.Errorf("worker cap violated: peak %d > 4", peak) } } func TestParallelDoEmpty(t *testing.T) { called := false parallelDo([]int{}, 4, func(_ int, _ int) { called = true }) if called { t.Error("expected fn to not be invoked on empty input") } } func TestParallelDoAllItemsProcessed(t *testing.T) { items := []int{1, 2, 3, 4, 5, 6, 7, 8} var sum int64 parallelDo(items, 3, func(_ int, v int) { atomic.AddInt64(&sum, int64(v)) }) if sum != 36 { t.Errorf("expected sum 36, got %d", sum) } } func TestTaskOutputSerial(t *testing.T) { t.Cleanup(resetMocks) tOut := newSerialOutput() // Serial: writes go straight to stdout; Writer() returns nil. if tOut.Writer() != nil { t.Error("expected Writer() to be nil in serial mode") } // flushing serial mode is a no-op var buf bytes.Buffer tOut.Flush(&buf) if buf.Len() != 0 { t.Error("expected Flush to be no-op in serial mode") } } func TestTaskOutputCaptured(t *testing.T) { tOut := newCapturedOutput("mypkg") tOut.Printf("first %s\n", "line") tOut.Println("second line") tOut.Printf("third line") var buf bytes.Buffer tOut.Flush(&buf) got := buf.String() expected := " [mypkg] first line\n [mypkg] second line\n [mypkg] third line\n" if got != expected { t.Errorf("captured output mismatch:\nexpected:\n%q\ngot:\n%q", expected, got) } // Flush is idempotent (second call writes nothing). buf.Reset() tOut.Flush(&buf) if buf.Len() != 0 { t.Errorf("expected second Flush to be empty, got %q", buf.String()) } } func TestWithTaskOutputRoutesPrints(t *testing.T) { tOut := newCapturedOutput("worker") withTaskOutput(tOut, func() { taskPrintf("hello %d\n", 7) taskPrintln("world") }) var buf bytes.Buffer tOut.Flush(&buf) got := buf.String() if !strings.Contains(got, "[worker] hello 7") || !strings.Contains(got, "[worker] world") { t.Errorf("expected routed output with prefix, got: %q", got) } } func TestWithTaskOutputUnsetsAfter(t *testing.T) { tOut := newCapturedOutput("worker") withTaskOutput(tOut, func() { if currentTask() == nil { t.Error("expected active task inside withTaskOutput") } }) if currentTask() != nil { t.Error("expected no active task after withTaskOutput returns") } } func TestParallelTaskOutputIsolation(t *testing.T) { // Each goroutine should see only its own task output, even though // they all share package-global state. const n = 8 var wg sync.WaitGroup results := make([]string, n) for i := 0; i < n; i++ { wg.Add(1) go func(i int) { defer wg.Done() label := "g" + string(rune('a'+i)) tOut := newCapturedOutput(label) withTaskOutput(tOut, func() { taskPrintf("from %s\n", label) }) var buf bytes.Buffer tOut.Flush(&buf) results[i] = buf.String() }(i) } wg.Wait() for i, got := range results { label := "g" + string(rune('a'+i)) want := " [" + label + "] from " + label + "\n" if got != want { t.Errorf("goroutine %d: expected %q, got %q", i, want, got) } } } func TestCmdOptsOutRoutesRunCmd(t *testing.T) { defer resetMocks() var buf bytes.Buffer // Pick a command guaranteed to exist and produce output. result := runCmdReal([]string{"echo", "hello-out"}, CmdOpts{Out: &buf}) if !result.OK() { t.Fatalf("echo failed: %v", result.Err) } got := buf.String() if !strings.Contains(got, "$ echo hello-out") { t.Errorf("expected command echo in Out, got: %q", got) } if !strings.Contains(got, "hello-out") { t.Errorf("expected stdout in Out, got: %q", got) } } func TestCmdOptsOutRoutesRunShell(t *testing.T) { defer resetMocks() var buf bytes.Buffer result := runShellReal("echo shell-out", CmdOpts{Out: &buf}) if !result.OK() { t.Fatalf("shell failed: %v", result.Err) } got := buf.String() if !strings.Contains(got, "$ echo shell-out") { t.Errorf("expected command echo in Out, got: %q", got) } if !strings.Contains(got, "shell-out") { t.Errorf("expected stdout in Out, got: %q", got) } } func TestIssueLogRoutesViaTaskOutput(t *testing.T) { defer resetMocks() // Suppress fallback stdout for the non-task branch. issueLogWriter = &bytes.Buffer{} tOut := newCapturedOutput("isolated") withTaskOutput(tOut, func() { warn("a warning") errLog("an error") }) var buf bytes.Buffer tOut.Flush(&buf) out := buf.String() if !strings.Contains(out, "[isolated] [WARN] a warning") { t.Errorf("expected routed WARN line, got: %q", out) } if !strings.Contains(out, "[isolated] [ERROR] an error") { t.Errorf("expected routed ERROR line, got: %q", out) } // Issues should still flow into the global issues slice. if !hasErrors() { t.Error("expected errLog to register a global error even when routed via task") } } func TestParallelPartitionOrdering(t *testing.T) { items := []int{1, 2, 3, 4, 5, 6, 7, 8} even, odd := parallelPartition(items, func(v int) bool { return v%2 == 0 }) wantEven := []int{2, 4, 6, 8} wantOdd := []int{1, 3, 5, 7} if !equalIntSlices(even, wantEven) { t.Errorf("evens: want %v, got %v", wantEven, even) } if !equalIntSlices(odd, wantOdd) { t.Errorf("odds: want %v, got %v", wantOdd, odd) } } func equalIntSlices(a, b []int) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } func TestGoidUnique(t *testing.T) { mainID := goid() if mainID == 0 { t.Error("goid returned 0 for main goroutine") } var wg sync.WaitGroup wg.Add(1) var childID uint64 go func() { defer wg.Done() childID = goid() }() wg.Wait() if childID == 0 { t.Error("goid returned 0 for child goroutine") } if childID == mainID { t.Errorf("expected child goroutine id %d to differ from main id %d", childID, mainID) } } // Verify BOOTSTRAP_PARALLELISM=0 falls back to NumCPU rather than 0 workers. func TestCpuWorkersInvalidEnv(t *testing.T) { t.Setenv("BOOTSTRAP_PARALLELISM", "0") if n := cpuWorkers(); n < 1 { t.Errorf("expected fallback to runtime.NumCPU for invalid env, got %d", n) } } func TestCpuWorkersBadEnv(t *testing.T) { t.Setenv("BOOTSTRAP_PARALLELISM", "notanumber") if n := cpuWorkers(); n < 1 { t.Errorf("expected fallback for non-numeric env, got %d", n) } } // Ensure the parallelDo guard returns when len(items) == 0 even with // maxWorkers larger than 1. Smoke test for the early return. func TestParallelDoBigWorkersSmallInput(t *testing.T) { items := []string{"only"} called := 0 var mu sync.Mutex parallelDo(items, 32, func(_ int, _ string) { mu.Lock() called++ mu.Unlock() }) if called != 1 { t.Errorf("expected exactly 1 invocation, got %d", called) } } // Confirm runCmd inside withTaskOutput auto-routes via Out without callers // having to set it explicitly — that's the contract that lets install // handlers rely on taskOut(). func TestRunCmdInsideWithTaskOutput(t *testing.T) { defer resetMocks() tOut := newCapturedOutput("autoroute") withTaskOutput(tOut, func() { runCmdReal([]string{"echo", "auto"}, CmdOpts{Out: taskOut()}) }) var buf bytes.Buffer tOut.Flush(&buf) got := buf.String() if !strings.Contains(got, "[autoroute] $ echo auto") { t.Errorf("expected routed echo command, got: %q", got) } if !strings.Contains(got, "[autoroute] auto") { t.Errorf("expected routed echo stdout, got: %q", got) } } // Sanity: when no token is in env and the user prompt is suppressed, // httpWorkers stays capped. Just exercise the path; ensures no panic. func TestPromptGitHubTokenNoOp(t *testing.T) { defer func() { githubTokenSet = false }() githubTokenSet = false os.Unsetenv("GITHUB_TOKEN") // We can't easily prompt in a test; just confirm cap is in effect. n := httpWorkers() if n > httpWorkersCap { t.Errorf("expected http workers <= %d without token, got %d", httpWorkersCap, n) } }