From 6f5b84d1415edc062a8fd84699364814e9d7c4d7 Mon Sep 17 00:00:00 2001 From: Mura Li <2606021+typeless@users.noreply.github.com> Date: Fri, 15 May 2026 14:01:23 +0800 Subject: [PATCH] Add [\!shouldfail] reproducer for transitive implicit-dep tracking bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Minimal fixture demonstrating that pup fails to record a newly-transitive header when an existing tracked header is edited to include it. After the edit, the source's recompile produces a .d file listing the new header, but pup's implicit-dep set is not extended — so subsequent edits to the new header are invisible to change detection and pup reports "Nothing to do" while the .o is stale. Reproducer (test.sh): 1. source.c includes only old.h. Build, record old.h as implicit dep. 2. Edit old.h to also #include newhdr.h. Rebuild. .o picks up new value via newhdr.h. 3. Edit newhdr.h. Rebuild expected; pup reports "Nothing to do". .o stale. Tagged [\!shouldfail] so default CI stays green while the bug is on file. Once the underlying dep-recording bug is fixed, removing the tag converts this into a regression guard. The bug reproduces unscoped (no scope filter) and is distinct from the change-detection scope-filter bug fixed in 315b2a2 — that one was about an existing recorded header being filtered out; this is about a header never being recorded in the first place. Refs pup-header-detection-bug.md (working notes). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../header_dep_transitive/Tupfile.ini | 0 .../header_dep_transitive/Tuprules.tup | 2 + .../header_dep_transitive/include/newhdr.h | 2 + .../header_dep_transitive/include/old.h | 2 + .../header_dep_transitive/src/Tupfile | 2 + .../fixtures/header_dep_transitive/src/main.c | 3 + .../fixtures/header_dep_transitive/test.sh | 122 ++++++++++++++++++ test/unit/test_e2e.cpp | 23 ++++ 8 files changed, 156 insertions(+) create mode 100644 test/e2e/fixtures/header_dep_transitive/Tupfile.ini create mode 100644 test/e2e/fixtures/header_dep_transitive/Tuprules.tup create mode 100644 test/e2e/fixtures/header_dep_transitive/include/newhdr.h create mode 100644 test/e2e/fixtures/header_dep_transitive/include/old.h create mode 100644 test/e2e/fixtures/header_dep_transitive/src/Tupfile create mode 100644 test/e2e/fixtures/header_dep_transitive/src/main.c create mode 100755 test/e2e/fixtures/header_dep_transitive/test.sh diff --git a/test/e2e/fixtures/header_dep_transitive/Tupfile.ini b/test/e2e/fixtures/header_dep_transitive/Tupfile.ini new file mode 100644 index 00000000..e69de29b diff --git a/test/e2e/fixtures/header_dep_transitive/Tuprules.tup b/test/e2e/fixtures/header_dep_transitive/Tuprules.tup new file mode 100644 index 00000000..d5cc09cf --- /dev/null +++ b/test/e2e/fixtures/header_dep_transitive/Tuprules.tup @@ -0,0 +1,2 @@ +S = $(TUP_CWD) +CFLAGS = -I$(S)/include diff --git a/test/e2e/fixtures/header_dep_transitive/include/newhdr.h b/test/e2e/fixtures/header_dep_transitive/include/newhdr.h new file mode 100644 index 00000000..b09c4287 --- /dev/null +++ b/test/e2e/fixtures/header_dep_transitive/include/newhdr.h @@ -0,0 +1,2 @@ +#pragma once +#define EXTRA 100 diff --git a/test/e2e/fixtures/header_dep_transitive/include/old.h b/test/e2e/fixtures/header_dep_transitive/include/old.h new file mode 100644 index 00000000..b9834c05 --- /dev/null +++ b/test/e2e/fixtures/header_dep_transitive/include/old.h @@ -0,0 +1,2 @@ +#pragma once +#define ANSWER 1 diff --git a/test/e2e/fixtures/header_dep_transitive/src/Tupfile b/test/e2e/fixtures/header_dep_transitive/src/Tupfile new file mode 100644 index 00000000..8ea3f580 --- /dev/null +++ b/test/e2e/fixtures/header_dep_transitive/src/Tupfile @@ -0,0 +1,2 @@ +include_rules +: main.c |> gcc $(CFLAGS) -c %f -o %o |> %B.o diff --git a/test/e2e/fixtures/header_dep_transitive/src/main.c b/test/e2e/fixtures/header_dep_transitive/src/main.c new file mode 100644 index 00000000..866b7bc3 --- /dev/null +++ b/test/e2e/fixtures/header_dep_transitive/src/main.c @@ -0,0 +1,3 @@ +#include "old.h" +int get_answer(void) { return ANSWER; } +int main(void) { return get_answer(); } diff --git a/test/e2e/fixtures/header_dep_transitive/test.sh b/test/e2e/fixtures/header_dep_transitive/test.sh new file mode 100755 index 00000000..3dd2ecec --- /dev/null +++ b/test/e2e/fixtures/header_dep_transitive/test.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# Reproducer for pup's transitive implicit-dep tracking bug. +# +# Scenario: +# 1. Source includes only old.h. pup builds, records old.h as implicit dep. +# 2. old.h is modified to transitively include a new header (newhdr.h). +# pup detects old.h changed and recompiles, but FAILS to add newhdr.h +# to the source's implicit-dep set. +# 3. newhdr.h is modified. pup reports "Nothing to do." because its +# stale dep set says the source only depends on old.h (unchanged). +# +# Expected: step 3 rebuilds, .o reflects the new constant. +# Observed: step 3 is a no-op; .o is stale. +# +# Test exits 0 on success (bug fixed), non-zero on failure (bug present). + +set -u + +PUP="${PUP:-$(command -v pup || echo /home/mural/bin/pup)}" +BUILD_DIR="build/x86" +OBJ="${BUILD_DIR}/src/main.o" + +die() { + echo "FAIL: $*" >&2 + exit 1 +} + +# Extract the immediate-mode operand of the first mov $0xXX instruction. +# Returns the hex literal as it appears in objdump output (e.g. "0x1"). +constant_in_obj() { + objdump -d "$1" 2>/dev/null \ + | grep -oE 'mov +\$0x[0-9a-f]+' \ + | head -1 \ + | grep -oE '0x[0-9a-f]+' +} + +# Decode a hex literal (with 0x prefix) to a decimal integer for printing. +hex_to_dec() { + printf '%d' "$1" 2>/dev/null +} + +# ---------- Step 1: initial build ---------- +echo "=== Step 1: initial build (source includes old.h only) ===" +"$PUP" configure -B "$BUILD_DIR" >/dev/null || die "configure failed" +"$PUP" -B "$BUILD_DIR" >/dev/null || die "initial build failed" +[[ -f "$OBJ" ]] || die "expected $OBJ after initial build" + +K1="$(constant_in_obj "$OBJ")" +echo " observed constant in $OBJ: $K1 ($(hex_to_dec "$K1"))" +[[ "$K1" == "0x1" ]] || die "expected 0x1 (ANSWER=1) after step 1, got $K1" + +# ---------- Step 2: make old.h transitively include newhdr.h ---------- +echo +echo "=== Step 2: modify old.h to #include newhdr.h ===" +cat > include/old.h << 'EOF' +#pragma once +#include "newhdr.h" +#define ANSWER EXTRA +EOF + +"$PUP" -B "$BUILD_DIR" >/dev/null || die "rebuild after old.h edit failed" + +K2="$(constant_in_obj "$OBJ")" +echo " observed constant in $OBJ: $K2 ($(hex_to_dec "$K2"))" +[[ "$K2" == "0x64" ]] \ + || die "expected 0x64 (EXTRA=100) after step 2, got $K2 — old.h change not picked up" + +# ---------- Forensic between step 2 and 3: did pup record newhdr.h? ---------- +# After step 2 the .d file from gcc lists both old.h AND newhdr.h. If pup's +# dep-recording is healthy, both should appear as implicit edges for main.o's +# compile. If newhdr.h is missing here, the bug is in dep recording (post- +# execution .d-file processing), not in change detection. +echo +echo "=== Forensic: pup show index for main.c's compile after step 2 ===" +INDEX_OUT="$("$PUP" show index -B "$BUILD_DIR" main.c 2>&1)" +echo "$INDEX_OUT" | sed 's/^/ /' + +if echo "$INDEX_OUT" | grep -q "implicit:.*newhdr.h"; then + echo " forensic: newhdr.h IS recorded as an implicit dep — proceed" +else + echo + echo " forensic: newhdr.h is NOT in main.c's implicit-dep set." + echo " Root cause: pup's post-execution .d-file processing did" + echo " not extend the implicit-dep set with newly-discovered" + echo " transitive headers." +fi + +# ---------- Step 3: modify newhdr.h (the transitive header) ---------- +echo +echo "=== Step 3: modify newhdr.h (EXTRA 100 -> 7777) ===" +cat > include/newhdr.h << 'EOF' +#pragma once +#define EXTRA 7777 +EOF + +OUT="$("$PUP" -B "$BUILD_DIR" 2>&1)" +EC=$? +echo " pup output: $OUT" +[[ $EC -eq 0 ]] || die "rebuild returned non-zero: $EC" + +K3="$(constant_in_obj "$OBJ")" +echo " observed constant in $OBJ: $K3 ($(hex_to_dec "$K3"))" + +if [[ "$OUT" == *"Nothing to do"* ]]; then + echo + echo "BUG PRESENT: pup reported 'Nothing to do' after newhdr.h change." + echo " newhdr.h is transitively included by old.h, which is in" + echo " the source's implicit-dep set, but newhdr.h itself was" + echo " never recorded — confirmed by 'pup show index' above." +fi + +if [[ "$K3" != "0x1e61" ]]; then + echo + echo "Diagnostic:" + echo " expected $OBJ to contain 0x1e61 (EXTRA=7777)" + echo " actually contains $K3 ($(hex_to_dec "$K3"))" + echo " this means main.c was NOT recompiled after newhdr.h changed" + die "transitive header change was not detected" +fi + +echo +echo "PASS: pup detected transitive header change." diff --git a/test/unit/test_e2e.cpp b/test/unit/test_e2e.cpp index 9ebf1184..f800bd82 100644 --- a/test/unit/test_e2e.cpp +++ b/test/unit/test_e2e.cpp @@ -2105,6 +2105,29 @@ SCENARIO("Pupignore test via shell fixture", "[e2e][shell]") } } +// Reproducer: when a source's already-tracked header is edited to transitively +// include a new header, pup recompiles the source but fails to record the new +// transitive header. Subsequent edits to that new header are then invisible to +// change detection, and pup reports "Nothing to do" while the .o is stale. +// See test/e2e/fixtures/header_dep_transitive/ and the parent project's +// notes/pup-header-detection-bug.md. +SCENARIO("Transitive implicit-dep header tracking", "[e2e][shell][incremental][!shouldfail]") +{ + WHEN("the header_dep_transitive shell fixture runs") + { + auto result = run_shell_fixture("header_dep_transitive"); + + THEN("a newly-transitive header's change triggers rebuild") + { + INFO("test.sh stdout:\n" << result.stdout_output); + INFO("test.sh stderr:\n" << result.stderr_output); + // CHECK (not REQUIRE) so !shouldfail can mark the case green + // without SIGABRT — the diagnostic output above survives. + CHECK(result.success()); + } + } +} + // ============================================================================= // Show Command Tests // =============================================================================