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