Skip to content
quarry Artifact [9b0dcc14ab]
Login

Artifact [9b0dcc14ab]

Artifact 9b0dcc14aba7a531ccc5f84a4721c392ec94f7386ea1b660d8a8b7f018680e76:


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"]))
			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: {}"),
		},
	}
	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"}) {
		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]["branch"] != "trunk" || labels[0]["checkin"] != "t1" {
		t.Errorf("FAIL: the tag run's labels %v; want its tag, branch and check-in", 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"}) {
		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"}) {
		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"}) {
		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 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: {}")},
	}
	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")
}