e2e/build_cancel_test.go
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}