e2e/build_cancel_test.go
170 lines · 7900 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\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 opts := fmt.Sprintf("-p %d -i %s -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=%s -o BatchMode=yes",
121 inst.port, runnerKey, filepath.Join(inst.sshDir, "known_hosts"))
122 runner := exec.Command(inst.runner, "-once", "-remote", "git@127.0.0.1", "-ssh-opts", opts,
123 "-clone-base", fmt.Sprintf("ssh://git@127.0.0.1:%d", inst.port), "-workdir", t.TempDir())
124 runner.Env = append(os.Environ(), "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null")
125 var runnerOut strings.Builder
126 runner.Stdout, runner.Stderr = &runnerOut, &runnerOut
127 if err := runner.Start(); err != nil {
128 t.Fatal(err)
129 }
130 exited := make(chan error, 1)
131 go func() { exited <- runner.Wait() }()
132 t.Cleanup(func() { runner.Process.Kill() })
133 deadline := time.Now().Add(30 * time.Second)
134 for !strings.Contains(slowList(), "slow\trunning") {
135 if time.Now().After(deadline) {
136 t.Fatalf("runner never claimed the build:\n%s\nbuild list:\n%s", runnerOut.String(), slowList())
137 }
138 time.Sleep(200 * time.Millisecond)
139 }
140 started := time.Now()
141 out, errOut, code := inst.ssh(t, aliceKey, "", "build", "cancel", "alice/slow", "1")
142 if code != 0 || !strings.Contains(out, "the runner stops at its next check") {
143 t.Fatalf("cancel running: exit %d %s%s", code, out, errOut)
144 }
145 select {
146 case <-exited:
147 case <-time.After(60 * time.Second):
148 t.Fatalf("runner still running 60s after cancel:\n%s", runnerOut.String())
149 }
150 // Well inside the step's 120s sleep: the runner stopped because it
151 // was told to, not because the step ended.
152 if took := time.Since(started); took > 45*time.Second {
153 t.Fatalf("runner took %s to stop", took)
154 }
155 if !strings.Contains(runnerOut.String(), "cancelled") {
156 t.Fatalf("runner did not say it was cancelled:\n%s", runnerOut.String())
157 }
158 // The row, log and status say cancelled, and the runner's late report
159 // changed none of them.
160 if list := slowList(); !strings.Contains(list, "slow\tcancelled") {
161 t.Fatalf("build after cancel:\n%s", list)
162 }
163 log, _, _ := inst.ssh(t, aliceKey, "", "build", "log", "alice/slow", "1")
164 if !strings.Contains(log, "starting") || !strings.Contains(log, "cancelled by alice while running") || strings.Contains(log, "never") {
165 t.Fatalf("log after cancel:\n%s", log)
166 }
167 if st, _, _ := inst.ssh(t, aliceKey, "", "status", "list", "alice/slow", sha); !strings.Contains(st, "error") || !strings.Contains(st, "cancelled") {
168 t.Fatalf("status after cancel:\n%s", st)
169 }
170}