package runner
import (
"context"
"net/http/httptest"
"os"
"path/filepath"
"sort"
"sync"
"testing"
"time"
)
// dirCheckout records the workspace each checkout is asked for, and
// leaves a marker the first time a workspace is used, so a reused one
// is seen to keep its checkout.
type dirCheckout struct {
manifest string
mu sync.Mutex
dirs []string
fresh []bool
}
func (d *dirCheckout) Checkout(_ context.Context, dir, _ string) error {
marker := filepath.Join(dir, "marker")
_, err := os.Stat(marker)
d.mu.Lock()
d.dirs = append(d.dirs, dir)
d.fresh = append(d.fresh, err != nil)
d.mu.Unlock()
if err := os.MkdirAll(filepath.Join(dir, "ci"), 0o755); err != nil {
return err
}
if err := os.WriteFile(marker, nil, 0o644); err != nil {
return err
}
return os.WriteFile(filepath.Join(dir, ManifestPath), []byte(d.manifest), 0o644)
}
// runManifestTasks runs n one-second manifest tasks of one type and
// returns how long they took, and what the checkouts saw.
func runManifestTasks(t *testing.T, n, maxTasks int) (time.Duration, *dirCheckout) {
t.Helper()
co := &dirCheckout{manifest: `{"tasks":{"t-race":["sleep","1"]}}`}
var queue []polled
for i := 0; i < n; i++ {
queue = append(queue, task(string(rune('a'+i)), "c1"))
}
br := &fakeBridge{queue: map[string][]polled{"t-race": queue}, resolved: map[string]map[string]any{}, logs: map[string]string{}}
srv := httptest.NewServer(br.handler())
defer srv.Close()
r, err := New(Config{
Bridge: srv.URL, Token: "WT", Tasks: []string{"t-race"}, MaxTasks: maxTasks,
WorkDir: t.TempDir(), Checkout: co, Heartbeat: 50 * time.Millisecond, Logf: t.Logf,
})
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() { r.Run(ctx); close(done) }()
start := time.Now()
for deadline := start.Add(15 * time.Second); time.Now().Before(deadline); time.Sleep(20 * time.Millisecond) {
br.mu.Lock()
got := len(br.resolved)
br.mu.Unlock()
if got == n {
break
}
}
took := time.Since(start)
cancel()
<-done
br.mu.Lock()
defer br.mu.Unlock()
for id, res := range br.resolved {
if res["action"] != "complete" {
t.Fatalf("FAIL: task %s resolved %v", id, res)
}
}
if len(br.resolved) != n {
t.Fatalf("FAIL: %d of %d tasks resolved", len(br.resolved), n)
}
return took, co
}
// MANIFEST TASKS OF ONE TYPE RUN SIDE BY SIDE UP TO --max-tasks.
//
// Two landings' race checks used to queue behind one workspace per task
// type. Each now takes a numbered workspace, so two one-second tasks of
// one type finish in about one second, each in a workspace of its own.
func TestManifestTasksOfOneTypeRunSideBySide(t *testing.T) {
took, co := runManifestTasks(t, 2, 2)
if took >= 1900*time.Millisecond {
t.Errorf("FAIL: two one-second tasks took %s with --max-tasks 2; want them side by side", took)
}
if len(co.dirs) != 2 || co.dirs[0] == co.dirs[1] {
t.Errorf("FAIL: the two tasks shared a workspace: %v", co.dirs)
}
t.Logf("ok: two one-second tasks in %s", took.Round(10*time.Millisecond))
}
// A THIRD TASK WAITS ONLY WHEN --max-tasks ARE BUSY.
func TestManifestTaskWaitsOnlyWhenMaxTasksAreBusy(t *testing.T) {
took, _ := runManifestTasks(t, 3, 2)
if took < 1900*time.Millisecond || took >= 2900*time.Millisecond {
t.Errorf("FAIL: three one-second tasks took %s with --max-tasks 2; want two rounds", took)
}
}
// A REUSED WORKSPACE KEEPS ITS CHECKOUT; A NEW ONE STARTS CLEAN.
func TestManifestWorkspaceIsReusedIncrementally(t *testing.T) {
_, co := runManifestTasks(t, 3, 2)
seen := map[string]bool{}
for i, dir := range co.dirs {
if co.fresh[i] != !seen[dir] {
t.Errorf("FAIL: checkout %d of %s fresh=%v, want %v", i, dir, co.fresh[i], !seen[dir])
}
seen[dir] = true
}
var names []string
for d := range seen {
names = append(names, filepath.Base(d))
}
sort.Strings(names)
if len(names) != 2 {
t.Errorf("FAIL: three tasks with --max-tasks 2 used %d workspaces: %v", len(names), names)
}
}