diff --git a/src/graph/dag.cpp b/src/graph/dag.cpp index 4c5e80a..c1a200d 100644 --- a/src/graph/dag.cpp +++ b/src/graph/dag.cpp @@ -1161,6 +1161,18 @@ auto collect_affected_commands(Graph const& graph, Vec const& changed_ } } + // InjectImplicitDeps siblings (dep-scan commands) have no graph outputs, + // so the cascade above can't reach them. They must run whenever their + // parent compile runs — otherwise newly-introduced transitive includes + // are never re-discovered. Walk all commands and attach any whose + // parent_command was just marked affected. + for (auto cmd_id : nodes_of_type(graph, NodeType::Command)) { + auto parent = get_parent_command(graph, cmd_id); + if (parent != INVALID_NODE_ID && affected.contains(parent)) { + affected.set(cmd_id, 1); + } + } + return affected; } diff --git a/test/e2e/fixtures/header_dep_transitive/src/Tupfile b/test/e2e/fixtures/header_dep_transitive/src/Tupfile index 8ea3f58..cd1d5b8 100644 --- a/test/e2e/fixtures/header_dep_transitive/src/Tupfile +++ b/test/e2e/fixtures/header_dep_transitive/src/Tupfile @@ -1,2 +1,3 @@ include_rules : main.c |> gcc $(CFLAGS) -c %f -o %o |> %B.o +: main.o |> gcc %f -o %o |> program diff --git a/test/e2e/fixtures/header_dep_transitive/test.sh b/test/e2e/fixtures/header_dep_transitive/test.sh index 3dd2ece..1dfce32 100755 --- a/test/e2e/fixtures/header_dep_transitive/test.sh +++ b/test/e2e/fixtures/header_dep_transitive/test.sh @@ -9,34 +9,32 @@ # 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. +# Expected: step 3 rebuilds, the linked program reflects the new constant. +# Observed: step 3 is a no-op; program is stale. # # Test exits 0 on success (bug fixed), non-zero on failure (bug present). +# +# Oracle: link main.c into a tiny program whose main() returns the constant. +# Running it and reading $? is portable across Linux GNU-binutils and macOS +# LLVM toolchains. Constants stay < 256 so the exit code carries them +# unmodified (POSIX masks exit codes to 8 bits). set -u PUP="${PUP:-$(command -v pup || echo /home/mural/bin/pup)}" BUILD_DIR="build/x86" OBJ="${BUILD_DIR}/src/main.o" +PROG="${BUILD_DIR}/src/program" 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 +# Run the program and report its exit code (= the constant baked into main.o). +constant_in_prog() { + "$1" + echo $? } # ---------- Step 1: initial build ---------- @@ -44,10 +42,11 @@ 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" +[[ -x "$PROG" ]] || die "expected $PROG 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" +K1="$("$PROG"; echo $?)" +echo " $PROG exits with: $K1" +[[ "$K1" == "1" ]] || die "expected exit 1 (ANSWER=1) after step 1, got $K1" # ---------- Step 2: make old.h transitively include newhdr.h ---------- echo @@ -60,10 +59,10 @@ 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" +K2="$("$PROG"; echo $?)" +echo " $PROG exits with: $K2" +[[ "$K2" == "100" ]] \ + || die "expected exit 100 (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 @@ -87,10 +86,10 @@ fi # ---------- Step 3: modify newhdr.h (the transitive header) ---------- echo -echo "=== Step 3: modify newhdr.h (EXTRA 100 -> 7777) ===" +echo "=== Step 3: modify newhdr.h (EXTRA 100 -> 77) ===" cat > include/newhdr.h << 'EOF' #pragma once -#define EXTRA 7777 +#define EXTRA 77 EOF OUT="$("$PUP" -B "$BUILD_DIR" 2>&1)" @@ -98,8 +97,8 @@ 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"))" +K3="$("$PROG"; echo $?)" +echo " $PROG exits with: $K3" if [[ "$OUT" == *"Nothing to do"* ]]; then echo @@ -109,11 +108,11 @@ if [[ "$OUT" == *"Nothing to do"* ]]; then echo " never recorded — confirmed by 'pup show index' above." fi -if [[ "$K3" != "0x1e61" ]]; then +if [[ "$K3" != "77" ]]; then echo echo "Diagnostic:" - echo " expected $OBJ to contain 0x1e61 (EXTRA=7777)" - echo " actually contains $K3 ($(hex_to_dec "$K3"))" + echo " expected $PROG to exit 77 (EXTRA=77)" + echo " actually exits $K3" echo " this means main.c was NOT recompiled after newhdr.h changed" die "transitive header change was not detected" fi diff --git a/test/unit/test_e2e.cpp b/test/unit/test_e2e.cpp index f800bd8..d8af037 100644 --- a/test/unit/test_e2e.cpp +++ b/test/unit/test_e2e.cpp @@ -2105,13 +2105,11 @@ 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]") +// Regression guard: when a source's already-tracked header is edited to +// transitively include a new header, pup must record the new transitive +// header and rebuild on subsequent edits to it. Fixed by binding dep-scan +// command dirty-status to its parent compile (collect_affected_commands). +SCENARIO("Transitive implicit-dep header tracking", "[e2e][shell][incremental]") { WHEN("the header_dep_transitive shell fixture runs") { @@ -2121,9 +2119,7 @@ SCENARIO("Transitive implicit-dep header tracking", "[e2e][shell][incremental][! { 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()); + REQUIRE(result.success()); } } }