e2e/build_cancel_test.go

ebf1e30fe0ae09b2d5f93705c1cbf7f513b6ed22
gitbay/e2e/build_cancel_test.go history · blame · raw

173 lines · 8101 bytes

  1package e2e
  2
  3import (
  4	"fmt"
  5	"os"
  6	"os/exec"
  7	"path/filepath"
  8	"strings"
  9	"testing"
 10	"time"
 11)
 12
 13// build cancel withdraws a queued build; a running one is the runner's.
 14// Cancelling a duplicate of a commit that already passed puts that
 15// result back on the commit.
 16func TestBuildCancel(t *testing.T) {
 17	inst := startInstance(t)
 18	inst.runner = buildRunner(t)
 19	aliceKey := inst.newKey(t, "alice")
 20	bobKey := inst.newKey(t, "bob")
 21	runnerKey := inst.newKey(t, "ci")
 22	inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
 23	inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
 24	inst.admin(t, "admin", "user", "create", "ci", "--key", runnerKey+".pub", "--admin")
 25	if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
 26		t.Fatal("repo create failed")
 27	}
 28	work := t.TempDir()
 29	env := inst.gitEnv(aliceKey)
 30	mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
 31	dir := filepath.Join(work, "w")
 32	os.MkdirAll(filepath.Join(dir, ".gitbay"), 0o755)
 33	os.WriteFile(filepath.Join(dir, ".gitbay", "ci.yml"), []byte("jobs:\n  unit:\n    steps:\n      - echo fine\n"), 0o644)
 34	mustGit(t, dir, env, "checkout", "-q", "-b", "main")
 35	mustGit(t, dir, env, "add", ".")
 36	mustGit(t, dir, env, "commit", "-q", "-m", "ci")
 37	mustGit(t, dir, env, "push", "-q", "origin", "main")
 38	sha := strings.Fields(strings.TrimSpace(mustGit(t, dir, env, "ls-remote", "origin", "refs/heads/main")))[0]
 39
 40	// Build 1 is queued. A reader cannot cancel it; the owner can.
 41	if _, _, code := inst.ssh(t, bobKey, "", "build", "cancel", "alice/app", "1"); code != 4 {
 42		t.Fatal("reader cancelled a build")
 43	}
 44	if out, errOut, code := inst.ssh(t, aliceKey, "", "build", "cancel", "alice/app", "1"); code != 0 || !strings.Contains(out, "cancelled alice/app build 1") {
 45		t.Fatalf("cancel: exit %d %s%s", code, out, errOut)
 46	}
 47	if list := inst.buildList(t, aliceKey); !strings.Contains(list, "unit\tcancelled") {
 48		t.Fatalf("build list after cancel:\n%s", list)
 49	}
 50	if out, _, _ := inst.ssh(t, aliceKey, "", "build", "log", "alice/app", "1"); !strings.Contains(out, "cancelled by alice") {
 51		t.Fatalf("log after cancel:\n%s", out)
 52	}
 53	if st, _, _ := inst.ssh(t, aliceKey, "", "status", "list", "alice/app", sha); !strings.Contains(st, "error") || !strings.Contains(st, "cancelled") {
 54		t.Fatalf("status after cancel:\n%s", st)
 55	}
 56	if _, _, code := inst.ssh(t, aliceKey, "", "build", "cancel", "alice/app", "1"); code != 2 {
 57		t.Fatal("cancelled a cancelled build")
 58	}
 59	// Nothing for a runner to claim.
 60	if out := inst.runnerOnce(t, runnerKey); strings.Contains(out, "unit") && !strings.Contains(out, "no pending") {
 61		t.Fatalf("runner picked up a cancelled build:\n%s", out)
 62	}
 63	if status, _ := inst.get(t, "/alice/app/badge/build.svg"); status != 200 {
 64		t.Fatalf("badge after cancel: %d", status)
 65	}
 66
 67	// Build 2: triggered, run to success. Build 3: the same commit queued
 68	// again; cancelling it restores the passed result on the commit.
 69	if _, _, code := inst.ssh(t, aliceKey, "", "build", "trigger", "alice/app", "unit"); code != 0 {
 70		t.Fatal("trigger failed")
 71	}
 72	inst.runnerOnce(t, runnerKey)
 73	if list := inst.buildList(t, aliceKey); !strings.Contains(list, "unit\tsuccess") {
 74		t.Fatalf("build 2 did not pass:\n%s", list)
 75	}
 76	if _, _, code := inst.ssh(t, aliceKey, "", "build", "trigger", "alice/app", "unit"); code != 0 {
 77		t.Fatal("second trigger failed")
 78	}
 79	if st, _, _ := inst.ssh(t, aliceKey, "", "status", "list", "alice/app", sha); !strings.Contains(st, "pending") {
 80		t.Fatalf("status before cancelling the duplicate:\n%s", st)
 81	}
 82	if _, errOut, code := inst.ssh(t, aliceKey, "", "build", "cancel", "alice/app", "3"); code != 0 {
 83		t.Fatalf("cancel duplicate: %s", errOut)
 84	}
 85	if st, _, _ := inst.ssh(t, aliceKey, "", "status", "list", "alice/app", sha); !strings.Contains(st, "success") || !strings.Contains(st, "passed in build 2") {
 86		t.Fatalf("status after cancelling the duplicate:\n%s", st)
 87	}
 88}
 89
 90// Cancelling a running build ends it at the runner within seconds: the
 91// server closes the log session, the runner kills the step, and its late
 92// report lands on a row that already says cancelled.
 93func TestBuildCancelRunning(t *testing.T) {
 94	inst := startInstance(t)
 95	inst.runner = buildRunner(t)
 96	aliceKey := inst.newKey(t, "alice")
 97	runnerKey := inst.newKey(t, "ci")
 98	inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
 99	inst.admin(t, "admin", "user", "create", "ci", "--key", runnerKey+".pub", "--admin")
100	if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/slow"); code != 0 {
101		t.Fatal("repo create failed")
102	}
103	work := t.TempDir()
104	env := inst.gitEnv(aliceKey)
105	mustGit(t, work, env, "clone", inst.sshURL("alice/slow"), "w")
106	dir := filepath.Join(work, "w")
107	os.MkdirAll(filepath.Join(dir, ".gitbay"), 0o755)
108	os.WriteFile(filepath.Join(dir, ".gitbay", "ci.yml"), []byte("jobs:\n  slow:\n    steps:\n      - echo starting\n      - sleep 120 & wait\n      - echo never\n"), 0o644)
109	mustGit(t, dir, env, "checkout", "-q", "-b", "main")
110	mustGit(t, dir, env, "add", ".")
111	mustGit(t, dir, env, "commit", "-q", "-m", "slow")
112	mustGit(t, dir, env, "push", "-q", "origin", "main")
113	sha := strings.Fields(strings.TrimSpace(mustGit(t, dir, env, "ls-remote", "origin", "refs/heads/main")))[0]
114	slowList := func() string {
115		out, _, _ := inst.ssh(t, aliceKey, "", "build", "list", "alice/slow")
116		return out
117	}
118
119	// A real runner, in the background, claims the build and sits in sleep.
120	// The step forks sleep as a child of the shell, the way dash runs
121	// every command, so the cancel must reach past the shell to end it.
122	opts := fmt.Sprintf("-p %d -i %s -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=%s -o BatchMode=yes",
123		inst.port, runnerKey, filepath.Join(inst.sshDir, "known_hosts"))
124	runner := exec.Command(inst.runner, "-once", "-remote", "git@127.0.0.1", "-ssh-opts", opts,
125		"-isolation", "none",
126		"-clone-base", fmt.Sprintf("ssh://git@127.0.0.1:%d", inst.port), "-workdir", t.TempDir())
127	runner.Env = append(os.Environ(), "XDG_CONFIG_HOME="+t.TempDir(), "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null")
128	var runnerOut strings.Builder
129	runner.Stdout, runner.Stderr = &runnerOut, &runnerOut
130	if err := runner.Start(); err != nil {
131		t.Fatal(err)
132	}
133	exited := make(chan error, 1)
134	go func() { exited <- runner.Wait() }()
135	t.Cleanup(func() { runner.Process.Kill() })
136	deadline := time.Now().Add(30 * time.Second)
137	for !strings.Contains(slowList(), "slow\trunning") {
138		if time.Now().After(deadline) {
139			t.Fatalf("runner never claimed the build:\n%s\nbuild list:\n%s", runnerOut.String(), slowList())
140		}
141		time.Sleep(200 * time.Millisecond)
142	}
143	started := time.Now()
144	out, errOut, code := inst.ssh(t, aliceKey, "", "build", "cancel", "alice/slow", "1")
145	if code != 0 || !strings.Contains(out, "the runner stops at its next check") {
146		t.Fatalf("cancel running: exit %d %s%s", code, out, errOut)
147	}
148	select {
149	case <-exited:
150	case <-time.After(60 * time.Second):
151		t.Fatalf("runner still running 60s after cancel:\n%s", runnerOut.String())
152	}
153	// Well inside the step's 120s sleep: the runner stopped because it
154	// was told to, not because the step ended.
155	if took := time.Since(started); took > 45*time.Second {
156		t.Fatalf("runner took %s to stop", took)
157	}
158	if !strings.Contains(runnerOut.String(), "cancelled") {
159		t.Fatalf("runner did not say it was cancelled:\n%s", runnerOut.String())
160	}
161	// The row, log and status say cancelled, and the runner's late report
162	// changed none of them.
163	if list := slowList(); !strings.Contains(list, "slow\tcancelled") {
164		t.Fatalf("build after cancel:\n%s", list)
165	}
166	log, _, _ := inst.ssh(t, aliceKey, "", "build", "log", "alice/slow", "1")
167	if !strings.Contains(log, "starting") || !strings.Contains(log, "cancelled by alice while running") || strings.Contains(log, "never") {
168		t.Fatalf("log after cancel:\n%s", log)
169	}
170	if st, _, _ := inst.ssh(t, aliceKey, "", "status", "list", "alice/slow", sha); !strings.Contains(st, "error") || !strings.Contains(st, "cancelled") {
171		t.Fatalf("status after cancel:\n%s", st)
172	}
173}