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}