Skip to content
quarry Artifact [9056311597]
Login

Artifact [9056311597]

Artifact 9056311597956f4680065dbaa0f3afad3e5e7a16b7acf9929799b8aaa284bfe9:


package ci

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"slices"
	"strings"
	"sync"
	"testing"
)

// A TAG ADDED TO A CHECK-IN STARTS ITS TAG WORKFLOWS, ONCE (RFC 0020).
//
// The run's subject is the tagged check-in, recorded as an ordinary run
// of it, so secret delivery judges it by that check-in's branch as it
// judges every run. Only workflows whose on.tag matches start: not the
// push tests, not a JSON pipeline. A tag run supersedes nothing, and a
// first sight starts nothing.
func TestATagStartsItsWorkflowsOnTheTaggedCheckin(t *testing.T) {
	var mu sync.Mutex
	var started []string
	var labels []map[string]string
	superseded := 0
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		mu.Lock()
		defer mu.Unlock()
		switch {
		case r.URL.Path == "/v1/ci/compile":
			var body struct{ Name, Spec string }
			json.NewDecoder(r.Body).Decode(&body)
			on, _, _ := strings.Cut(body.Spec, "\n")
			fmt.Fprintf(w, `{"workflow":{"name":%q},"registered":true,"on":%s}`, body.Name, on)
		case r.URL.Path == "/workflows":
			w.WriteHeader(http.StatusCreated)
			fmt.Fprint(w, `{"status":"registered"}`)
		case r.URL.Path == "/runs" && r.Method == http.MethodPost:
			var body struct {
				Input  map[string]any    `json:"input"`
				Labels map[string]string `json:"labels"`
			}
			json.NewDecoder(r.Body).Decode(&body)
			started = append(started, fmt.Sprint(body.Input["checkin"], " ", body.Input["branch"], " ", body.Input["pipeline"], " ", body.Input["tag"]))
			labels = append(labels, body.Labels)
			w.WriteHeader(http.StatusCreated)
			fmt.Fprint(w, `{"run_id":"r"}`)
		case r.URL.Path == "/runs":
			superseded++
			fmt.Fprint(w, `[]`)
		}
	}))
	defer srv.Close()
	take := func() []string {
		mu.Lock()
		defer mu.Unlock()
		out := started
		started = nil
		slices.Sort(out)
		return out
	}
	e := New(srv.URL)
	st := &fakeStore{mark: map[string]string{"alpha": "t1"}}
	repo := &fakeRepo{
		files: []string{"ci/build.json", "ci/release.yml", "ci/test.yml"},
		blobs: map[string][]byte{
			"ci/build.json":  []byte(`{"name":"build","version":"1.0","steps":[]}`),
			"ci/test.yml":    []byte(`{"push":{},"landing":{}}` + "\njobs: {}"),
			"ci/release.yml": []byte(`{"tag":{"tags":["version-*"]}}` + "\njobs: {}"),
		},
		news: []Checkin{{Hash: "t1", Branch: "trunk"}},
	}
	at := func(hash, branch string) Checkin { return Checkin{Hash: hash, Branch: branch, User: "dan"} }
	key := func(src, name, target string) TagKey { return TagKey{Src: src, Target: target, Name: name} }
	old := Tagged{TagKey: key("s0", "version-0.1.0", "t0"), At: at("t0", "trunk")}

	// First sight: what is there is remembered and nothing runs.
	repo.tags = []Tagged{old}
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	if got := take(); len(got) != 0 {
		t.Errorf("FAIL: first sight started %v; want nothing", got)
	}
	if seen, primed, _ := st.CITagsSeen("alpha"); !primed || !seen[old.TagKey] {
		t.Errorf("FAIL: first sight remembered %v (primed %v); want the existing tag", seen, primed)
	}

	release := Tagged{TagKey: key("s1", "version-0.2.0", "t1"), At: at("t1", "trunk")}
	moving := Tagged{TagKey: key("s1", "release", "t1"), At: at("t1", "trunk")}
	repo.tags = []Tagged{old, release, moving}
	superseded = 0
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	if got := take(); !slices.Equal(got, []string{"t1 trunk ci/release.yml version-0.2.0"}) {
		t.Errorf("FAIL: tagging t1 version-0.2.0 and release started %v; want the release workflow once, on t1", got)
	}
	if len(st.runs) != 1 || !strings.HasPrefix(st.runs[0], "r t1 ") {
		t.Errorf("FAIL: recorded runs %v; want one run of t1, the tagged check-in", st.runs)
	}
	if len(labels) != 1 || labels[0]["tag"] != "version-0.2.0" || labels[0]["subject"] != "tag" || labels[0]["checkin"] != "t1" || labels[0]["branch"] != "" {
		t.Errorf("FAIL: the tag run's labels %v; want its tag, subject=tag and check-in, and no branch label for a push to supersede", labels)
	}
	if superseded != 0 {
		t.Errorf("FAIL: a tag run swept %d time(s) for runs to supersede; want none", superseded)
	}
	if seen, _, _ := st.CITagsSeen("alpha"); !seen[moving.TagKey] || !seen[release.TagKey] {
		t.Errorf("FAIL: remembered %v; want the non-matching release tag too", seen)
	}

	// Nothing new: nothing starts again.
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	if got := take(); len(got) != 0 {
		t.Errorf("FAIL: a round with no new tag started %v", got)
	}

	// A tag on a branch is still a tag event; whether its run receives
	// secrets is the environment's branch policy, read from the branch
	// of the check-in the run records.
	feat := Tagged{TagKey: key("s2", "version-0.3.0", "b1"), At: at("b1", "feat")}
	repo.tags = append(repo.tags, feat)
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	if got := take(); !slices.Equal(got, []string{"b1 feat ci/release.yml version-0.3.0"}) {
		t.Errorf("FAIL: a tag on feat started %v; want the release workflow on b1, branch feat", got)
	}
	take()

	// Pushed late: a tag made before the tags already started, so it
	// sorts first by its card's date, but received only now. It starts.
	early := Tagged{TagKey: key("s-early", "version-0.1.1", "t0"), At: at("t0", "trunk")}
	repo.tags = append([]Tagged{early}, repo.tags...)
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	if got := take(); !slices.Equal(got, []string{"t0 trunk ci/release.yml version-0.1.1"}) {
		t.Errorf("FAIL: a tag dated before the others but received after them started %v; want it on t0", got)
	}

	// A tag received before its check-in: the index lists it only once
	// the check-in is folded, and then it starts, once.
	waiting := Tagged{TagKey: key("s3", "version-0.4.0", "t9"), At: at("t9", "trunk")}
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	repo.tags = append(repo.tags, waiting)
	for range 2 {
		if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
			t.Fatal(err)
		}
	}
	if got := take(); !slices.Equal(got, []string{"t9 trunk ci/release.yml version-0.4.0"}) {
		t.Errorf("FAIL: a tag whose check-in arrived later started %v over two rounds; want it once, on t9", got)
	}
	t.Log("ok: a tag starts its tag workflows once, on the tagged check-in, with its branch, whatever order it arrived in")
}

// A repository with check-ins and no tags at first sight remembers
// none, so its first tag starts rather than being taken for history.
func TestTheFirstTagOfAnUntaggedRepositoryStarts(t *testing.T) {
	var mu sync.Mutex
	var started []string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		mu.Lock()
		defer mu.Unlock()
		switch {
		case r.URL.Path == "/v1/ci/compile":
			var body struct{ Name, Spec string }
			json.NewDecoder(r.Body).Decode(&body)
			on, _, _ := strings.Cut(body.Spec, "\n")
			fmt.Fprintf(w, `{"workflow":{"name":%q},"registered":true,"on":%s}`, body.Name, on)
		case r.URL.Path == "/runs" && r.Method == http.MethodPost:
			var body struct {
				Input map[string]any `json:"input"`
			}
			json.NewDecoder(r.Body).Decode(&body)
			started = append(started, fmt.Sprint(body.Input["checkin"]))
			w.WriteHeader(http.StatusCreated)
			fmt.Fprint(w, `{"run_id":"r"}`)
		case r.URL.Path == "/runs":
			fmt.Fprint(w, `[]`)
		}
	}))
	defer srv.Close()
	e := New(srv.URL)
	st := &fakeStore{mark: map[string]string{"alpha": "t1"}}
	repo := &fakeRepo{
		files: []string{"ci/release.yml"},
		blobs: map[string][]byte{"ci/release.yml": []byte(`{"tag":{}}` + "\njobs: {}")},
		news:  []Checkin{{Hash: "t1", Branch: "trunk"}},
	}
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	if seen, primed, _ := st.CITagsSeen("alpha"); !primed || len(seen) != 0 {
		t.Fatalf("FAIL: an untagged first sight left %v (primed %v); want an empty, primed set", seen, primed)
	}
	repo.tags = []Tagged{{TagKey: TagKey{Src: "s", Target: "t1", Name: "version-0.2.0"}, At: Checkin{Hash: "t1", Branch: "trunk"}}}
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	mu.Lock()
	defer mu.Unlock()
	if !slices.Equal(started, []string{"t1"}) {
		t.Errorf("FAIL: the first tag started %v; want t1", started)
	}
	t.Log("ok: the first tag of an untagged repository starts")
}

// tagEngine is a fake engine for the tag walk's interactions with the
// rest of the trigger: it compiles a spec whose first line is its on:,
// starts runs, lists every run it started as running (so Supersede sees
// them and judges by their labels), and counts cancels.
type tagEngine struct {
	mu        sync.Mutex
	runs      []Run
	cancelled []string
}

func (f *tagEngine) handler() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		f.mu.Lock()
		defer f.mu.Unlock()
		switch {
		case r.URL.Path == "/v1/ci/compile":
			var body struct{ Name, Spec string }
			json.NewDecoder(r.Body).Decode(&body)
			on, _, _ := strings.Cut(body.Spec, "\n")
			fmt.Fprintf(w, `{"workflow":{"name":%q},"registered":true,"on":%s}`, body.Name, on)
		case r.URL.Path == "/runs" && r.Method == http.MethodPost:
			var body struct {
				Input  map[string]any    `json:"input"`
				Labels map[string]string `json:"labels"`
			}
			json.NewDecoder(r.Body).Decode(&body)
			id := fmt.Sprintf("r%d", len(f.runs))
			f.runs = append(f.runs, Run{RunID: id, Status: "running", Labels: body.Labels, Input: body.Input})
			w.WriteHeader(http.StatusCreated)
			fmt.Fprintf(w, `{"run_id":%q}`, id)
		case r.URL.Path == "/runs":
			json.NewEncoder(w).Encode(f.runs)
		case strings.HasSuffix(r.URL.Path, "/cancel"):
			f.cancelled = append(f.cancelled, strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/runs/"), "/cancel"))
		}
	}
}

// A TAG RUN IS NOT A PUSH: IT NEITHER ENDS A CHECK-IN'S WAIT NOR IS
// SUPERSEDED BY THE NEXT PUSH.
//
// A check-in deferred for a worker slot is still owed its push
// pipelines when a tag lands on it, whether or not a tag workflow
// matched. And with `on: {push, tag}` a release in flight on trunk is
// not a stale build of trunk: the next push must not cancel it.
func TestATagRunLeavesTheWaitAndSurvivesTheNextPush(t *testing.T) {
	f := &tagEngine{}
	srv := httptest.NewServer(f.handler())
	defer srv.Close()
	e := New(srv.URL)
	repo := &fakeRepo{
		files: []string{"ci/release.yml"},
		blobs: map[string][]byte{"ci/release.yml": []byte(`{"push":{},"tag":{}}` + "\njobs: {}")},
		news:  []Checkin{{Hash: "t1", Branch: "trunk"}},
	}
	st := &fakeStore{mark: map[string]string{"alpha": "t1"}}
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil { // prime
		t.Fatal(err)
	}
	b1 := Checkin{Hash: "b1", Branch: "feat"}
	st.Defer("alpha", b1)
	repo.tags = []Tagged{
		{TagKey: TagKey{Src: "s1", Target: "b1", Name: "version-0.2.0"}, At: b1},
		{TagKey: TagKey{Src: "s1", Target: "b1", Name: "nightly"}, At: b1}, // matches no workflow
		{TagKey: TagKey{Src: "s2", Target: "t1", Name: "version-0.3.0"}, At: Checkin{Hash: "t1", Branch: "trunk"}},
	}
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	if w, _ := st.Deferred("alpha"); len(w) != 1 || w[0].Hash != "b1" {
		t.Errorf("FAIL: after tags on the waiting b1, the waits are %v; want b1 still waiting for its push pipelines", w)
	}
	t2 := Checkin{Hash: "t2", Branch: "trunk"}
	pipelines, err := pipelinesAt(repo, t2)
	if err != nil {
		t.Fatal(err)
	}
	if err := launch(e, st, "alpha", repo, t2, "u", pipelines, scopePush); err != nil {
		t.Fatal(err)
	}
	f.mu.Lock()
	defer f.mu.Unlock()
	if len(f.runs) != 3 {
		t.Fatalf("FAIL: started %d runs; want the two tag runs and the push", len(f.runs))
	}
	if len(f.cancelled) != 0 {
		t.Errorf("FAIL: the push to trunk cancelled %v; want the tag runs left running", f.cancelled)
	}
	t.Log("ok: a tag run leaves a check-in's wait alone and is not superseded by the next push")
}

// A TAG RUNS EVEN WHEN ITS CHECK-IN ALREADY HAS A VERDICT.
//
// A push and a pair skip a pipeline the check-in already ran; a tag
// is its own event, so a release on a tested check-in still starts.
func TestATagRunsOnACheckinItsPushAlreadyRan(t *testing.T) {
	f := &tagEngine{}
	srv := httptest.NewServer(f.handler())
	defer srv.Close()
	e := New(srv.URL)
	t1 := Checkin{Hash: "t1", Branch: "trunk"}
	repo := &fakeRepo{
		files: []string{"ci/release.yml"},
		blobs: map[string][]byte{"ci/release.yml": []byte(`{"push":{},"tag":{}}` + "\njobs: {}")},
		news:  []Checkin{t1},
	}
	st := &fakeStore{mark: map[string]string{"alpha": "t1"}}
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil { // prime
		t.Fatal(err)
	}
	pipelines, err := pipelinesAt(repo, t1)
	if err != nil {
		t.Fatal(err)
	}
	if err := launch(e, st, "alpha", repo, t1, "u", pipelines, scopePush); err != nil {
		t.Fatal(err)
	}
	repo.tags = []Tagged{{TagKey: TagKey{Src: "s1", Target: "t1", Name: "version-0.4.0"}, At: t1}}
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	f.mu.Lock()
	defer f.mu.Unlock()
	if len(f.runs) != 2 {
		t.Fatalf("FAIL: started %d runs; want the push run and the tag run", len(f.runs))
	}
	t.Log("ok: a tag on a check-in with a verdict still starts its run")
}

// THE TAG WALK PRIMES ON THE FIRST ROUND THAT SEES A CHECK-IN.
//
// A repository created empty, or a mirror attached before its first
// pull, has no history yet when CI first looks; priming then would make
// every historical tag the pull brings look new. Nothing starts for the
// tags present at the first round with a check-in; a tag after it does.
func TestTheTagWalkPrimesOnTheFirstRoundWithACheckin(t *testing.T) {
	f := &tagEngine{}
	srv := httptest.NewServer(f.handler())
	defer srv.Close()
	e := New(srv.URL)
	repo := &fakeRepo{
		files: []string{"ci/release.yml"},
		blobs: map[string][]byte{"ci/release.yml": []byte(`{"tag":{}}` + "\njobs: {}")},
	}
	st := &fakeStore{mark: map[string]string{}}
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	if _, primed, _ := st.CITagsSeen("alpha"); primed {
		t.Fatal("FAIL: an empty repository primed the tag walk; want it to wait for a check-in")
	}
	at := Checkin{Hash: "t1", Branch: "trunk"}
	repo.news = []Checkin{at}
	repo.tags = []Tagged{
		{TagKey: TagKey{Src: "s1", Target: "t1", Name: "version-0.1.0"}, At: at},
		{TagKey: TagKey{Src: "s2", Target: "t1", Name: "version-0.2.0"}, At: at},
	}
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	f.mu.Lock()
	n := len(f.runs)
	f.mu.Unlock()
	if n != 0 {
		t.Errorf("FAIL: the first pull's history started %d tag run(s); want none", n)
	}
	repo.tags = append(repo.tags, Tagged{TagKey: TagKey{Src: "s3", Target: "t1", Name: "version-0.3.0"}, At: at})
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	f.mu.Lock()
	defer f.mu.Unlock()
	if len(f.runs) != 1 || f.runs[0].Input["tag"] != "version-0.3.0" {
		t.Errorf("FAIL: the tag after the first pull started %v; want version-0.3.0 once", f.runs)
	}
	t.Log("ok: the tag walk primes on the first round with a check-in, and a later tag starts")
}