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:"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(task.Argv, []string{"make", "race"}) {
if pipeline.When != "landing" {
t.Errorf("FAIL: %s runs make race on every push; it belongs in a \"when\": \"landing\" pipeline", f)
}
racePipeline = filepath.Base(f)
}
}
}
if racePipeline == "" {
t.Fatal("FAIL: no pipeline step runs `make race`")
}
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)*\tgo test -race `).Match(mk) {
t.Error("FAIL: the Makefile has no race target running go test -race")
}
t.Logf("ok: %s runs make race on trunk and landing tips, and make check runs it too", racePipeline)
}