Skip to content

Commit af4dea3

Browse files
authored
Make install.sh sourceable; unit-test its helpers (#23)
install.sh's release-resolution had no automated coverage: e2e installs a local .deb via CLAUDE_CODE_DEB, skipping the download/parse path. - Extract release-JSON parsing into a pure asset_url() helper; move the install flow into main() behind a CLAUDE_CODE_INSTALL_LIB guard so the unit test can source the helpers without running it (curl | bash still runs main). - Add test/install_test.sh: hermetic shUnit2 tests for the helpers (no network/apt), exercising the no-match guard under pipefail. - Split test:fast into an aggregator over test:greatest (the C launcher) and test:shunit2; greatest defaults to -v. - Keep the temp-.deb EXIT trap working under set -u: deb stays script-global, since a local would be out of scope when the trap fires. Seam for verifying the download against the release asset digest (#18), which reapplies as a sibling asset_digest() helper plus its cases.
1 parent 528b4ef commit af4dea3

8 files changed

Lines changed: 204 additions & 90 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ jobs:
3535
with:
3636
persist-credentials: false
3737
- uses: gtbuchanan/tooling/.github/actions/mise-setup@main
38-
# -v lists each test by name in the log (default is a compact summary).
39-
- run: mise run test:fast -- -v
38+
- run: mise run test:fast
4039

4140
# Native arm64 runner (free for public repos) → the aarch64 container runs
4241
# without QEMU. Split into two jobs so a compile break and an install/behavior

AGENTS.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ publish the binary; the `.deb` contains no Anthropic bytes.
5656

5757
| Path | Role |
5858
|---|---|
59-
| `install.sh` | Single install path. Downloads the latest release `.deb` (or installs a local one via `CLAUDE_CODE_DEB=<path>`), enables `glibc-repo`, `apt install`s it. |
59+
| `install.sh` | Single install path. Downloads the latest release `.deb` (or installs a local one via `CLAUDE_CODE_DEB=<path>`), enables `glibc-repo`, `apt install`s it. The install flow is in `main` behind a `CLAUDE_CODE_INSTALL_LIB` guard so the unit test can source its pure helpers without running it (and `curl \| bash` still runs `main`). |
6060
| `package/control` | Metadata. `Depends: bash, curl, jq, python3, glibc-runner, patchelf-glibc`. |
6161
| `package/postinst` | Settings merge (skip via `CLAUDE_CODE_SKIP_SETTINGS`) + native-path symlink (via `link-native.sh`) + fetch the binary. |
6262
| `package/postrm` | Removes the fetched binary and the native-path symlink on uninstall. |
@@ -68,9 +68,10 @@ publish the binary; the `.deb` contains no Anthropic bytes.
6868
| `src/claude-wrapper.c` | The C launcher (`-DBINARY=`/`-DTMPDIR_PATH=`/`-DUNAME_SHIM=` baked in at compile). `claude_wrapper_run()` takes its exec function as a parameter — a seam the unit tests fake. |
6969
| `src/uname-shim.c` | Freestanding `LD_PRELOAD` `uname()` interposer reporting kernel `5.10.0`, so Bun avoids the `epoll_pwait2` startup segfault (bun#32489). Built by `build-wrapper.sh`, shipped to `lib/claude-code-termux/uname-spoof.so`, preloaded by the launcher. |
7070
| `test/wrapper_test.c` | Unit tests for the launcher (greatest; recording exec stub). |
71+
| `test/install_test.sh` | Hermetic unit tests for `install.sh`'s pure helpers (shUnit2; no network/apt). |
7172
| `test/compat.h` | Test-only `setenv`/`unsetenv` shims for Windows libc; force-included into the test build, never shipped. |
7273
| `vendor/greatest.h` | Vendored single-header test framework (greatest 1.5.0, ISC; from silentbicycle/greatest). |
73-
| `vendor/shunit2` | Vendored single-file shell test framework (shUnit2 2.1.9pre, Apache-2.0; pinned to kward/shunit2 master @ `f39734a` — no tagged release since 2.1.8, and master carries the `egrep``grep -E` fix we'd otherwise patch). Drives `scripts/e2e.sh`. Carries one local patch (grep `PATCHED FOR TERMUX`; re-apply on bump): `#! /bin/sh` stub scripts → resolve `sh` via PATH (submitted upstream as kward/shunit2#189). |
74+
| `vendor/shunit2` | Vendored single-file shell test framework (shUnit2 2.1.9pre, Apache-2.0; pinned to kward/shunit2 master @ `f39734a` — no tagged release since 2.1.8, and master carries the `egrep``grep -E` fix we'd otherwise patch). Drives `scripts/e2e.sh` and `test/install_test.sh`. Carries one local patch (grep `PATCHED FOR TERMUX`; re-apply on bump): `#! /bin/sh` stub scripts → resolve `sh` via PATH (submitted upstream as kward/shunit2#189). |
7475
| `scripts/build-wrapper.sh` | Compile the wrapper + uname shim with Termux's clang. |
7576
| `scripts/build-deb.sh` | Stage + `dpkg-deb --build``artifacts/packages/`. |
7677
| `scripts/compile.sh` | Compile the wrapper + assemble the `.deb`, in a Termux env. |
@@ -94,11 +95,13 @@ hosts and the Linux CI runner.
9495
- `mise run hk:all` — run the hk hooks over all files (autofixes locally,
9596
checks in CI). `mise run hk:base [ref]` runs them over files changed from a
9697
base ref (default `origin/main`). Append `-- -S <step>` to target one step.
97-
- `mise run test:fast` — fast, host-native unit tests for the C launcher:
98-
compiles `src/claude-wrapper.c` with mise's `zig` and runs it directly (no
99-
Termux/Docker), in milliseconds. Append `-- -v` for per-test greatest output.
98+
- `mise run test:fast` — the fast, host-native unit suites (no Termux/Docker,
99+
milliseconds); an aggregator over two focused tasks you can also run alone:
100+
- `mise run test:greatest` — the C launcher: compiles `src/claude-wrapper.c`
101+
with mise's `zig` and runs it directly (defaults to `-v`, per-test names).
102+
- `mise run test:shunit2``install.sh`'s pure helpers, via shUnit2.
100103
- `mise run check` — the fast local gate: the hk hooks (`hk:all`) plus
101-
the launcher unit tests (`test:fast`). No Docker.
104+
the unit suites (`test:fast`). No Docker.
102105
- `mise run compile [version]` — compile the launcher + assemble the `.deb`
103106
(→ `artifacts/packages/`), env-aware: `scripts/compile.sh` natively on a Termux
104107
device, else `scripts/docker-run.sh` (the container) on a dev host.
@@ -133,10 +136,11 @@ The version is passed in by the caller; `build-deb.sh`, `compile.sh`, and
133136

134137
## Testing locally
135138

136-
Two layers. The launcher's logic (env shaping + the exec handoff) has fast
137-
**unit tests** that run anywhere via `mise run test:fast` — no Termux, no
138-
Docker (see Dev environment). The end-to-end behaviors (real binary, dispatch,
139-
install) need the container, split across two tasks:
139+
Two layers. Fast **unit tests** for the launcher's logic (env shaping + the exec
140+
handoff) and for `install.sh`'s pure helpers run anywhere via `mise run
141+
test:fast` — no Termux, no Docker (see Dev environment). The end-to-end
142+
behaviors (real binary, dispatch, install) need the container, split across two
143+
tasks:
140144

141145
`mise run compile` produces the `.deb`, then `mise run test:e2e` installs it via
142146
`install.sh` and asserts the real fixes: `argv[0]=rg`/`bfs` dispatch to the
@@ -171,8 +175,9 @@ vars **inline** in the `bash -c` string instead.
171175
the shared `tooling/.github/workflows/pre-commit.yml` reusable (PR-only — the
172176
reusable diffs against the PR base, so it's skipped on push/schedule
173177
`workflow_call` invocations); a `unit` job that runs `mise run test:fast`
174-
(the launcher unit tests build + run natively via `zig` — no container, fast,
175-
arch-independent); plus the `compile` and `e2e` jobs. `compile` runs `mise run
178+
(the launcher tests build + run natively via `zig`, the `install.sh` tests via
179+
shUnit2 — no container, fast, arch-independent); plus the `compile` and `e2e`
180+
jobs. `compile` runs `mise run
176181
compile` and uploads the `.deb` as an artifact; `e2e` (`needs: compile`)
177182
downloads it and runs `mise run test:e2e` — so a compile break and an
178183
install/behavior break surface as distinct job failures. Both go through the

CONTRIBUTING.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ the generated `hk:*` tasks in `mise.tasks.toml`:
3838
- `mise run hk:all` — run the hk hooks over all files (autofixes locally, checks
3939
in CI). `mise run hk:base [ref]` runs them over files changed from a base ref
4040
(default `origin/main`). Append `-- -S <step>` to target one step.
41-
- `mise run check` — the fast local gate: the hk hooks plus the C launcher
42-
unit tests (`test:fast`). No Docker.
41+
- `mise run check` — the fast local gate: the hk hooks plus the host-native
42+
unit suites (`test:fast` → the C launcher via greatest + `install.sh`'s
43+
helpers via shUnit2). No Docker.
4344
- `mise run compile [version]` — compile the launcher and assemble the `.deb`
4445
(→ `artifacts/packages/`). `version` is optional; empty derives the local
4546
CalVer (`scripts/version.sh`, run number `0`).

install.sh

Lines changed: 54 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
# Anthropic's Commercial Terms of Service:
2020
# https://www.anthropic.com/legal/commercial-terms
2121
#
22-
set -euo pipefail
23-
2422
REPO="gtbuchanan/claude-code-termux"
2523
API="https://api.github.com/repos/$REPO/releases/latest"
2624

@@ -30,39 +28,62 @@ die() {
3028
exit 1
3129
}
3230

33-
command -v curl >/dev/null 2>&1 || die "missing 'curl' — pkg install curl"
34-
command -v apt-get >/dev/null 2>&1 || die "apt-get not found — this installer is for Termux."
35-
36-
deb="${CLAUDE_CODE_DEB:-}"
37-
if [ -n "$deb" ]; then
38-
[ -f "$deb" ] || die "CLAUDE_CODE_DEB not found: $deb"
39-
log "Installing local package $deb"
40-
else
41-
log "Resolving the latest release of $REPO"
42-
url=$(curl -fsSL "$API" |
43-
grep -oE '"browser_download_url"[[:space:]]*:[[:space:]]*"[^"]*_aarch64\.deb"' |
31+
# Extract the aarch64 .deb's browser_download_url from GitHub release JSON ($1).
32+
# grep/sed, not jq: jq is only pulled in later as a package dependency. `|| true`
33+
# keeps a no-match grep from tripping main's pipefail; main handles the empty.
34+
asset_url() {
35+
printf '%s' "$1" |
36+
{ grep -oE '"browser_download_url"[[:space:]]*:[[:space:]]*"[^"]*_aarch64\.deb"' || true; } |
4437
head -1 |
45-
sed -E 's/.*"(https[^"]+)"$/\1/')
46-
[ -n "$url" ] || die "no aarch64 .deb asset found in the latest release."
38+
sed -E 's/.*"(https[^"]+)"$/\1/'
39+
}
40+
41+
main() {
42+
set -euo pipefail
4743

48-
deb=$(mktemp --suffix=.deb)
49-
trap 'rm -f "$deb"' EXIT
50-
log "Downloading $url"
51-
curl -fsSL "$url" -o "$deb"
52-
fi
44+
command -v curl >/dev/null 2>&1 || die "missing 'curl' — pkg install curl"
45+
command -v apt-get >/dev/null 2>&1 || die "apt-get not found — this installer is for Termux."
5346

54-
# glibc-runner and patchelf-glibc live in the glibc-packages repo, which the
55-
# `glibc-repo` package enables. It must be added (and the index refreshed)
56-
# BEFORE installing the package — apt resolves the whole dependency graph up
57-
# front, so a repo added mid-transaction wouldn't be consulted.
58-
log "Enabling the glibc package repo…"
59-
apt-get update -y || true
60-
apt-get install -y glibc-repo
61-
apt-get update -y
47+
# Not `local`: the EXIT trap below fires after main() returns and reads $deb,
48+
# which a local would put out of scope (unbound under set -u).
49+
deb="${CLAUDE_CODE_DEB:-}"
50+
if [ -n "$deb" ]; then
51+
[ -f "$deb" ] || die "CLAUDE_CODE_DEB not found: $deb"
52+
log "Installing local package $deb"
53+
else
54+
log "Resolving the latest release of $REPO"
55+
local api url
56+
api=$(curl -fsSL "$API") ||
57+
die "could not reach the GitHub API (network down or rate limited)."
58+
url=$(asset_url "$api")
59+
[ -n "$url" ] || die "no aarch64 .deb asset found in the latest release."
6260

63-
# apt reads the package's Depends and pulls glibc-runner, patchelf-glibc, jq,
64-
# and python3 automatically (dpkg -i would not).
65-
log "Installing (apt resolves dependencies)…"
66-
apt-get install -y "$deb"
61+
deb=$(mktemp --suffix=.deb)
62+
trap 'rm -f -- "$deb"' EXIT
63+
log "Downloading $url"
64+
curl -fsSL "$url" -o "$deb"
65+
fi
66+
67+
# glibc-runner and patchelf-glibc live in the glibc-packages repo, which the
68+
# `glibc-repo` package enables. It must be added (and the index refreshed)
69+
# BEFORE installing the package — apt resolves the whole dependency graph up
70+
# front, so a repo added mid-transaction wouldn't be consulted.
71+
log "Enabling the glibc package repo…"
72+
apt-get update -y || true
73+
apt-get install -y glibc-repo
74+
apt-get update -y
75+
76+
# apt reads the package's Depends and pulls glibc-runner, patchelf-glibc, jq,
77+
# and python3 automatically (dpkg -i would not).
78+
log "Installing (apt resolves dependencies)…"
79+
apt-get install -y "$deb"
80+
81+
log "Done. Run 'claude' to start."
82+
}
6783

68-
log "Done. Run 'claude' to start."
84+
# Run the installer, unless this file is being sourced by the unit test for its
85+
# helper functions (which sets CLAUDE_CODE_INSTALL_LIB). A piped `curl … | bash`
86+
# leaves the var unset — and since BASH_SOURCE is empty in that case the usual
87+
# "${BASH_SOURCE[0]}" = "$0" guard would wrongly skip the install — so the
88+
# normal one-liner still runs main.
89+
[ -n "${CLAUDE_CODE_INSTALL_LIB:-}" ] || main "$@"

mise-tasks/test/fast

Lines changed: 2 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,3 @@
11
#!/usr/bin/env bash
2-
#MISE description="Unit-test the C launcher natively (fast; no Docker)"
3-
# Keep Git Bash / MSYS from rewriting the leading-slash sentinel paths
4-
# (BINARY/TMPDIR_PATH/UNAME_SHIM below) into Windows paths on the way to zig
5-
# (no-op elsewhere).
6-
#MISE env={ MSYS_NO_PATHCONV = "1" }
7-
#
8-
# Compiles src/claude-wrapper.c with zig and runs it directly — no Termux, no
9-
# Docker, milliseconds. The wrapper is pure POSIX env-shaping plus an injected
10-
# exec handoff (the seam in claude-wrapper.c), so a native build exercises the
11-
# real logic.
12-
#
13-
# mise run test:fast [-- greatest args, e.g. -v]
14-
#
15-
set -euo pipefail
16-
17-
# Sentinel literals baked into the build so the test can assert the exact
18-
# path/tmpdir the wrapper bakes in. They need not exist on disk: exec is faked.
19-
BINARY=/claude-code-termux-test/claude
20-
TMPDIR_PATH=/claude-code-termux-test/tmp
21-
UNAME_SHIM=/claude-code-termux-test/uname-spoof.so
22-
23-
out="$MISE_PROJECT_ROOT/artifacts/build"
24-
mkdir -p "$out"
25-
26-
bin="$out/wrapper_test"
27-
case "${OSTYPE:-}" in msys* | cygwin* | win*) bin="$bin.exe" ;; esac
28-
29-
# Flags mirror build-wrapper.sh (notably no -std: the default gnu mode exposes
30-
# POSIX setenv/unsetenv on Linux/macOS, where compat.h is a no-op).
31-
# -DCLAUDE_WRAPPER_NO_MAIN drops the wrapper's main() (greatest supplies one);
32-
# -include compat.h shims setenv/unsetenv on Windows.
33-
zig cc -O2 -Wall -Wextra -Werror \
34-
-DBINARY="\"$BINARY\"" -DTMPDIR_PATH="\"$TMPDIR_PATH\"" \
35-
-DUNAME_SHIM="\"$UNAME_SHIM\"" \
36-
-DCLAUDE_WRAPPER_NO_MAIN \
37-
-include "$MISE_PROJECT_ROOT/test/compat.h" \
38-
-I "$MISE_PROJECT_ROOT/vendor" \
39-
-o "$bin" \
40-
"$MISE_PROJECT_ROOT/src/claude-wrapper.c" "$MISE_PROJECT_ROOT/test/wrapper_test.c"
41-
42-
"$bin" "$@"
2+
#MISE description="Run the fast host-native unit suites (greatest + shUnit2; no Docker)"
3+
#MISE depends=["test:greatest", "test:shunit2"]

mise-tasks/test/greatest

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env bash
2+
#MISE description="Unit-test the C launcher with greatest natively (fast; no Docker)"
3+
# Keep Git Bash / MSYS from rewriting the leading-slash sentinel paths
4+
# (BINARY/TMPDIR_PATH/UNAME_SHIM below) into Windows paths on the way to zig
5+
# (no-op elsewhere).
6+
#MISE env={ MSYS_NO_PATHCONV = "1" }
7+
#
8+
# Compiles src/claude-wrapper.c with zig and runs it directly — no Termux, no
9+
# Docker, milliseconds. The wrapper is pure POSIX env-shaping plus an injected
10+
# exec handoff (the seam in claude-wrapper.c), so a native build exercises the
11+
# real logic.
12+
#
13+
# mise run test:greatest [-- greatest args] # defaults to -v (per-test names)
14+
#
15+
set -euo pipefail
16+
17+
# Sentinel literals baked into the build so the test can assert the exact
18+
# path/tmpdir the wrapper bakes in. They need not exist on disk: exec is faked.
19+
BINARY=/claude-code-termux-test/claude
20+
TMPDIR_PATH=/claude-code-termux-test/tmp
21+
UNAME_SHIM=/claude-code-termux-test/uname-spoof.so
22+
23+
out="$MISE_PROJECT_ROOT/artifacts/build"
24+
mkdir -p "$out"
25+
26+
bin="$out/wrapper_test"
27+
case "${OSTYPE:-}" in msys* | cygwin* | win*) bin="$bin.exe" ;; esac
28+
29+
# Flags mirror build-wrapper.sh (notably no -std: the default gnu mode exposes
30+
# POSIX setenv/unsetenv on Linux/macOS, where compat.h is a no-op).
31+
# -DCLAUDE_WRAPPER_NO_MAIN drops the wrapper's main() (greatest supplies one);
32+
# -include compat.h shims setenv/unsetenv on Windows.
33+
zig cc -O2 -Wall -Wextra -Werror \
34+
-DBINARY="\"$BINARY\"" -DTMPDIR_PATH="\"$TMPDIR_PATH\"" \
35+
-DUNAME_SHIM="\"$UNAME_SHIM\"" \
36+
-DCLAUDE_WRAPPER_NO_MAIN \
37+
-include "$MISE_PROJECT_ROOT/test/compat.h" \
38+
-I "$MISE_PROJECT_ROOT/vendor" \
39+
-o "$bin" \
40+
"$MISE_PROJECT_ROOT/src/claude-wrapper.c" "$MISE_PROJECT_ROOT/test/wrapper_test.c"
41+
42+
# Default to -v (greatest lists each test by name; its bare default is a compact
43+
# dot summary). Explicit args override — e.g. a `-t <name>` filter.
44+
"$bin" "${@:--v}"

mise-tasks/test/shunit2

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env bash
2+
#MISE description="Unit-test install.sh's pure helpers with shUnit2 (fast; no Docker)"
3+
#
4+
# Sources install.sh under its lib guard and drives its pure helpers via
5+
# shUnit2 — no network, no apt, no Termux. See test/install_test.sh.
6+
#
7+
# mise run test:shunit2 [-- shUnit2 args, e.g. a test_* name filter]
8+
set -euo pipefail
9+
10+
exec bash "$MISE_PROJECT_ROOT/test/install_test.sh" "$@"

test/install_test.sh

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Hermetic unit tests for install.sh's pure helpers. Sources install.sh with
4+
# CLAUDE_CODE_INSTALL_LIB set so its main() does NOT run (no network, no apt),
5+
# then drives the helpers directly. Runs anywhere via `mise run test:shunit2`
6+
# — no Termux, no Docker, milliseconds.
7+
#
8+
# Built on the vendored shUnit2 (vendor/shunit2): each behavior is a `test_*`
9+
# function; shUnit2 discovers them and prints the run summary. Like e2e.sh, this
10+
# deliberately does NOT `set -e` (errexit fights the framework) — and sourcing
11+
# install.sh under the lib guard skips its `set -euo pipefail` (that lives in
12+
# main()), so the test shell stays errexit-free.
13+
14+
# Resolve install.sh and vendor/shunit2 against the repo root regardless of cwd.
15+
cd "$(dirname "$0")/.." || exit 1
16+
17+
# Source the installer for its functions only; the guard keeps main() — and its
18+
# strict mode — from running.
19+
# shellcheck source=/dev/null
20+
CLAUDE_CODE_INSTALL_LIB=1 . ./install.sh
21+
22+
# A release-JSON shape mirroring GitHub's, with a decoy asset before the aarch64
23+
# .deb so the selector has to discriminate rather than grab the first URL.
24+
release_json() {
25+
cat <<'EOF'
26+
{
27+
"assets": [
28+
{
29+
"name": "SHA256SUMS",
30+
"browser_download_url": "https://github.com/gtbuchanan/claude-code-termux/releases/download/v2026.6.19/SHA256SUMS"
31+
},
32+
{
33+
"name": "claude-code-termux_2026.6.19_aarch64.deb",
34+
"browser_download_url": "https://github.com/gtbuchanan/claude-code-termux/releases/download/v2026.6.19/claude-code-termux_2026.6.19_aarch64.deb"
35+
}
36+
]
37+
}
38+
EOF
39+
}
40+
41+
test_asset_url_extracts_aarch64_deb() {
42+
assertEquals 'picks the aarch64 .deb download URL over a decoy asset' \
43+
'https://github.com/gtbuchanan/claude-code-termux/releases/download/v2026.6.19/claude-code-termux_2026.6.19_aarch64.deb' \
44+
"$(asset_url "$(release_json)")"
45+
}
46+
47+
# Run asset_url under pipefail — as main() does — and assert it yields no output
48+
# AND still exits 0. This is what proves the `|| true` guard: without it, the
49+
# no-match grep's non-zero status would trip pipefail and abort the install.
50+
assert_no_match_under_pipefail() { # $1 = message, $2 = release JSON
51+
local out rc
52+
out="$(
53+
set -o pipefail
54+
asset_url "$2"
55+
)"
56+
rc=$?
57+
assertEquals "$1" '' "$out"
58+
assertEquals "$1 (exits 0 under pipefail)" 0 "$rc"
59+
}
60+
61+
test_asset_url_empty_when_no_aarch64_deb() {
62+
assert_no_match_under_pipefail 'empty when the release has no aarch64 .deb asset' \
63+
'{"assets":[{"browser_download_url":"https://example.com/x.txt"}]}'
64+
}
65+
66+
test_asset_url_empty_on_non_json() {
67+
assert_no_match_under_pipefail 'empty on a non-JSON blob' 'this is not json'
68+
}
69+
70+
# shUnit2 hands control here: it discovers the test_* functions above and prints
71+
# the summary.
72+
# shellcheck source=/dev/null
73+
. ./vendor/shunit2

0 commit comments

Comments
 (0)