| 1 | #!/bin/sh |
| 2 | # One go test role of the suite. |
| 3 | # |
| 4 | # hack/gotest.sh browser <name> [go test flags] go test -tags browser, one package |
| 5 | # hack/gotest.sh test <name> [go test flags] go test, one package |
| 6 | # hack/gotest.sh root '<split>' go test ./... minus the split packages |
| 7 | # |
| 8 | # <name> is a package path with its slashes written as dots |
| 9 | # (internal.designsystem), optionally ~<k>of<n> to run only the k-th of |
| 10 | # n slices of its tests. Flags after the name go to go test, after the |
| 11 | # role's own, and only for a whole package: a slice is its own -run. |
| 12 | # |
| 13 | # A browser role runs only its package's browser tests: the ones |
| 14 | # `go test -tags browser -list` names and plain `go test -list` does |
| 15 | # not. Every other test in the package runs once already, in root or in |
| 16 | # the package's test.<pkg> roles, and running it again under the tag |
| 17 | # was the same check paid for twice. ci_test.go checks that every test |
| 18 | # lands in exactly one role. |
| 19 | # |
| 20 | # A slice is dealt from the package's own test list: heaviest first, |
| 21 | # each onto the lightest slice so far, by the seconds in |
| 22 | # hack/test-weights.txt (a test not listed there weighs 1). Every shard |
| 23 | # computes the same deal, so the n slices of a package are a partition |
| 24 | # of it: each test runs in exactly one, and a test added later lands in |
| 25 | # a slice without anyone listing it. Two refusals keep that honest. A |
| 26 | # package that lists no tests is a broken build or a broken listing, not |
| 27 | # an empty package; and a slice dealt nothing means n is larger than the |
| 28 | # package, which would report green having run nothing. |
| 29 | # |
| 30 | # hack/gotest.sh --deal <mode> <pkg> <n> reads test names on stdin and |
| 31 | # prints "<k> <name>" for each: the deal alone, which amadan_ci_test.go |
| 32 | # checks is a partition without compiling anything. |
| 33 | set -eu |
| 34 | |
| 35 | weights=$(dirname "$0")/test-weights.txt |
| 36 | |
| 37 | # deal <mode> <pkg> <n>: names on stdin, "<k> <name>" out. Ties sort by |
| 38 | # name, so the deal does not depend on the order the names arrive in. |
| 39 | deal() { |
| 40 | awk -v mode="$1" -v pkg="$2" -v wf="$weights" ' |
| 41 | BEGIN { while ((getline l < wf) > 0) { split(l, f, " "); if (f[1] == mode && f[2] == pkg) w[f[3]] = f[4] } } |
| 42 | NF { print (($1 in w) ? w[$1] : 1), $1 }' | |
| 43 | sort -k1,1nr -k2,2 | |
| 44 | awk -v n="$3" ' |
| 45 | { best = 1; for (i = 2; i <= n; i++) if (load[i] < load[best]) best = i |
| 46 | load[best] += $1; print best, $2 }' |
| 47 | } |
| 48 | |
| 49 | case "${1:-}" in |
| 50 | --deal) |
| 51 | deal "$2" "$3" "$4" |
| 52 | exit 0 |
| 53 | ;; |
| 54 | root) |
| 55 | # Every package but the split ones, which run as roles of their own. |
| 56 | # Each split package must be one go list names: one that was renamed |
| 57 | # or deleted would otherwise drop out of both halves silently. |
| 58 | all=$(go list ./...) |
| 59 | keep=$all |
| 60 | for p in $2; do |
| 61 | ip=$(go list "./${p%@*}") |
| 62 | printf '%s\n' "$all" | grep -qx "$ip" || { |
| 63 | echo "gotest: $ip is split out of root but go list ./... does not name it" |
| 64 | exit 1 |
| 65 | } |
| 66 | keep=$(printf '%s\n' "$keep" | grep -vx "$ip") |
| 67 | done |
| 68 | # shellcheck disable=SC2086 # one argument per package, on purpose |
| 69 | exec go test -count=1 $keep |
| 70 | ;; |
| 71 | browser) |
| 72 | tags="-tags=browser" |
| 73 | timeout=20m |
| 74 | ;; |
| 75 | test) |
| 76 | tags= |
| 77 | timeout=10m |
| 78 | ;; |
| 79 | *) |
| 80 | echo "gotest: usage: gotest.sh browser|test <name> | root '<split>' | --deal <mode> <pkg> <n>" |
| 81 | exit 2 |
| 82 | ;; |
| 83 | esac |
| 84 | mode=$1 |
| 85 | |
| 86 | name=$2 |
| 87 | shift 2 |
| 88 | rel=$name |
| 89 | slice= |
| 90 | case "$name" in |
| 91 | *~*) |
| 92 | rel=${name%%~*} |
| 93 | slice=${name#*~} |
| 94 | ;; |
| 95 | esac |
| 96 | rel=$(printf '%s' "$rel" | tr . /) |
| 97 | pkg=./$rel/ |
| 98 | |
| 99 | # Chromium writes a profile per browser into TMPDIR, and a /tmp shared |
| 100 | # by a whole machine can fill, so browser roles prefer /var/tmp. Only |
| 101 | # where it is writable: amadan's bwrap sandbox mounts the root |
| 102 | # filesystem read-only and gives each job a private tmpfs /tmp, which |
| 103 | # is then the right place. Not inside the checkout, ever: a scratch |
| 104 | # module built under it would find this repo's .git walking up. |
| 105 | if [ "$mode" = browser ] && [ -z "${TMPDIR:-}" ] && [ -w /var/tmp ]; then |
| 106 | TMPDIR=/var/tmp |
| 107 | export TMPDIR |
| 108 | fi |
| 109 | |
| 110 | # The headless shell never restores a page from the back/forward cache, |
| 111 | # and ui's TestBusyButtonDrive needs one (no flag changes that; full |
| 112 | # Chromium of the same version does). Where `chromium` is a headless |
| 113 | # shell, as amadan's cloud runner links it, the packages the Makefile |
| 114 | # names in FULL_CHROMIUM use full Chromium for Testing from beside it |
| 115 | # when it is installed there. Only those: on the fleet full Chromium |
| 116 | # took the design system's slices from minutes to as long as twenty. |
| 117 | # Discovery only: a readlink without -f leaves the browser to the |
| 118 | # harness's own search rather than failing the role. |
| 119 | case " ${FULL_CHROMIUM:-} " in |
| 120 | *" $rel "*) full=yes ;; |
| 121 | *) full= ;; |
| 122 | esac |
| 123 | if [ "$mode" = browser ] && [ -n "$full" ] && [ -z "${FIESTA_CHROME:-}" ] && |
| 124 | shell=$(command -v chromium 2>/dev/null) && shell=$(readlink -f "$shell" 2>/dev/null); then |
| 125 | case "${shell##*/}" in |
| 126 | headless_shell | chrome-headless-shell) |
| 127 | # The same build number as the shell: one Playwright installs both. |
| 128 | build=${shell#*/chromium_headless_shell-} |
| 129 | for c in "${shell%/chromium_headless_shell-*}/chromium-${build%%/*}"/chrome-linux*/chrome; do |
| 130 | if [ -x "$c" ]; then |
| 131 | FIESTA_CHROME=$c |
| 132 | export FIESTA_CHROME |
| 133 | fi |
| 134 | done |
| 135 | ;; |
| 136 | esac |
| 137 | fi |
| 138 | |
| 139 | # Full Chromium's crash handler wants a database under its config |
| 140 | # directory, ~/.config by default, and will not start without one |
| 141 | # ("chrome_crashpad_handler: --database is required"). The cloud |
| 142 | # runner's HOME is read-only, so there it gets a scratch one. |
| 143 | # Which browser a role drove is the first question a red browser role |
| 144 | # raises, and nothing else in the log answers it. |
| 145 | if [ "$mode" = browser ]; then |
| 146 | echo "gotest: on $(uname -n), driving ${FIESTA_CHROME:-the harness's own choice ($(command -v chromium 2>/dev/null || echo none on PATH))}" |
| 147 | fi |
| 148 | |
| 149 | scratch= |
| 150 | if [ "$mode" = browser ] && [ -z "${CHROME_CONFIG_HOME:-}" ] && ! [ -w "${HOME:-/}" ]; then |
| 151 | scratch=$(mktemp -d) |
| 152 | CHROME_CONFIG_HOME=$scratch |
| 153 | export CHROME_CONFIG_HOME |
| 154 | fi |
| 155 | |
| 156 | # run is exec, unless there is scratch to remove after go test. |
| 157 | run() { |
| 158 | if [ -z "$scratch" ]; then |
| 159 | exec "$@" |
| 160 | fi |
| 161 | status=0 |
| 162 | "$@" || status=$? |
| 163 | rm -rf "$scratch" |
| 164 | exit "$status" |
| 165 | } |
| 166 | |
| 167 | # list <tags> prints the package's tests, examples and fuzz targets, |
| 168 | # one per line. Its own command, not the head of a pipeline: a package |
| 169 | # that fails to build must fail here, not list nothing through a grep |
| 170 | # that succeeds. |
| 171 | list() { |
| 172 | # shellcheck disable=SC2086 # an empty tag list is no argument at all |
| 173 | listing=$(go test $1 -list '.*' "$pkg") || { |
| 174 | echo "gotest: could not list the tests in $pkg:" >&2 |
| 175 | printf '%s\n' "$listing" >&2 |
| 176 | return 1 |
| 177 | } |
| 178 | # Examples and fuzz seeds answer to -run as well, so they are dealt too. |
| 179 | printf '%s\n' "$listing" | grep -E '^(Test|Example|Fuzz)' || true |
| 180 | } |
| 181 | |
| 182 | if [ "$mode" = test ] && [ -z "$slice" ]; then |
| 183 | run go test -timeout "$timeout" "$pkg" -count=1 "$@" |
| 184 | fi |
| 185 | |
| 186 | all=$(list "$tags") |
| 187 | if [ "$mode" = browser ]; then |
| 188 | # The plain tests, where the package builds without the tag at all: |
| 189 | # a package of nothing but browser files is not in go list ./.... |
| 190 | plain= |
| 191 | if go list ./... | grep -qx "$(go list $tags "$pkg")"; then |
| 192 | plain=$(list "") |
| 193 | fi |
| 194 | if [ -n "$plain" ]; then |
| 195 | all=$(printf '%s\n' "$all" | grep -vxF "$plain" || true) |
| 196 | fi |
| 197 | fi |
| 198 | total=$(printf '%s\n' "$all" | grep -c . || true) |
| 199 | if [ "$total" -eq 0 ]; then |
| 200 | echo "gotest: $pkg listed no $mode tests; refusing to report a role of nothing as a pass" |
| 201 | exit 1 |
| 202 | fi |
| 203 | |
| 204 | if [ -z "$slice" ]; then |
| 205 | echo "gotest: $pkg runs its $total $mode tests" |
| 206 | # The role's -run first, so a -run among the flags overrides it. |
| 207 | # shellcheck disable=SC2086 |
| 208 | run go test $tags -timeout "$timeout" -run "^($(printf '%s\n' "$all" | paste -sd'|' -))\$" "$pkg" -count=1 "$@" |
| 209 | fi |
| 210 | if [ $# -gt 0 ]; then |
| 211 | echo "gotest: flags for go test are for a whole package, not slice $slice of $pkg" |
| 212 | exit 2 |
| 213 | fi |
| 214 | |
| 215 | k=${slice%of*} |
| 216 | n=${slice#*of} |
| 217 | mine=$(printf '%s\n' "$all" | deal "$mode" "$rel" "$n" | awk -v k="$k" '$1 == k { print $2 }') |
| 218 | if [ -z "$mine" ]; then |
| 219 | echo "gotest: slice $k of $n of $pkg was dealt none of its $total tests; lower its slice count" |
| 220 | exit 1 |
| 221 | fi |
| 222 | echo "gotest: slice $k of $n of $pkg runs $(printf '%s\n' "$mine" | wc -l) of $total $mode tests" |
| 223 | # shellcheck disable=SC2086 |
| 224 | run go test $tags -timeout "$timeout" -run "^($(printf '%s\n' "$mine" | paste -sd'|' -))\$" "$pkg" -count=1 |
| 225 | |