package runner
import (
"context"
"encoding/json"
"net/http/httptest"
"testing"
"time"
)
// WORKFLOW JOBS RUN SIDE BY SIDE UP TO --max-tasks.
//
// Every job is task type <group>.job, and the runner kept one workspace
// per task type behind a lock, so jobs ran one at a time whatever
// --max-tasks said (measured on zeitforge by an outside tester). Two
// jobs that each sleep a second finish in about one second, not two.
func TestWorkflowJobsRunSideBySide(t *testing.T) {
job, _ := json.Marshal(Job{Steps: []JobStep{{Run: "sleep 1"}}})
meta := map[string]string{JobKey: string(job)}
a, b := task("a", "c1"), task("b", "c1")
a.Metadata, b.Metadata = meta, meta
br := &fakeBridge{queue: map[string][]polled{"t-job": {a, b}}, 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-job"}, MaxTasks: 2,
WorkDir: t.TempDir(), Checkout: appCheckout{}, 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(10 * time.Second); time.Now().Before(deadline); time.Sleep(20 * time.Millisecond) {
br.mu.Lock()
n := len(br.resolved)
br.mu.Unlock()
if n == 2 {
break
}
}
took := time.Since(start)
cancel()
<-done
br.mu.Lock()
defer br.mu.Unlock()
if br.resolved["a"]["action"] != "complete" || br.resolved["b"]["action"] != "complete" {
t.Fatalf("FAIL: resolved %v", br.resolved)
}
if took >= 1900*time.Millisecond {
t.Errorf("FAIL: two one-second jobs took %s with --max-tasks 2; want them side by side", took)
}
t.Logf("ok: two one-second jobs in %s", took.Round(10*time.Millisecond))
}