package runner
import (
"encoding/json"
"os"
"path/filepath"
"regexp"
"slices"
"testing"
"github.com/danmestas/dagnats/dag"
)
// quarry's own CI: every step in its pipelines names a task the runner
// manifest can run (a step no manifest entry serves sits queued until
// it times out), no step re-runs a failed attempt, and the race
// detector is its own pipeline, marked to run on trunk and landing tips
// only, as the same make target a local `make check` runs.
func TestQuarrysPipelineRunsTheRaceTargetAsItsOwnStep(t *testing.T) {
root := filepath.Join("..", "..")
b, err := os.ReadFile(filepath.Join(root, ManifestPath))
if err != nil {
t.Fatal(err)
}
m, err := ParseManifest(b)
if err != nil {
t.Fatal(err)
}
files, err := filepath.Glob(filepath.Join(root, "ci", "*.json"))
if err != nil {
t.Fatal(err)
}
racePipeline := ""
for _, f := range files {
if filepath.Base(f) == filepath.Base(ManifestPath) {
continue
}
b, err := os.ReadFile(f)
if err != nil {
t.Fatal(err)
}
var pipeline struct {
When string `json:"quarry_when"`
Steps []struct {
ID string `json:"id"`
Task string `json:"task"`
Retry *struct {
MaxAttempts *int `json:"max_attempts"`
} `json:"retry"`
} `json:"steps"`
}
if err := json.Unmarshal(b, &pipeline); err != nil {
t.Fatal(err)
}
// The engine's own reading: the file is a valid workflow, and
// after a first failed attempt (Attempts == 1) no step is
// scheduled again (dagnats' internal/engine/retry_handlers.go
// retries while Attempts <= MaxAttempts). Why every step runs
// once: ci/quarry.json's _comment.
var def dag.WorkflowDef
if err := json.Unmarshal(b, &def); err != nil {
t.Fatal(err)
}
if err := dag.Validate(def); err != nil {
t.Fatalf("FAIL: the engine would refuse %s: %v", f, err)
}
for _, st := range def.Steps {
if p := dag.ResolveRetryPolicy(def, st); p != nil && 1 <= p.MaxAttempts {
t.Errorf("FAIL: %s step %q re-runs a failed attempt (max_attempts %d)", f, st.ID, p.MaxAttempts)
}
}
for _, s := range pipeline.Steps {
if s.Retry == nil || s.Retry.MaxAttempts == nil {
t.Errorf("FAIL: %s step %q leaves its retry policy implicit", f, s.ID)
}
task, ok := m.Tasks[s.Task]
if !ok {
t.Errorf("FAIL: %s step %q runs task %q, which %s does not name", f, s.ID, s.Task, ManifestPath)
continue
}
if slices.Equal(pinnedZit(task.Argv), []string{"make", "race"}) {
if pipeline.When != "landing" {
t.Errorf("FAIL: %s runs make race on every push; it belongs in a \"quarry_when\": \"landing\" pipeline", f)
}
racePipeline = filepath.Base(f)
}
}
}
if racePipeline == "" {
t.Fatal("FAIL: no pipeline step runs `make race`")
}
for _, name := range []string{"quarry.test", "quarry.race"} {
if argv := m.Tasks[name].Argv; len(argv) < 2 || argv[1] != "scripts/ci-zit.sh" {
t.Errorf("FAIL: %s runs %v, not through scripts/ci-zit.sh: it would test with the runner's zit, not the one this check-in pins", name, argv)
}
}
mk, err := os.ReadFile(filepath.Join(root, "Makefile"))
if err != nil {
t.Fatal(err)
}
if !regexp.MustCompile(`(?m)^check:.*\brace\b`).Match(mk) {
t.Error("FAIL: make check does not run the race target CI runs")
}
if !regexp.MustCompile(`(?m)^race:\n(\t.*\n)*\t.*\bsh scripts/race\.sh\b`).Match(mk) {
t.Error("FAIL: the Makefile's race target does not run scripts/race.sh")
}
rs, err := os.ReadFile(filepath.Join(root, "scripts", "race.sh"))
if err != nil {
t.Fatal(err)
}
if !regexp.MustCompile(`(?m)^\s*go test -race `).Match(rs) {
t.Error("FAIL: scripts/race.sh runs no go test -race")
}
t.Logf("ok: %s runs make race on trunk and landing tips, and make check runs it too", racePipeline)
}
// pinnedZit is a task's command with the scripts/ci-zit.sh wrapper taken
// off, which runs it with the zit the check-in pins.
func pinnedZit(argv []string) []string {
if len(argv) > 2 && argv[0] == "sh" && argv[1] == "scripts/ci-zit.sh" {
return argv[2:]
}
return argv
}