1// SPDX-License-Identifier: MIT
2
3package executor
4
5import (
6 "os"
7 "path/filepath"
8 "testing"
9 "time"
10)
11
12// fakeCtrDocker is a docker CLI for the container steps: it records every
13// call (one argument per line between "--" markers, like fakeDocker) and
14// answers like the real one. Behaviour is chosen by files in its directory:
15//
16// pull_fail N: the first N pulls fail
17// health words, one per inspect: starting, healthy, unhealthy
18// create_fail docker create exits 125
19// exec_out what docker exec prints
20// exec_vars names of variables docker exec prints ("VAR NAME=value")
21// hang_exec docker exec waits until it gets a signal
22type fakeCtr struct {
23 dir, bin, log string
24}
25
26func newFakeCtr(t *testing.T) *fakeCtr {
27 t.Helper()
28 d := t.TempDir()
29 f := &fakeCtr{dir: d, bin: filepath.Join(d, "docker"), log: filepath.Join(d, "calls")}
30 script := `#!/bin/sh
31D=` + d + `
32{ echo "--"; for a in "$@"; do printf "%s\n" "$a"; done; echo "ENV:CVAR=$CVAR:EMPTYV=${EMPTYV-unset}:PATH=$PATH"; } >> $D/calls
33[ "$1" = "--config" ] && { CFG=$2; shift 2; }
34hang() { if [ -f $D/hang_ignore ]; then trap '' INT; trap 'exit 143' TERM; else trap 'exit 130' INT TERM; fi; while :; do sleep 0.05; done; }
35[ -f $D/hang_$1 ] && hang
36if [ -f $D/hang_for ]; then case "$*" in *"$(cat $D/hang_for)"*) hang ;; esac; fi
37case "$1" in
38version) echo "'1.48'" ;;
39ps) case "$*" in *--quiet*) [ -f $D/created ] && cat $D/created ;; *status=running*) id=$(echo "$*" | sed 's/.*id=\([^ ]*\).*/\1/'); echo "$id Up Less than a second" ;; esac ;;
40network) if [ "$2" = rm ]; then
41 [ -f $D/netrm_fail ] && { echo "Error response from daemon: network is busy"; exit 1; }
42 [ -f $D/netrm_absent ] && { echo "Error response from daemon: network $3 not found"; exit 1; }
43 fi ;;
44login) cat > $D/login_stdin
45 if [ -f $D/lock_cfg ]; then mkdir -p $CFG/sub; : > $CFG/sub/f; chmod 500 $CFG/sub; fi ;;
46pull) if [ -f $D/pull_fail ]; then n=$(cat $D/pull_fail); if [ "$n" -gt 0 ]; then echo $((n-1)) > $D/pull_fail; echo "pull denied"; exit 1; fi; fi; echo "pulled $2" ;;
47create) [ -f $D/create_fail ] && { echo "unknown flag"; exit 125; }
48 [ -f $D/create_out ] && { cat $D/create_out; exit 0; }
49 n=$(cat $D/seq 2>/dev/null || echo 0); n=$((n+1)); echo $n > $D/seq; id=$(printf "%064d" $n); echo $id >> $D/created
50 [ -f $D/hang_create_after ] && hang
51 echo $id ;;
52start) echo "$2" ;;
53inspect) case "$*" in
54 *Healthcheck*) if [ -s $D/health ]; then w=$(head -1 $D/health); sed -i 1d $D/health; echo "$w"; fi ;;
55 *Config.Env*) printf 'HOME=/github/home\nGITHUB_ACTIONS=true\nPATH=/usr/bin:/bin\n' ;;
56 esac ;;
57port) [ -f $D/ports ] && cat $D/ports ;;
58logs) echo "service log of $3" ;;
59rm) [ -f $D/rm_fail ] && { echo "Error response from daemon: cannot remove container: boom"; exit 1; }
60 [ -f $D/rm_absent ] && { echo "Error response from daemon: No such container: $3"; exit 1; }
61 [ -f $D/created ] && { grep -v "^$3$" $D/created > $D/created.n; mv $D/created.n $D/created; }
62 echo "$3" ;;
63exec) if [ -f $D/add_path ]; then f=$(echo "$GITHUB_PATH" | sed "s#^/__w#$(cat $D/add_path)#"); echo /added >> "$f"; fi
64 [ -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"
65 [ -f $D/exec_vars ] && for v in $(cat $D/exec_vars); do eval "echo \"VAR $v=\${$v-unset}\""; done
66 [ -f $D/hang_exec ] && { trap 'exit 130' INT TERM; while :; do sleep 0.05; done; }
67 [ -f $D/exec_out ] && cat $D/exec_out
68 if [ -f $D/exec_exit ]; then n=$(cat $D/exec_exit); [ "$n" != 0 ] && echo 0 > $D/exec_exit && exit $n; fi ;;
69esac
70exit 0
71`
72 if err := os.WriteFile(f.bin, []byte(script), 0o755); err != nil {
73 t.Fatal(err)
74 }
75 return f
76}
77
78func (f *fakeCtr) set(t *testing.T, name, content string) {
79 t.Helper()
80 if err := os.WriteFile(filepath.Join(f.dir, name), []byte(content), 0o644); err != nil {
81 t.Fatal(err)
82 }
83}
84
85func (f *fakeCtr) calls(t *testing.T) [][]string { return calls(t, f.log) }
86
87// verbs are the docker subcommands called, in order.
88func (f *fakeCtr) verbs(t *testing.T) []string {
89 var out []string
90 for _, c := range f.calls(t) {
91 if c[0] == "--config" && len(c) > 2 {
92 c = c[2:]
93 }
94 v := c[0]
95 if v == "network" && len(c) > 1 {
96 v += " " + c[1]
97 }
98 out = append(out, v)
99 }
100 return out
101}
102
103// ctrRun runs a workflow whose job has the given container/services YAML.
104func ctrRun(t *testing.T, f *fakeCtr, jobYAML, steps string, tweak func(*Config)) (*recorder, Result) {
105 t.Helper()
106 return ctrRunC(t, f, nil, jobYAML, steps, tweak)
107}
108
109func ctrRunC(t *testing.T, f *fakeCtr, cancel *Canceller, jobYAML, steps string, tweak func(*Config)) (*recorder, Result) {
110 t.Helper()
111 needTools(t, "bash")
112 return runLocalC(t, cancel, wfHead+jobYAML+" steps:\n"+steps, "j", func(c *Config) {
113 c.Actions.DockerPath = f.bin
114 c.Actions.Backoff = func() time.Duration { return time.Millisecond }
115 c.Actions.ExternalsDir = fakeNode(t)
116 if tweak != nil {
117 tweak(c)
118 }
119 })
120}
121
122// concl is the conclusion of the step called name.
123func (r *recorder) concl(name string) Conclusion {
124 r.mu.Lock()
125 defer r.mu.Unlock()
126 for _, s := range r.steps {
127 if s.Name == name {
128 return s.Conclusion
129 }
130 }
131 return ""
132}