Skip to content
quarry Artifact [4460ec7367]
Login

Artifact [4460ec7367]

Artifact 4460ec73671e5732b86ee838b7a38c69b763483d78ad634366286a083236a89e:


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
}