fix(887): two defects the FIX introduced, found by attacking it rather than reading it
Both were measured, not reasoned, and both are the shape this repo keeps recording — the fix round introducing an adjacent defect, and a unit test using a simpler input shape than the real file has. `npm test -- --run && echo ok || true` reported NO suppression. `&&`/`||` chain across a whole list, so when the suite fails the `&&` right-hand side is skipped and the `||` right-hand side runs: the list exits 0 and the suite's failure is swallowed even though the `||` is not adjacent to it. The detector looked only at the separator IMMEDIATELY after the suite segment. It is now scoped to the `;`-delimited list, which also catches a backgrounded `npm test &` (status never awaited) and `( npm test ) || true`. A `;` ends the list and resets, so `npm test; other || true` stays clean — that `||` is about the other command. `--exclude 2 > log` reported `['--exclude']`, losing the filter's own value: stripping redirections as a PRE-PASS let the file-descriptor rule claim the `2` before the flag could. Redirections are now consumed inside the walk, after flag values are taken. The mutant battery grew from 17 to 24 and is 0-missed. The `docs/guard-inventory.md` row now states the count and, explicitly, the grading: exactly ONE of the 24 is declared in `mutation_manifest.py` and re-executed every suite; the other 23 were witnessed by hand and are NOT standing. That is the same footing `pageSizeCallSites.guard.test.ts` states for its nine, and saying so is the difference between evidence for the reach and a claim of a per-run proof. Refs: #887 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019T79beF1Ufid3dXju4yqkF
This commit is contained in:
@@ -461,35 +461,14 @@ def suite_invocations(text: str, scripts: Mapping[str, list[str]] | None = None)
|
||||
return found
|
||||
|
||||
|
||||
def _strip_redirections(args: list[str]) -> list[str]:
|
||||
"""Drop redirection operators, their targets, and any file descriptor written before them.
|
||||
|
||||
`2>&1` tokenises as `['2', '>&', '1']`, so the descriptor and the target are bare tokens that
|
||||
would otherwise both be reported as positional spec filters.
|
||||
"""
|
||||
kept: list[str] = []
|
||||
index = 0
|
||||
while index < len(args):
|
||||
argument = args[index]
|
||||
if _REDIRECTION.match(argument):
|
||||
index += 2 # the operator and its target
|
||||
continue
|
||||
if (
|
||||
argument.isdigit()
|
||||
and index + 1 < len(args)
|
||||
and _REDIRECTION.match(args[index + 1])
|
||||
and not args[index + 1][:1].isdigit()
|
||||
):
|
||||
index += 3 # descriptor, operator, target
|
||||
continue
|
||||
kept.append(argument)
|
||||
index += 1
|
||||
return kept
|
||||
|
||||
|
||||
def _narrowing_in(args: list[str]) -> list[str]:
|
||||
"""The subset of `args` that restricts WHICH specs run."""
|
||||
args = _strip_redirections(args)
|
||||
"""The subset of `args` that restricts WHICH specs run.
|
||||
|
||||
Redirections are consumed HERE rather than in a pre-pass, so a flag's value is claimed before the
|
||||
file-descriptor rule can see it. As a pre-pass, `--exclude 2 > log` lost the `2`: the digit was
|
||||
read as the descriptor of the following `>` and dropped with it, and the fault message then named
|
||||
a filter without its value. Measured.
|
||||
"""
|
||||
found: list[str] = []
|
||||
index = 0
|
||||
while index < len(args):
|
||||
@@ -497,6 +476,17 @@ def _narrowing_in(args: list[str]) -> list[str]:
|
||||
index += 1
|
||||
if argument == "--":
|
||||
continue
|
||||
if _REDIRECTION.match(argument):
|
||||
index += 1 # the operator's target
|
||||
continue
|
||||
if (
|
||||
argument.isdigit()
|
||||
and index < len(args)
|
||||
and _REDIRECTION.match(args[index])
|
||||
and not args[index][:1].isdigit()
|
||||
):
|
||||
index += 2 # the operator and its target; `argument` was its descriptor
|
||||
continue
|
||||
base = argument.split("=", 1)[0]
|
||||
if base in NARROWING_FLAGS:
|
||||
found.append(argument)
|
||||
@@ -551,6 +541,13 @@ def failure_suppressions(command: str, scripts: Mapping[str, list[str]], depth:
|
||||
tokens = tokens[1:] if tokens[0] == "RUN" else []
|
||||
|
||||
current: list[str] = []
|
||||
# Whether the SUITE has run earlier in this `;`-delimited list. `&&`/`||` chain across a
|
||||
# whole list, so `npm test && echo ok || true` runs `true` when the suite fails and exits 0 —
|
||||
# the suite's failure is swallowed even though the `||` does not sit immediately after it.
|
||||
# Looking only at the NEXT separator missed that; measured. A `;` (or a newline, since each
|
||||
# line is walked separately) ends the list and resets, because a `||` in the next list is
|
||||
# about the next command.
|
||||
suite_in_list = False
|
||||
for word in [*tokens, ";"]:
|
||||
if word not in SEPARATORS:
|
||||
current.append(word)
|
||||
@@ -559,17 +556,21 @@ def failure_suppressions(command: str, scripts: Mapping[str, list[str]], depth:
|
||||
head = _strip_prefixes(current)
|
||||
if head[:2] == ["set", "+e"]:
|
||||
found.append("`set +e` disarms the step's own `set -e`")
|
||||
if suite_args(current, scripts) is not None and word in {"||", "|"}:
|
||||
found.append(
|
||||
"a trailing or-true swallows the exit status"
|
||||
if word == "||"
|
||||
else "a pipeline reports the LAST command's status"
|
||||
)
|
||||
if suite_args(current, scripts) is not None:
|
||||
suite_in_list = True
|
||||
if word == "|":
|
||||
found.append("a pipeline reports the LAST command's status")
|
||||
if len(head) >= 3 and head[0] in SHELLS:
|
||||
for offset, inner in enumerate(head[1:-1], start=1):
|
||||
if inner.startswith("-") and not inner.startswith("--") and "c" in inner.lstrip("-"):
|
||||
found.extend(failure_suppressions(head[offset + 1], scripts, depth + 1))
|
||||
break
|
||||
if word == "||" and suite_in_list:
|
||||
found.append("an `||` later in the same list swallows the exit status")
|
||||
if word == "&" and suite_in_list:
|
||||
found.append("`&` backgrounds the command, so its status is never awaited")
|
||||
if word == ";":
|
||||
suite_in_list = False
|
||||
current = []
|
||||
return found
|
||||
|
||||
@@ -1041,10 +1042,19 @@ def test_a_SUPPRESSED_failure_is_advisory_by_another_spelling() -> None:
|
||||
assert failure_suppressions("npm test -- --run || true", scripts)
|
||||
assert failure_suppressions("npm test -- --run | tee /tmp/spa.log", scripts)
|
||||
assert failure_suppressions("set +e\nnpm test -- --run", scripts)
|
||||
# A `||` LATER in the same list still swallows it: if the suite fails, the `&&` right-hand side
|
||||
# is skipped and the `||` right-hand side runs, so the list exits 0. Measured missed before the
|
||||
# detector became list-scoped rather than next-separator-scoped.
|
||||
assert failure_suppressions("npm test -- --run && echo ok || true", scripts)
|
||||
assert failure_suppressions("npm test -- --run && npm run build || true", scripts)
|
||||
assert failure_suppressions("npm test -- --run &", scripts)
|
||||
assert failure_suppressions("( npm test -- --run ) || true", scripts)
|
||||
# An ordinary run, and a `||` on something that is NOT the suite, are both clean.
|
||||
assert failure_suppressions(marker + "npm test -- --run", scripts) == []
|
||||
assert failure_suppressions("npm run lint && npm test -- --run", scripts) == []
|
||||
assert failure_suppressions("npm run lint || true\nnpm test -- --run", scripts) == []
|
||||
# A `;` ends the list, so a `||` after it is about the NEXT command, not the suite.
|
||||
assert failure_suppressions("npm test -- --run; npm run other || true", scripts) == []
|
||||
|
||||
|
||||
def test_a_REDIRECTION_is_not_a_spec_filter() -> None:
|
||||
@@ -1059,8 +1069,11 @@ def test_a_REDIRECTION_is_not_a_spec_filter() -> None:
|
||||
assert narrowing_arguments("npm test -- --run >> test.log", scripts) == []
|
||||
assert narrowing_arguments("npm test -- --run 2>&1", scripts) == []
|
||||
assert narrowing_arguments("npm test -- --run > test.log 2>&1", scripts) == []
|
||||
# A real filter beside a redirection is still reported.
|
||||
# A real filter beside a redirection is still reported, INCLUDING a numeric value: stripping
|
||||
# redirections as a pre-pass ate the `2` in `--exclude 2 > log` by reading it as a descriptor.
|
||||
assert narrowing_arguments("npm test -- --run --exclude a.ts > log", scripts) == ["--exclude", "a.ts"]
|
||||
assert narrowing_arguments("npm test -- --run --exclude 2 > log", scripts) == ["--exclude", "2"]
|
||||
assert narrowing_arguments("npm test -- --run 2 --exclude a.ts", scripts) == ["2", "--exclude", "a.ts"]
|
||||
|
||||
|
||||
def test_the_flags_that_move_the_COLLECTED_SET_count_as_filters() -> None:
|
||||
|
||||
Reference in New Issue
Block a user