Add mutation for ternary operators - #478
Conversation
| mutants = mutants_for_source("a if b or c else d") | ||
|
|
||
| assert "a if (b or c) and False else d" in mutants | ||
| assert "a if (b or c) or True else d" in mutants |
There was a problem hiding this comment.
Thank you for the PR!
It's an interesting question, but If I'm not mistaken, I don't think the precedence matters in these cases.
For both 1 if (b or c) and False else 2 and 1 if b or (c and False) else 2:
- we always evaluate expression b first
- if b is falsy, we always evaluate c
- we always return False
In the general case, I think adding and False or or True won't effect which expressions we evaluate, regardless of precedence.
Are you aware of a case where it could matter? Then it would be nice to add it here as test case. Otherwise, I think we could simplify the operator_if_exp mutation to directly use node.test.
|
@ryanfreckleton since there's been no activity on this PR in 2 months, I'm going to go ahead and close it. The contribution and idea seems really interesting, and I would encourage you to re-submit an updated PR for it. |
Closes #196. A ternary is a branch that nothing here mutated. Branch coverage does not see an uncovered arm either, so an untested arm read as a pass twice over. `grep -rn "IfExp\|ternary\|if_exp"` over src/, tests/ and docs/ returned nothing before this. `operator_if_exp` yields two mutants per ternary, neutralising the condition in each direction: a if b else c -> a if (b) and False else c a if (b) or True else c On the parentheses, which #478's review asked to drop: they are load-bearing for the `and False` half. `and` binds tighter than a top-level `or`, so an unparenthesised `b or c` becomes `b or (c and False)`, which still takes the true branch whenever `b` is truthy. Over the 16 assignments of a `a if b or c else d`: (b or c) and False differs from the original on 6 of 16 b or c and False differs on 2 of 16, and only when b is falsy `or True` genuinely does not need them — parenthesised and not are identical on all 16 — but wrapping both keeps one rule rather than two, and costs nothing. test_function_with_annotation shifts because its fixture contains a ternary: the two new mutants take numbers 1 and 2 and push the arithmetic and index mutants to 3-5. The three original assertions are kept, renumbered, and the two new mutants are asserted alongside them, so the test still shows nothing was lost. Measured on 4f12080, `pytest tests/`: 12 failed / 345 passed before and after, with an identical FAILED set. (tests/utils/test_safe_setproctitle.py is flaky here regardless of this change — 1 pass in 5 runs both with and without it.) ruff check, ruff format --check and mypy pass. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #196
This adds mutations for ternary expressions (
a if cond else b), as discussed in the issue.The new mutations target the
IfExpcondition and force each branch explicitly:can now mutate to:
Implementation notes:
IfExpmutation operator innode_mutation.pyIfExpnode, following the implementation pointer in the issue commentsb or cbefore appendingand False/or True, so we preserve the original condition semanticsI kept this scoped to ternary operators only, not normal
ifstatements, which matches the issue rationale that branch coverage is less helpful for inline conditionals.Tests: