v0.4.2 257b319

containers_life_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package executor
4
5import (
6	"os"
7	"path/filepath"
8	"strings"
9	"testing"
10	"time"
11)
12
13const ctrJob = "    container: ubuntu:24.04\n"
14
15func TestPullRetriesThenFailsAndStillCleansUp(t *testing.T) {
16	f := newFakeCtr(t)
17	f.set(t, "pull_fail", "9")
18	rec, res := ctrRun(t, f, "    container: no-such-image:nope\n", "      - name: never\n        run: echo never\n", nil)
19	if res.Conclusion != ConclusionFailure {
20		t.Fatalf("%+v", res)
21	}
22	if got := strings.Join(rec.names(), "|"); got != "Set up job|Initialize containers|never|Stop containers|Complete job" {
23		t.Fatalf("steps %s", got)
24	}
25	if c := rec.concl("never"); c != ConclusionSkipped {
26		t.Errorf("never: %s", c)
27	}
28	log := logOf(rec.step(t, "Initialize containers"))
29	if n := strings.Count(log, "##[command]"+f.bin+" pull no-such-image:nope"); n != 3 {
30		t.Errorf("%d pulls:\n%s", n, log)
31	}
32	for _, w := range []string{"##[warning]Docker pull failed with exit code 1, back off 0.001 seconds before retry.", "##[error]Docker pull failed with exit code 1"} {
33		if !strings.Contains(log, w) {
34			t.Errorf("lacks %q:\n%s", w, log)
35		}
36	}
37	// Nothing was created: the stop step has only the network to remove.
38	stop := logOf(rec.step(t, "Stop containers"))
39	if strings.Contains(stop, "Stop and remove container") || !strings.Contains(stop, "Remove container network: github_network_") {
40		t.Errorf("stop:\n%s", stop)
41	}
42}
43
44func TestCreateFailureMessage(t *testing.T) {
45	f := newFakeCtr(t)
46	f.set(t, "create_fail", "1")
47	rec, res := ctrRun(t, f, "    container:\n      image: ubuntu:24.04\n      options: --not-a-docker-flag\n", "      - run: echo never\n", nil)
48	if res.Conclusion != ConclusionFailure {
49		t.Fatalf("%+v", res)
50	}
51	log := logOf(rec.step(t, "Initialize containers"))
52	if !strings.Contains(log, "##[error]Exit code 125 returned from process: file name '"+f.bin+"', arguments 'create --name ") ||
53		!strings.Contains(log, " --not-a-docker-flag -e \"HOME=/github/home\"") {
54		t.Errorf("log:\n%s", log)
55	}
56	if got := logOf(rec.step(t, "Stop containers")); !strings.Contains(got, "Remove container network") {
57		t.Errorf("stop:\n%s", got)
58	}
59}
60
61func TestUnhealthyServiceFailsInit(t *testing.T) {
62	f := newFakeCtr(t)
63	f.set(t, "health", "starting\nstarting\nunhealthy\n")
64	rec, res := ctrRun(t, f, "    services:\n      broken:\n        image: redis:7\n", "      - run: echo never\n", nil)
65	if res.Conclusion != ConclusionFailure {
66		t.Fatalf("%+v", res)
67	}
68	log := logOf(rec.step(t, "Initialize containers"))
69	for _, w := range []string{
70		"##[group]Service container broken failed.\n##[command]" + f.bin + " logs --details ",
71		"##[error]Failed to initialize container redis:7\n##[endgroup]\n##[error]One or more containers failed to start.",
72	} {
73		if !strings.Contains(log, w) {
74			t.Errorf("lacks %q:\n%s", w, log)
75		}
76	}
77	// A service that failed to initialise has its logs printed once, not twice.
78	stop := logOf(rec.step(t, "Stop containers"))
79	if strings.Contains(stop, "Print service container logs") || !strings.Contains(stop, "Stop and remove container: ") {
80		t.Errorf("stop:\n%s", stop)
81	}
82}
83
84func TestRegistryCredentialsAreMaskedAndRemoved(t *testing.T) {
85	f := newFakeCtr(t)
86	var temp string
87	rec, res := ctrRun(t, f, "    container:\n      image: ghcr.io/o/img:1\n      credentials:\n        username: bob\n        password: hunter2-secret\n", "      - run: echo ok\n", func(c *Config) { temp = c.Layout.Temp })
88	if res.Conclusion != ConclusionSuccess {
89		t.Fatalf("%+v", res)
90	}
91	all := ""
92	for _, s := range rec.steps {
93		all += logOf(s)
94	}
95	if strings.Contains(all, "hunter2-secret") {
96		t.Errorf("password in the log")
97	}
98	if b, _ := os.ReadFile(filepath.Join(f.dir, "login_stdin")); string(b) != "hunter2-secret\n" {
99		t.Errorf("password not passed through stdin: %q", b)
100	}
101	log := logOf(rec.step(t, "Initialize containers"))
102	if !strings.Contains(log, " login ghcr.io -u bob --password-stdin") || !strings.Contains(log, " pull ghcr.io/o/img:1") || !strings.Contains(log, " --config ") {
103		t.Errorf("log:\n%s", log)
104	}
105	if m, _ := filepath.Glob(filepath.Join(temp, ".docker_*")); len(m) != 0 {
106		t.Errorf("config left behind: %v", m)
107	}
108}
109
110func TestCancelDuringStepStillStopsContainers(t *testing.T) {
111	f := newFakeCtr(t)
112	f.set(t, "hang_exec", "1")
113	cancel := NewCanceller()
114	go func() { time.Sleep(600 * time.Millisecond); cancel.Cancel(CancelUser) }()
115	rec, res := ctrRunC(t, f, cancel, ctrJob, "      - name: long\n        run: sleep 60\n", nil)
116	if res.Conclusion != ConclusionCancelled {
117		t.Fatalf("%+v %v", res, rec.names())
118	}
119	stop := rec.step(t, "Stop containers")
120	if got := logOf(stop); !strings.Contains(got, "Stop and remove container: ") || !strings.Contains(got, "Remove container network: ") {
121		t.Errorf("stop:\n%s", got)
122	}
123	if c := rec.concl("Stop containers"); c != ConclusionSuccess {
124		t.Errorf("Stop containers: %s", c)
125	}
126	if v := f.verbs(t); v[len(v)-1] != "network rm" || v[len(v)-2] != "rm" {
127		t.Errorf("last calls %v", v)
128	}
129}
130
131func TestStepTimeoutStopsContainersAndIsAFailure(t *testing.T) {
132	f := newFakeCtr(t)
133	f.set(t, "hang_exec", "1")
134	rec, res := ctrRun(t, f, ctrJob, "      - name: long\n        timeout-minutes: 1\n        run: sleep 60\n", func(c *Config) {
135		c.StepTimeoutSkew = -59*time.Second - 700*time.Millisecond
136	})
137	if res.Conclusion != ConclusionFailure {
138		t.Fatalf("%+v %v", res, rec.names())
139	}
140	if got := logOf(rec.step(t, "long")); !strings.Contains(got, "##[error]The action 'long' has timed out after 1 minutes.") {
141		t.Errorf("long:\n%s", got)
142	}
143	if got := logOf(rec.step(t, "Stop containers")); !strings.Contains(got, "Remove container network: ") {
144		t.Errorf("stop:\n%s", got)
145	}
146}
147
148func TestJobTimeoutStopsContainers(t *testing.T) {
149	f := newFakeCtr(t)
150	f.set(t, "hang_exec", "1")
151	rec, res := ctrRun(t, f, ctrJob, "      - name: long\n        run: sleep 60\n", func(c *Config) { c.Deadline = 700 * time.Millisecond })
152	if res.Conclusion != ConclusionTimedOut {
153		t.Fatalf("%+v %v", res, rec.names())
154	}
155	if got := logOf(rec.step(t, "Stop containers")); !strings.Contains(got, "Stop and remove container: ") || !strings.Contains(got, "Remove container network: ") {
156		t.Errorf("stop:\n%s", got)
157	}
158}
159
160func TestStopContainersRunsAfterAFailedStepAndPostStepsRunInTheContainer(t *testing.T) {
161	f := newFakeCtr(t)
162	f.set(t, "exec_exit", "1") // the first exec fails
163	rec, res := ctrRun(t, f, ctrJob, "      - name: bad\n        run: exit 1\n      - name: after\n        if: always()\n        run: echo after\n", nil)
164	if res.Conclusion != ConclusionFailure {
165		t.Fatalf("%+v", res)
166	}
167	if got := strings.Join(rec.names(), "|"); got != "Set up job|Initialize containers|bad|after|Stop containers|Complete job" {
168		t.Errorf("steps %s", got)
169	}
170	if len(f.execCalls(t)) != 2 {
171		t.Errorf("both steps run through docker exec: %v", f.execCalls(t))
172	}
173}
174
175// After a failed "Initialize containers" an always() step has no container:
176// the runner fails it before its header with a null ContainerId (recorded:
177// the ermite-dump step of ctr-errors).
178func TestStepAfterFailedInitFailsWithNullContainerId(t *testing.T) {
179	f := newFakeCtr(t)
180	f.set(t, "create_fail", "1")
181	tree := files{".github/actions/x/action.yml": "name: x\nruns:\n  using: node20\n  main: i.js\n", ".github/actions/x/i.js": "x\n"}
182	rec, res := ctrRun(t, f, ctrJob, "      - name: run\n        if: always()\n        run: echo no\n      - name: act\n        if: always()\n        uses: ./.github/actions/x\n", func(c *Config) {
183		writeFiles(t, c.Layout.Workspace, tree)
184	})
185	if res.Conclusion != ConclusionFailure {
186		t.Fatalf("%+v", res)
187	}
188	for _, n := range []string{"run", "act"} {
189		if got := logOf(rec.step(t, n)); got != "##[error]Value cannot be null. (Parameter 'ContainerId')" || rec.concl(n) != ConclusionFailure {
190			t.Errorf("%s: %q %s", n, got, rec.concl(n))
191		}
192	}
193}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close