Skip to content

Add mutation for ternary operators - #478

Closed
ryanfreckleton wants to merge 1 commit into
boxed:mainfrom
ryanfreckleton:add-ternary-ifexp-mutations
Closed

Add mutation for ternary operators#478
ryanfreckleton wants to merge 1 commit into
boxed:mainfrom
ryanfreckleton:add-ternary-ifexp-mutations

Conversation

@ryanfreckleton

Copy link
Copy Markdown

Closes #196

This adds mutations for ternary expressions (a if cond else b), as discussed in the issue.

The new mutations target the IfExp condition and force each branch explicitly:

mark = "x" if count % 2 else "o"

can now mutate to:

mark = "x" if count % 2 and False else "o"
mark = "x" if count % 2 or True else "o"

Implementation notes:

  • adds an IfExp mutation operator in node_mutation.py
  • uses LibCST's IfExp node, following the implementation pointer in the issue comments
  • wraps low-precedence conditions like b or c before appending and False / or True, so we preserve the original condition semantics

I kept this scoped to ternary operators only, not normal if statements, which matches the issue rationale that branch coverage is less helpful for inline conditionals.

Tests:

  • added direct mutation tests for ternary expressions
  • added a precedence-preservation test for boolean conditions inside ternaries
  • updated the existing mutated-function assertion to check for the new ternary mutants without depending on exact mutant numbering

Comment thread tests/test_mutation.py
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

@Otto-AA Otto-AA Mar 7, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@nicklafleur

Copy link
Copy Markdown
Collaborator

@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.

boxed pushed a commit that referenced this pull request Aug 2, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add mutation for ternary operators

3 participants