Skip to content
quarry Artifact [5e0b01712f]
Login

Artifact [5e0b01712f]

Artifact 5e0b01712fb0a291cab16fc2f360db12149c3f29ea3bf7ac9c7ddb181e3fc72a:


package ci

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

// ACTIONS' BRANCH PATTERNS, AND ON:'S RULES, AS RFC 0019 STATES THEM.
func TestOnFiltersReadAsActionsReadsThem(t *testing.T) {
	for _, c := range []struct {
		pat, name string
		want      bool
	}{
		{"trunk", "trunk", true}, {"trunk", "trunk2", false},
		{"release/*", "release/1.2", true}, {"release/*", "release/1/2", false},
		{"release/**", "release/1/2", true}, {"feature-?", "feature-a", true},
		{"v[12].*", "v2.0", true}, {"v[12].*", "v3.0", false}, {"a+b", "aaab", true},
		{"*", "a/b", false}, {"**", "a/b", true}, {"a.b", "axb", false},
	} {
		got, err := globMatch(c.pat, c.name)
		if err != nil || got != c.want {
			t.Errorf("FAIL: %q against %q = %v, %v; want %v", c.pat, c.name, got, err, c.want)
		}
	}
	on := func(s string) json.RawMessage { return json.RawMessage(s) }
	for _, c := range []struct {
		on   string
		ev   event
		want bool
	}{
		{``, event{kind: "push", branch: "feat"}, true},
		{`{"push":{}}`, event{kind: "landing", branch: "trunk"}, false},
		{`{"push":{"branches":["trunk"]}}`, event{kind: "push", branch: "trunk"}, true},
		{`{"push":{"branches":["trunk"]}}`, event{kind: "push", branch: "feat"}, false},
		{`{"push":{"branches":["release/**","!release/old/**"]}}`, event{kind: "push", branch: "release/old/1"}, false},
		{`{"push":{"branches":["release/**","!release/old/**"]}}`, event{kind: "push", branch: "release/new/1"}, true},
		{`{"push":{"branches-ignore":["wip/*"]}}`, event{kind: "push", branch: "wip/x"}, false},
		{`{"landing":{"branches":["trunk"]}}`, event{kind: "landing", branch: "trunk"}, true},
		{`{"push":{"branches":["trunk"]}}`, event{kind: "workflow_dispatch", pushed: "trunk"}, true},
		{`{"push":{"branches":["trunk"]}}`, event{kind: "workflow_dispatch", pushed: "feat"}, false},
		{`{"workflow_dispatch":{}}`, event{kind: "workflow_dispatch", pushed: "feat"}, true},
		{`{"schedule":{}}`, event{kind: "push", branch: "trunk"}, false},
	} {
		got, err := runsOn(on(c.on), c.ev)
		if err != nil || got != c.want {
			t.Errorf("FAIL: on %s for %+v = %v, %v; want %v", c.on, c.ev, got, err, c.want)
		}
	}
	for _, bad := range []string{
		`{"pull_request":{}}`,
		`{"push":{"paths":["src/**"]}}`,
		`{"push":{"tags":["v*"]}}`,
		`{"push":{"branches":["a"],"branches-ignore":["b"]}}`,
		`{"push":{"branches":["[a"]}}`,
	} {
		if _, err := runsOn(on(bad), event{kind: "push", branch: "a"}); err == nil {
			t.Errorf("FAIL: on %s was accepted; want a refusal that names it", bad)
		}
	}
	t.Log("ok: patterns, negation, ignore lists, re-runs and refusals")
}

// A JOBS: WORKFLOW RUNS WHEN ITS ON: SAYS, BY EVERY DOOR.
//
// The engine compiles the ci.yml with the repository's group as the
// task namespace and hands on: back; the trigger judges it for a push,
// a landing's merge (by its target branch) and a re-run.
func TestAWorkflowRunsWhenItsOnSays(t *testing.T) {
	var mu sync.Mutex
	var started, namespaces []string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.URL.Path == "/v1/ci/compile":
			var body struct {
				Name, Spec    string
				TaskNamespace string `json:"task_namespace"`
			}
			json.NewDecoder(r.Body).Decode(&body)
			mu.Lock()
			namespaces = append(namespaces, body.TaskNamespace)
			mu.Unlock()
			on, _, _ := strings.Cut(body.Spec, "\n") // the fixture's first line is its normalised on:
			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)
			mu.Lock()
			started = append(started, fmt.Sprint(body.Input["checkin"], " ", body.Input["pipeline"]))
			mu.Unlock()
			w.WriteHeader(http.StatusCreated)
			fmt.Fprint(w, `{"run_id":"r"}`)
		case r.URL.Path == "/runs":
			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": "old"}}
	repo := &fakeRepo{
		files: []string{"ci/deploy.yml", "ci/test.yml"},
		blobs: map[string][]byte{
			"ci/test.yml":   []byte(`{"push":{},"landing":{}}` + "\njobs: {}"),
			"ci/deploy.yml": []byte(`{"push":{"branches":["trunk","release/*"]}}` + "\njobs: {}"),
		},
		landings: map[string]bool{},
		branches: map[string]string{"t1": "trunk", "b1": "feat", "r1": "release/2"},
	}
	push := func(c Checkin) []string {
		repo.news = []Checkin{c}
		if err := Trigger(e, st, "alpha", repo, "http://x/r/alpha"); err != nil {
			t.Fatal(err)
		}
		return take()
	}
	if got := push(Checkin{Hash: "b1", Branch: "feat"}); !slices.Equal(got, []string{"b1 ci/test.yml"}) {
		t.Errorf("FAIL: a push to feat started %v; want the tests only", got)
	}
	if got := push(Checkin{Hash: "t1", Branch: "trunk"}); !slices.Equal(got, []string{"t1 ci/deploy.yml", "t1 ci/test.yml"}) {
		t.Errorf("FAIL: a push to trunk started %v; want the deploy too", got)
	}
	if got := push(Checkin{Hash: "r1", Branch: "release/2"}); !slices.Equal(got, []string{"r1 ci/deploy.yml", "r1 ci/test.yml"}) {
		t.Errorf("FAIL: a push to release/2 started %v; want the deploy too (release/*)", got)
	}
	if _, err := RunPairAt(e, st, "alpha", repo, "http://x/r/alpha", "feat", "b1", "t1"); err != nil {
		t.Fatal(err)
	}
	if got := take(); !slices.Equal(got, []string{Pair("b1", "t1") + " ci/test.yml"}) {
		t.Errorf("FAIL: a landing into trunk started %v; want the tests only", got)
	}
	repo.news = nil
	if err := RunAt(e, st, "alpha", repo, "http://x/r/alpha", "t1"); err != nil {
		t.Fatal(err)
	}
	if got := take(); !slices.Equal(got, []string{"t1 ci/deploy.yml", "t1 ci/test.yml"}) {
		t.Errorf("FAIL: a re-run of a trunk check-in started %v; want the deploy too (the rollback)", got)
	}
	if err := RunAt(e, st, "alpha", repo, "http://x/r/alpha", "b1"); err != nil {
		t.Fatal(err)
	}
	if got := take(); !slices.Equal(got, []string{"b1 ci/test.yml"}) {
		t.Errorf("FAIL: a re-run of a feat check-in started %v; want no deploy", got)
	}
	repo.blobs["ci/deploy.yml"] = []byte(`{"pull_request":{}}` + "\njobs: {}")
	err := RunAt(e, st, "alpha", repo, "http://x/r/alpha", "t1")
	if err == nil || !strings.Contains(err.Error(), "pull_request") {
		t.Errorf("FAIL: an on: quarry does not emit answered %v; want a refusal naming it", err)
	}
	if got := take(); !slices.Equal(got, []string{"t1 ci/test.yml"}) {
		t.Errorf("FAIL: beside the refused workflow, started %v; want the tests", got)
	}
	mu.Lock()
	defer mu.Unlock()
	for _, ns := range namespaces {
		if ns != GroupFor("alpha") {
			t.Errorf("FAIL: compiled with task namespace %q; want the repository's group %q", ns, GroupFor("alpha"))
			break
		}
	}
	t.Log("ok: on: decides for pushes, landings and re-runs; the namespace is the repository's group")
}