test(executor): container lifecycle, cleanup after failure, cancel and timeouts, credentials
- commit
- 40f32c558663922d6c7fa53a231f69f52091b5a2
- author
- brice · · 2026-10-14 05:53 UTC
- parent
- 4d5e5f8
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +200 -3
M internal/executor/containers_fake_test.go +20 -2
| @@ -49,7 +49,8 @@ logs) echo "service log of $3" ;; | |||
| 49 | 49 | rm) echo "$3" ;; | |
| 50 | 50 | exec) [ -f $D/exec_env ] && echo "ENV GITHUB_WORKSPACE=$GITHUB_WORKSPACE RUNNER_TEMP=$RUNNER_TEMP HOME=${HOME:-<unset>} GITHUB_OUTPUT=$GITHUB_OUTPUT GITHUB_EVENT_PATH=$GITHUB_EVENT_PATH" | |
| 51 | 51 | [ -f $D/hang_exec ] && { trap 'exit 130' INT TERM; while :; do sleep 0.05; done; } | |
| 52 | - | [ -f $D/exec_out ] && cat $D/exec_out ;; | |
| 52 | + | [ -f $D/exec_out ] && cat $D/exec_out | |
| 53 | + | if [ -f $D/exec_exit ]; then n=$(cat $D/exec_exit); [ "$n" != 0 ] && echo 0 > $D/exec_exit && exit $n; fi ;; | |
| 53 | 54 | esac | |
| 54 | 55 | exit 0 | |
| 55 | 56 | ` | |
| @@ -86,9 +87,14 @@ func (f *fakeCtr) verbs(t *testing.T) []string { | |||
| 86 | 87 | ||
| 87 | 88 | // ctrRun runs a workflow whose job has the given container/services YAML. | |
| 88 | 89 | func ctrRun(t *testing.T, f *fakeCtr, jobYAML, steps string, tweak func(*Config)) (*recorder, Result) { | |
| 90 | + | t.Helper() | |
| 91 | + | return ctrRunC(t, f, nil, jobYAML, steps, tweak) | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | func ctrRunC(t *testing.T, f *fakeCtr, cancel *Canceller, jobYAML, steps string, tweak func(*Config)) (*recorder, Result) { | |
| 89 | 95 | t.Helper() | |
| 90 | 96 | needTools(t, "bash") | |
| 91 | - | return runLocal(t, wfHead+jobYAML+" steps:\n"+steps, "j", func(c *Config) { | |
| 97 | + | return runLocalC(t, cancel, wfHead+jobYAML+" steps:\n"+steps, "j", func(c *Config) { | |
| 92 | 98 | c.Actions.DockerPath = f.bin | |
| 93 | 99 | c.Actions.Backoff = func() time.Duration { return time.Millisecond } | |
| 94 | 100 | c.Actions.ExternalsDir = fakeNode(t) | |
| @@ -105,3 +111,15 @@ func joinCalls(cs [][]string) string { | |||
| 105 | 111 | } | |
| 106 | 112 | return b.String() | |
| 107 | 113 | } | |
| 114 | + | ||
| 115 | + | // concl is the conclusion of the step called name. | |
| 116 | + | func (r *recorder) concl(name string) Conclusion { | |
| 117 | + | r.mu.Lock() | |
| 118 | + | defer r.mu.Unlock() | |
| 119 | + | for _, s := range r.steps { | |
| 120 | + | if s.Name == name { | |
| 121 | + | return s.Conclusion | |
| 122 | + | } | |
| 123 | + | } | |
| 124 | + | return "" | |
| 125 | + | } | |
| @@ -49,7 +49,8 @@ logs) echo "service log of $3" ;; | |||||
| 49 | rm) echo "$3" ;; | 49 | rm) echo "$3" ;; | ||
| 50 | exec) [ -f $D/exec_env ] && echo "ENV GITHUB_WORKSPACE=$GITHUB_WORKSPACE RUNNER_TEMP=$RUNNER_TEMP HOME=${HOME:-<unset>} GITHUB_OUTPUT=$GITHUB_OUTPUT GITHUB_EVENT_PATH=$GITHUB_EVENT_PATH" | 50 | exec) [ -f $D/exec_env ] && echo "ENV GITHUB_WORKSPACE=$GITHUB_WORKSPACE RUNNER_TEMP=$RUNNER_TEMP HOME=${HOME:-<unset>} GITHUB_OUTPUT=$GITHUB_OUTPUT GITHUB_EVENT_PATH=$GITHUB_EVENT_PATH" | ||
| 51 | [ -f $D/hang_exec ] && { trap 'exit 130' INT TERM; while :; do sleep 0.05; done; } | 51 | [ -f $D/hang_exec ] && { trap 'exit 130' INT TERM; while :; do sleep 0.05; done; } | ||
| 52 | - | [ -f $D/exec_out ] && cat $D/exec_out ;; | 52 | + | [ -f $D/exec_out ] && cat $D/exec_out |
| 53 | + | if [ -f $D/exec_exit ]; then n=$(cat $D/exec_exit); [ "$n" != 0 ] && echo 0 > $D/exec_exit && exit $n; fi ;; | |||
| 53 | esac | 54 | esac | ||
| 54 | exit 0 | 55 | exit 0 | ||
| 55 | ` | 56 | ` | ||
| @@ -86,9 +87,14 @@ func (f *fakeCtr) verbs(t *testing.T) []string { | |||||
| 86 | 87 | ||||
| 87 | // ctrRun runs a workflow whose job has the given container/services YAML. | 88 | // ctrRun runs a workflow whose job has the given container/services YAML. | ||
| 88 | func ctrRun(t *testing.T, f *fakeCtr, jobYAML, steps string, tweak func(*Config)) (*recorder, Result) { | 89 | func ctrRun(t *testing.T, f *fakeCtr, jobYAML, steps string, tweak func(*Config)) (*recorder, Result) { | ||
| 90 | + | t.Helper() | |||
| 91 | + | return ctrRunC(t, f, nil, jobYAML, steps, tweak) | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | func ctrRunC(t *testing.T, f *fakeCtr, cancel *Canceller, jobYAML, steps string, tweak func(*Config)) (*recorder, Result) { | |||
| 89 | t.Helper() | 95 | t.Helper() | ||
| 90 | needTools(t, "bash") | 96 | needTools(t, "bash") | ||
| 91 | - | return runLocal(t, wfHead+jobYAML+" steps:\n"+steps, "j", func(c *Config) { | 97 | + | return runLocalC(t, cancel, wfHead+jobYAML+" steps:\n"+steps, "j", func(c *Config) { |
| 92 | c.Actions.DockerPath = f.bin | 98 | c.Actions.DockerPath = f.bin | ||
| 93 | c.Actions.Backoff = func() time.Duration { return time.Millisecond } | 99 | c.Actions.Backoff = func() time.Duration { return time.Millisecond } | ||
| 94 | c.Actions.ExternalsDir = fakeNode(t) | 100 | c.Actions.ExternalsDir = fakeNode(t) | ||
| @@ -105,3 +111,15 @@ func joinCalls(cs [][]string) string { | |||||
| 105 | } | 111 | } | ||
| 106 | return b.String() | 112 | return b.String() | ||
| 107 | } | 113 | } | ||
| 114 | + | ||||
| 115 | + | // concl is the conclusion of the step called name. | |||
| 116 | + | func (r *recorder) concl(name string) Conclusion { | |||
| 117 | + | r.mu.Lock() | |||
| 118 | + | defer r.mu.Unlock() | |||
| 119 | + | for _, s := range r.steps { | |||
| 120 | + | if s.Name == name { | |||
| 121 | + | return s.Conclusion | |||
| 122 | + | } | |||
| 123 | + | } | |||
| 124 | + | return "" | |||
| 125 | + | } | |||
A internal/executor/containers_life_test.go +173 -0
| @@ -0,0 +1,173 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package executor | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "os" | |
| 7 | + | "path/filepath" | |
| 8 | + | "strings" | |
| 9 | + | "testing" | |
| 10 | + | "time" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | const ctrJob = " container: ubuntu:24.04\n" | |
| 14 | + | ||
| 15 | + | func 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 | + | ||
| 44 | + | func 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 | + | ||
| 61 | + | func 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 | + | ||
| 84 | + | func 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 | + | ||
| 110 | + | func 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 | + | ||
| 131 | + | func 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 | + | ||
| 148 | + | func 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 | + | ||
| 160 | + | func 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 | + | } | |
| @@ -0,0 +1,173 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package executor | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "os" | |||
| 7 | + | "path/filepath" | |||
| 8 | + | "strings" | |||
| 9 | + | "testing" | |||
| 10 | + | "time" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | const ctrJob = " container: ubuntu:24.04\n" | |||
| 14 | + | ||||
| 15 | + | func 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 | + | ||||
| 44 | + | func 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 | + | ||||
| 61 | + | func 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 | + | ||||
| 84 | + | func 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 | + | ||||
| 110 | + | func 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 | + | ||||
| 131 | + | func 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 | + | ||||
| 148 | + | func 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 | + | ||||
| 160 | + | func 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 | + | } | |||
M internal/executor/corpus_test.go +7 -1
| @@ -155,6 +155,12 @@ func testGitHubContext(job string) string { | |||
| 155 | 155 | // runLocal runs one job of a workflow file on this machine under a temporary | |
| 156 | 156 | // home. The environment is minimal and fixed. | |
| 157 | 157 | func runLocal(t *testing.T, yaml, jobKey string, tweak func(*Config)) (*recorder, Result) { | |
| 158 | + | t.Helper() | |
| 159 | + | return runLocalC(t, nil, yaml, jobKey, tweak) | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | // runLocalC is runLocal with a canceller. | |
| 163 | + | func runLocalC(t *testing.T, cancel *Canceller, yaml, jobKey string, tweak func(*Config)) (*recorder, Result) { | |
| 158 | 164 | t.Helper() | |
| 159 | 165 | home := t.TempDir() | |
| 160 | 166 | layout := NewLayout(home, "hello", filepath.Join(home, "toolcache")) | |
| @@ -169,7 +175,7 @@ func runLocal(t *testing.T, yaml, jobKey string, tweak func(*Config)) (*recorder | |||
| 169 | 175 | if tweak != nil { | |
| 170 | 176 | tweak(&cfg) | |
| 171 | 177 | } | |
| 172 | - | res := Run(nil, cfg) | |
| 178 | + | res := Run(cancel, cfg) | |
| 173 | 179 | return rec, res | |
| 174 | 180 | } | |
| 175 | 181 | ||
| @@ -155,6 +155,12 @@ func testGitHubContext(job string) string { | |||||
| 155 | // runLocal runs one job of a workflow file on this machine under a temporary | 155 | // runLocal runs one job of a workflow file on this machine under a temporary | ||
| 156 | // home. The environment is minimal and fixed. | 156 | // home. The environment is minimal and fixed. | ||
| 157 | func runLocal(t *testing.T, yaml, jobKey string, tweak func(*Config)) (*recorder, Result) { | 157 | func runLocal(t *testing.T, yaml, jobKey string, tweak func(*Config)) (*recorder, Result) { | ||
| 158 | + | t.Helper() | |||
| 159 | + | return runLocalC(t, nil, yaml, jobKey, tweak) | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | // runLocalC is runLocal with a canceller. | |||
| 163 | + | func runLocalC(t *testing.T, cancel *Canceller, yaml, jobKey string, tweak func(*Config)) (*recorder, Result) { | |||
| 158 | t.Helper() | 164 | t.Helper() | ||
| 159 | home := t.TempDir() | 165 | home := t.TempDir() | ||
| 160 | layout := NewLayout(home, "hello", filepath.Join(home, "toolcache")) | 166 | layout := NewLayout(home, "hello", filepath.Join(home, "toolcache")) | ||
| @@ -169,7 +175,7 @@ func runLocal(t *testing.T, yaml, jobKey string, tweak func(*Config)) (*recorder | |||||
| 169 | if tweak != nil { | 175 | if tweak != nil { | ||
| 170 | tweak(&cfg) | 176 | tweak(&cfg) | ||
| 171 | } | 177 | } | ||
| 172 | - | res := Run(nil, cfg) | 178 | + | res := Run(cancel, cfg) |
| 173 | return rec, res | 179 | return rec, res | ||
| 174 | } | 180 | } | ||
| 175 | 181 | ||||