Skip to content

Commit 50e52e0

Browse files
committed
test: add unit tests for git_ungraft.sh and gha_timer_wrapper.sh
test_git_ungraft.sh builds a hermetic shallow clone from a local bare repo (no network) and asserts: - No-candidate case leaves .git/shallow untouched (mtime preserved). - --dry-run prints "Would ungraft <sha>" without rewriting the file. - Default run rewrites .git/shallow, removing the ungraftable entry. test_gha_timer_wrapper.sh exercises both branches of the wrapper: - When gha-timer is on PATH, argv is forwarded verbatim to a shim binary and no ::group::/::endgroup:: markers are emitted. - When gha-timer is absent, the fallback emits the expected ::group::/::endgroup:: workflow commands and drops `stop`. Both tests are picked up by the existing `tests/test_*.sh` loop in the shell-tests job, so the wrapper fallback is now covered in CI without needing a separate matrix entry.
1 parent 2580866 commit 50e52e0

2 files changed

Lines changed: 223 additions & 0 deletions

File tree

tests/test_gha_timer_wrapper.sh

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Unit test for src/scripts/gha_timer_wrapper.sh.
4+
#
5+
# Covers both branches of the wrapper:
6+
# 1. gha-timer present on PATH: arguments are forwarded verbatim and
7+
# no ::group::/::endgroup:: marker is emitted.
8+
# 2. gha-timer absent from PATH: the fallback emits the expected
9+
# ::group:: / ::endgroup:: workflow commands and is silent on
10+
# `stop` (no analogue in workflow commands).
11+
12+
set -euo pipefail
13+
14+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
15+
WRAPPER="${REPO_ROOT}/src/scripts/gha_timer_wrapper.sh"
16+
17+
TMP="$(mktemp -d)"
18+
trap 'rm -rf "${TMP}"' EXIT
19+
20+
# --- case 1: real binary shimmed onto PATH → pass-through, no fallback output ---
21+
SHIM_DIR="${TMP}/shim"
22+
mkdir -p "${SHIM_DIR}"
23+
cat > "${SHIM_DIR}/gha-timer" <<'EOF'
24+
#!/usr/bin/env bash
25+
# Record argv, one arg per line, for the test to inspect.
26+
printf '%s\n' "$@" > "${GHA_TIMER_SHIM_LOG}"
27+
EOF
28+
chmod +x "${SHIM_DIR}/gha-timer"
29+
30+
export GHA_TIMER_SHIM_LOG="${TMP}/shim.log"
31+
PATH="${SHIM_DIR}:${PATH}"
32+
# shellcheck source=../src/scripts/gha_timer_wrapper.sh
33+
. "${WRAPPER}"
34+
35+
out="$(gha_timer start --name "Hello, 🦝")"
36+
if [ -n "${out}" ]; then
37+
echo "FAIL (case 1): wrapper emitted output when gha-timer is on PATH"
38+
echo " got: ${out}"
39+
exit 1
40+
fi
41+
expected_argv=$'start\n--name\nHello, 🦝'
42+
if [ "$(cat "${GHA_TIMER_SHIM_LOG}")" != "${expected_argv}" ]; then
43+
echo "FAIL (case 1): shim did not receive expected argv"
44+
echo "--- expected ---"; printf '%s\n' "${expected_argv}"
45+
echo "--- actual ---"; cat "${GHA_TIMER_SHIM_LOG}"
46+
echo "----------------"
47+
exit 1
48+
fi
49+
echo "PASS (case 1): wrapper forwards argv verbatim when gha-timer is on PATH"
50+
51+
# --- case 2: gha-timer absent → fallback emits ::group::/::endgroup:: ---
52+
# Run the case in a fresh subshell with a PATH that does not contain
53+
# the shim (and does not contain any system-installed gha-timer). We
54+
# also scrub the parent shell's `gha_timer` function from the child.
55+
fallback_out="$(
56+
PATH="/usr/bin:/bin" env -u gha_timer bash -c '
57+
set -euo pipefail
58+
. "'"${WRAPPER}"'"
59+
gha_timer start --name "Step A"
60+
gha_timer elapsed --outcome success --name "Step A"
61+
gha_timer stop
62+
'
63+
)"
64+
expected_fallback=$'::group::Step A\n::endgroup::'
65+
if [ "${fallback_out}" != "${expected_fallback}" ]; then
66+
echo "FAIL (case 2): fallback output mismatch"
67+
echo "--- expected ---"; printf '%s\n' "${expected_fallback}"
68+
echo "--- actual ---"; printf '%s\n' "${fallback_out}"
69+
echo "----------------"
70+
exit 1
71+
fi
72+
echo "PASS (case 2): fallback emits ::group::/::endgroup:: and drops 'stop'"
73+
74+
echo "PASS: test_gha_timer_wrapper.sh"

tests/test_git_ungraft.sh

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Unit test for src/scripts/git_ungraft.sh.
4+
#
5+
# Builds a self-contained shallow clone from a local bare repo (no
6+
# network) and exercises three behaviors:
7+
# 1. Before parents are fetched: no candidates, shallow file unchanged
8+
# (mtime preserved).
9+
# 2. --dry-run: prints "Would ungraft <sha>" lines without modifying
10+
# .git/shallow.
11+
# 3. Default (non-dry) run: rewrites .git/shallow, removing the
12+
# entries whose parents are now locally present.
13+
14+
set -euo pipefail
15+
16+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
17+
UNGRAFT="${REPO_ROOT}/src/scripts/git_ungraft.sh"
18+
19+
TMP="$(mktemp -d)"
20+
trap 'rm -rf "${TMP}"' EXIT
21+
22+
REMOTE="${TMP}/remote.git"
23+
SRC="${TMP}/src"
24+
CLIENT="${TMP}/client"
25+
26+
# --- build a deterministic 20-commit linear history in a local bare repo ---
27+
# Enough commits that a small `--deepen` leaves the file non-empty so
28+
# cases 2 and 3 have something to assert against.
29+
git init --quiet --bare "${REMOTE}"
30+
git init --quiet -b main "${SRC}"
31+
(
32+
cd "${SRC}"
33+
for i in $(seq 1 20); do
34+
git -c user.email=t@t -c user.name=t commit --quiet --allow-empty -m "c${i}"
35+
done
36+
git push --quiet "${REMOTE}" main
37+
)
38+
39+
# --- shallow clone at depth=1 ---
40+
git clone --quiet --depth=1 "file://${REMOTE}" "${CLIENT}"
41+
cd "${CLIENT}"
42+
43+
shallow_file="${CLIENT}/.git/shallow"
44+
if [ ! -f "${shallow_file}" ]; then
45+
echo "FAIL: expected .git/shallow to exist after --depth=1 clone"
46+
exit 1
47+
fi
48+
shallow_before="$(cat "${shallow_file}")"
49+
50+
# --- case 1: no candidates, no rewrite, mtime preserved ---
51+
# Parents of the shallow-boundary commit are NOT present locally, so
52+
# no entry is ungraftable. The script must not rewrite the file.
53+
mtime_before="$(stat -f %m "${shallow_file}" 2>/dev/null || stat -c %Y "${shallow_file}")"
54+
output="$(bash "${UNGRAFT}")"
55+
mtime_after="$(stat -f %m "${shallow_file}" 2>/dev/null || stat -c %Y "${shallow_file}")"
56+
if [ "${output}" != "No candidate commits to ungraft" ]; then
57+
echo "FAIL (case 1): expected 'No candidate commits to ungraft'"
58+
echo " got: ${output}"
59+
exit 1
60+
fi
61+
if [ "${mtime_before}" != "${mtime_after}" ]; then
62+
echo "FAIL (case 1): .git/shallow mtime changed on no-op run"
63+
exit 1
64+
fi
65+
if [ "$(cat "${shallow_file}")" != "${shallow_before}" ]; then
66+
echo "FAIL (case 1): .git/shallow contents changed on no-op run"
67+
exit 1
68+
fi
69+
echo "PASS (case 1): no candidates, shallow file untouched"
70+
71+
# --- fetch HEAD's parent at depth=1 so HEAD's parents are locally present ---
72+
# This mirrors what src/scripts/git_fetch_parents.sh does in production:
73+
# the parent SHA is both present locally AND added to .git/shallow,
74+
# which makes HEAD (still in .git/shallow) ungraftable.
75+
head_sha="$(git rev-parse HEAD)"
76+
head_parent="$(git cat-file -p "${head_sha}" \
77+
| awk '/^$/ { exit } $1 == "parent" { print $2 }' | head -n1)"
78+
if [ -z "${head_parent}" ]; then
79+
echo "FAIL: could not determine HEAD's parent SHA"
80+
exit 1
81+
fi
82+
git fetch --quiet --update-shallow --depth=1 origin "${head_parent}:__parent__"
83+
if [ ! -f "${shallow_file}" ]; then
84+
echo "FAIL: expected .git/shallow to remain after parent fetch"
85+
exit 1
86+
fi
87+
88+
# Pick a shallow entry whose parents are now all locally present.
89+
ungraftable=""
90+
while IFS= read -r sha || [ -n "${sha}" ]; do
91+
[ -z "${sha}" ] && continue
92+
all_present=1
93+
parents="$(git cat-file -p "${sha}" | awk '/^$/ { exit } $1 == "parent" { print $2 }')"
94+
[ -z "${parents}" ] && continue # root commit — skip
95+
for p in ${parents}; do
96+
git cat-file -e "${p}^{commit}" 2>/dev/null || { all_present=0; break; }
97+
done
98+
if [ "${all_present}" -eq 1 ]; then
99+
ungraftable="${sha}"
100+
break
101+
fi
102+
done < "${shallow_file}"
103+
if [ -z "${ungraftable}" ]; then
104+
echo "FAIL: no shallow entry with locally-present parents after parent fetch"
105+
echo "--- .git/shallow ---"
106+
cat "${shallow_file}"
107+
echo "--------------------"
108+
exit 1
109+
fi
110+
echo "Ungraftable candidate: ${ungraftable}"
111+
112+
# --- case 2: --dry-run prints candidates without rewriting ---
113+
mtime_before="$(stat -f %m "${shallow_file}" 2>/dev/null || stat -c %Y "${shallow_file}")"
114+
contents_before="$(cat "${shallow_file}")"
115+
dry_output="$(bash "${UNGRAFT}" --dry-run)"
116+
mtime_after="$(stat -f %m "${shallow_file}" 2>/dev/null || stat -c %Y "${shallow_file}")"
117+
if ! grep -qxF "Would ungraft ${ungraftable}" <<< "${dry_output}"; then
118+
echo "FAIL (case 2): expected 'Would ungraft ${ungraftable}' in dry-run output"
119+
echo "--- dry-run output ---"
120+
echo "${dry_output}"
121+
echo "----------------------"
122+
exit 1
123+
fi
124+
if [ "${mtime_before}" != "${mtime_after}" ]; then
125+
echo "FAIL (case 2): --dry-run changed .git/shallow mtime"
126+
exit 1
127+
fi
128+
if [ "$(cat "${shallow_file}")" != "${contents_before}" ]; then
129+
echo "FAIL (case 2): --dry-run changed .git/shallow contents"
130+
exit 1
131+
fi
132+
echo "PASS (case 2): --dry-run printed candidate and left shallow file untouched"
133+
134+
# --- case 3: real run removes the ungraftable entry ---
135+
real_output="$(bash "${UNGRAFT}")"
136+
if ! grep -qxF "Ungrafted ${ungraftable}" <<< "${real_output}"; then
137+
echo "FAIL (case 3): expected 'Ungrafted ${ungraftable}' in output"
138+
echo "--- output ---"
139+
echo "${real_output}"
140+
echo "--------------"
141+
exit 1
142+
fi
143+
if grep -qxF "${ungraftable}" "${shallow_file}"; then
144+
echo "FAIL (case 3): ${ungraftable} still present in .git/shallow after ungraft"
145+
exit 1
146+
fi
147+
echo "PASS (case 3): ungraftable entry removed from .git/shallow"
148+
149+
echo "PASS: test_git_ungraft.sh"

0 commit comments

Comments
 (0)