Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/mutmut/mutation/mutators.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,27 @@ def operator_match(node: cst.Match) -> Iterable[cst.CSTNode]:
yield node.with_changes(cases=[*node.cases[:i], *node.cases[i + 1 :]])


def operator_if_exp(node: cst.IfExp) -> Iterable[cst.IfExp]:
"""Force a ternary down each branch by neutralising its condition.

A ternary's two arms are a branch that nothing else here mutates, and
branch coverage does not see an uncovered arm either, so an untested arm
reads as a pass twice over.

The condition is parenthesised before `and False` / `or True` is appended.
Without the parentheses `and` binds tighter than a top-level `or`, so
`b or c` would become `b or (c and False)` -- which still takes the true
branch whenever `b` is truthy, i.e. an almost-always-surviving mutant that
tests cannot kill. `or True` happens not to need them, but wrapping both
keeps one rule instead of two.
"""
parenthesised = node.test.with_changes(lpar=[cst.LeftParen()], rpar=[cst.RightParen()])
for operator, literal in ((cst.And(), "False"), (cst.Or(), "True")):
yield node.with_changes(
test=cst.BooleanOperation(left=parenthesised, operator=operator, right=cst.Name(literal))
)


# Operators that should be called on specific node types
mutation_operators: OPERATORS_TYPE = [
(cst.BaseNumber, operator_number),
Expand All @@ -265,6 +286,7 @@ def operator_match(node: cst.Match) -> Iterable[cst.CSTNode]:
(cst.CSTNode, operator_keywords),
(cst.CSTNode, operator_swap_op),
(cst.Match, operator_match),
(cst.IfExp, operator_if_exp),
]


Expand Down
10 changes: 7 additions & 3 deletions tests/mutation/test_mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,9 +429,13 @@ def test_function_with_annotation():
print(mutated_code)

expected_defs = [
"def x_capitalize__mutmut_1(s : str):\n return s[0].title() - s[1:] if s else s",
"def x_capitalize__mutmut_2(s : str):\n return s[1].title() + s[1:] if s else s",
"def x_capitalize__mutmut_3(s : str):\n return s[0].title() + s[2:] if s else s",
# The ternary's condition is mutated first; `operator_if_exp` yields
# these two and shifts the arithmetic/index mutants down by two.
"def x_capitalize__mutmut_1(s : str):\n return s[0].title() + s[1:] if (s) and False else s",
"def x_capitalize__mutmut_2(s : str):\n return s[0].title() + s[1:] if (s) or True else s",
"def x_capitalize__mutmut_3(s : str):\n return s[0].title() - s[1:] if s else s",
"def x_capitalize__mutmut_4(s : str):\n return s[1].title() + s[1:] if s else s",
"def x_capitalize__mutmut_5(s : str):\n return s[0].title() + s[2:] if s else s",
]

for expected in expected_defs:
Expand Down
Loading