Skip to content

Commit 96933a8

Browse files
committed
test(ci): instrument test_git_ungraft case 2 with strace + sha256 (investigation)
Temporary diagnostic branch only — not for merge. Adds strace (Linux) around the --dry-run invocation in case 2 and captures sha256 alongside inode before/after. Goal: identify which process+syscall is flipping .git/shallow's inode on ubuntu-24.04, and whether contents change or only the inode (atomic rename with same bytes). Also sets fail-fast: false on the shell-tests matrix so the flake does not cancel the other OS runs.
1 parent 0eb030b commit 96933a8

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

.github/workflows/tests.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ jobs:
5555
tests/*.sh
5656
shell-tests:
5757
strategy:
58+
fail-fast: false
5859
matrix:
5960
os: [ubuntu-24.04, ubuntu-22.04, macos-15, macos-14]
6061
runs-on: ${{ matrix.os }}
@@ -65,6 +66,9 @@ jobs:
6566
uses: fulcrumgenomics/gha-timer@d0d886fa587c7f4f714e64d84aa6e274e818d48b # v1.1.1
6667
with:
6768
skip-banner: true
69+
- name: Install strace (Linux only, for flake investigation)
70+
if: runner.os == 'Linux'
71+
run: sudo apt-get update && sudo apt-get install -y strace
6872
- name: Run shell unit tests
6973
shell: bash
7074
run: |

tests/test_git_ungraft.sh

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,56 @@ echo "Ungraftable candidate: ${ungraftable}"
156156
# --- case 2: --dry-run prints candidates without rewriting ---
157157
# As in case 1, use inode identity to detect the script's `mv`-based
158158
# rewrite; mtime is unreliable here (see case 1 comment).
159+
#
160+
# INSTRUMENTED (temporary, for flake investigation): also capture sha256
161+
# and, when strace is available, the syscall trace around the dry-run
162+
# invocation. The two-part invariant is:
163+
# (a) the script's own `mv` must not fire on --dry-run (inode identity)
164+
# (b) no process (script or sub-git) must change the bytes of shallow
165+
# (sha256 identity)
166+
# Contents-changing without our `mv` would be a real regression; inode-
167+
# changing without contents-changing would indicate a sub-git internal
168+
# rewrite is crossing our invariant. Strace disambiguates which PID
169+
# and syscall did the work.
159170
inode_before="$(stat -f %i "${shallow_file}" 2>/dev/null || stat -c %i "${shallow_file}")"
171+
sha_before="$(shasum -a 256 "${shallow_file}" | awk '{print $1}')"
160172
contents_before="$(cat "${shallow_file}")"
161-
dry_output="$(bash "${UNGRAFT}" --dry-run)"
173+
echo "[case 2 pre] inode=${inode_before} sha256=${sha_before} git=$(git --version)"
174+
175+
strace_log=""
176+
if command -v strace >/dev/null 2>&1; then
177+
strace_log="${TMP}/case2.strace"
178+
# -f: follow forks into git subprocesses.
179+
# Trace the superset of syscalls any write to .git/shallow could use
180+
# (atomic tempfile-rename, in-place write, unlink+create). Swallow
181+
# strace failures (e.g. ptrace restricted) and fall back to a plain
182+
# run so we still exercise the assertion.
183+
if ! dry_output="$(strace -f -o "${strace_log}" \
184+
-e trace=openat,creat,rename,renameat,renameat2,unlink,unlinkat,write \
185+
bash "${UNGRAFT}" --dry-run 2>&1)"; then
186+
echo "[case 2] strace invocation failed; retrying without it"
187+
strace_log=""
188+
dry_output="$(bash "${UNGRAFT}" --dry-run)"
189+
fi
190+
else
191+
echo "[case 2] strace not installed; running without syscall trace"
192+
dry_output="$(bash "${UNGRAFT}" --dry-run)"
193+
fi
194+
162195
inode_after="$(stat -f %i "${shallow_file}" 2>/dev/null || stat -c %i "${shallow_file}")"
196+
sha_after="$(shasum -a 256 "${shallow_file}" | awk '{print $1}')"
197+
echo "[case 2 post] inode=${inode_after} sha256=${sha_after}"
198+
199+
# Always dump any syscall on the shallow file so we see the baseline
200+
# even on passing runs. Grep on the basename so relative-path and
201+
# absolute-path writes both surface.
202+
if [ -n "${strace_log}" ] && [ -f "${strace_log}" ]; then
203+
echo "[case 2 trace] strace entries mentioning 'shallow':"
204+
if ! grep -F shallow "${strace_log}" | head -200; then
205+
echo " (no matches)"
206+
fi
207+
fi
208+
163209
if ! grep -qxF "Would ungraft ${ungraftable}" <<< "${dry_output}"; then
164210
echo "FAIL (case 2): expected 'Would ungraft ${ungraftable}' in dry-run output"
165211
echo "--- dry-run output ---"
@@ -169,6 +215,19 @@ if ! grep -qxF "Would ungraft ${ungraftable}" <<< "${dry_output}"; then
169215
fi
170216
if [ "${inode_before}" != "${inode_after}" ]; then
171217
echo "FAIL (case 2): --dry-run replaced .git/shallow (inode changed)"
218+
echo " inode_before=${inode_before} inode_after=${inode_after}"
219+
echo " sha256_before=${sha_before}"
220+
echo " sha256_after =${sha_after}"
221+
if [ "${sha_before}" = "${sha_after}" ]; then
222+
echo " (contents identical — atomic rewrite with same bytes)"
223+
else
224+
echo " (contents DIFFERED — real rewrite, not a same-bytes swap)"
225+
fi
226+
if [ -n "${strace_log}" ] && [ -f "${strace_log}" ]; then
227+
echo "--- strace entries touching 'shallow' (first 500) ---"
228+
grep -F shallow "${strace_log}" | head -500
229+
echo "------------------------------------------------------"
230+
fi
172231
exit 1
173232
fi
174233
if [ "$(cat "${shallow_file}")" != "${contents_before}" ]; then

0 commit comments

Comments
 (0)