Disclaimer: AI-drafted, verified by me before filing. I answer follow-ups.
operator_string emits three mutants for every string literal: the literal wrapped in XX, lowercased, and uppercased. Where the literal's consumer is case-insensitive -- SQL, codec names, HTTP header names -- the case flips are equivalent mutants by construction.
Expected: a mutation I cannot kill can be switched off, as --disable-mutation-types did in 2.x (#175, added by #225).
Actual: both case flips survive every run, and the only lever in 3.x, do_not_mutate_patterns, matches whole lines, so it drops the XX mutant on that line too.
They were 25% and 11% of all survivors on two of my codebases: re-read on every run, and nothing to be done about them.
Reproduce -- three files, mutmut 3.7.0, pytest 9.1.1, CPython 3.12.12
# example.py
import sqlite3
def note_count(db_path: str) -> int:
con = sqlite3.connect(db_path)
return con.execute("SELECT COUNT(*) FROM notes").fetchone()[0]
# test_example.py
import sqlite3
from example import note_count
def test_note_count(tmp_path):
db = tmp_path / "n.db"
con = sqlite3.connect(db)
con.execute("CREATE TABLE notes (id int)")
con.execute("INSERT INTO notes VALUES (1)")
con.commit()
con.close()
assert note_count(str(db)) == 1
# pyproject.toml
[project]
name = "repro"
version = "0"
requires-python = ">=3.11"
[tool.mutmut]
source_paths = ["example.py"]
$ mutmut run
7/7 🎉 5 🫥 0 ⏰ 0 🤔 0 🙁 2 🔇 0 🧙 0
$ mutmut results
example.x_note_count__mutmut_5: survived
example.x_note_count__mutmut_6: survived
$ mutmut show example.x_note_count__mutmut_5
# example.x_note_count__mutmut_5: survived
--- example.py
+++ example.py
@@ -1,3 +1,3 @@
def note_count(db_path: str) -> int:
con = sqlite3.connect(db_path)
- return con.execute("SELECT COUNT(*) FROM notes").fetchone()[0]
+ return con.execute("select count(*) from notes").fetchone()[0]
$ mutmut show example.x_note_count__mutmut_6 # the same line, uppercased
+ return con.execute("SELECT COUNT(*) FROM NOTES").fetchone()[0]
SQLite parses either casing and resolves table names case-insensitively, so both mutants are equivalent to the original. The only test that would kill them asserts the query string's exact casing, which tests the literal rather than the behaviour.
What the line pattern costs -- 7 mutants become 2
[tool.mutmut]
source_paths = ["example.py"]
do_not_mutate_patterns = ["SELECT"]
$ mutmut run
2/2 🎉 2 🫥 0 ⏰ 0 🤔 0 🙁 0 🔇 0 🧙 0
Five of the seven mutants are gone. Two of them, mutant 4 ("XXSELECT COUNT(*) FROM notesXX") and mutant 7 (.fetchone()[1]), are killed by the test as it stands, so the pattern removes working signal in order to remove two mutants that carry none.
operator_stringemits three mutants for every string literal: the literal wrapped inXX, lowercased, and uppercased. Where the literal's consumer is case-insensitive -- SQL, codec names, HTTP header names -- the case flips are equivalent mutants by construction.Expected: a mutation I cannot kill can be switched off, as
--disable-mutation-typesdid in 2.x (#175, added by #225).Actual: both case flips survive every run, and the only lever in 3.x,
do_not_mutate_patterns, matches whole lines, so it drops theXXmutant on that line too.They were 25% and 11% of all survivors on two of my codebases: re-read on every run, and nothing to be done about them.
Reproduce -- three files, mutmut 3.7.0, pytest 9.1.1, CPython 3.12.12
SQLite parses either casing and resolves table names case-insensitively, so both mutants are equivalent to the original. The only test that would kill them asserts the query string's exact casing, which tests the literal rather than the behaviour.
What the line pattern costs -- 7 mutants become 2
Five of the seven mutants are gone. Two of them, mutant 4 (
"XXSELECT COUNT(*) FROM notesXX") and mutant 7 (.fetchone()[1]), are killed by the test as it stands, so the pattern removes working signal in order to remove two mutants that carry none.