from __future__ import annotations
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
import mutants_exclude_check as mec
FIXTURE_LINES = [
"src/a.rs:10:5: replace < with <= in f",
"src/a.rs:10:5: replace < with == in f",
"src/a.rs:20:5: replace > with >= in g",
"src/a.rs:20:5: replace > with >= in g", ]
class MatchCounts(unittest.TestCase):
def test_a_pattern_present_once_counts_one(self):
counts = mec.match_counts(["replace < with <= in f"], FIXTURE_LINES)
self.assertEqual(counts["replace < with <= in f"], 1)
def test_a_pattern_present_twice_counts_two(self):
counts = mec.match_counts(["replace > with >= in g"], FIXTURE_LINES)
self.assertEqual(counts["replace > with >= in g"], 2)
def test_a_pattern_present_nowhere_counts_zero(self):
counts = mec.match_counts(["replace \\+ with - in h"], FIXTURE_LINES)
self.assertEqual(counts["replace \\+ with - in h"], 0)
def test_a_line_anchored_pattern_only_matches_its_own_line(self):
counts = mec.match_counts(["a\\.rs:10:.*replace < with <= in f"], FIXTURE_LINES)
self.assertEqual(counts["a\\.rs:10:.*replace < with <= in f"], 1)
counts = mec.match_counts(["a\\.rs:99:.*replace < with <= in f"], FIXTURE_LINES)
self.assertEqual(counts["a\\.rs:99:.*replace < with <= in f"], 0)
class StripAnsi(unittest.TestCase):
def test_a_line_with_no_escapes_is_unchanged(self):
line = "src/a.rs:10:5: replace < with <= in f"
self.assertEqual(mec.strip_ansi(line), line)
def test_an_escape_sequence_wrapping_an_identifier_is_removed(self):
colored = "src/joint.rs:1032:5: replace \x1b[38;5;13msolve_slider_joint\x1b[0m with ()"
self.assertEqual(
mec.strip_ansi(colored),
"src/joint.rs:1032:5: replace solve_slider_joint with ()",
)
def test_multiple_escapes_on_one_line_are_all_removed(self):
colored = "replace \x1b[33m+\x1b[0m with \x1b[38;5;11m-\x1b[0m in \x1b[38;5;13mf\x1b[0m"
self.assertEqual(mec.strip_ansi(colored), "replace + with - in f")
def test_a_pattern_naming_a_function_only_matches_once_stripped(self):
colored_lines = [
"src/joint.rs:1032:5: replace \x1b[38;5;13msolve_slider_joint\x1b[0m with ()"
]
pattern = "replace solve_slider_joint with \\(\\)"
raw_counts = mec.match_counts([pattern], colored_lines)
self.assertEqual(raw_counts[pattern], 0)
stripped_lines = [mec.strip_ansi(line) for line in colored_lines]
stripped_counts = mec.match_counts([pattern], stripped_lines)
self.assertEqual(stripped_counts[pattern], 1)
class Main(unittest.TestCase):
def run_main(self, mutants_toml_text: str, baseline_text: str | None, lines=FIXTURE_LINES):
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
mutants_toml = tmp_path / "mutants.toml"
mutants_toml.write_text(mutants_toml_text, encoding="utf-8")
baseline = tmp_path / "baseline.txt"
if baseline_text is not None:
baseline.write_text(baseline_text, encoding="utf-8")
with patch.object(mec, "full_mutant_list", return_value=lines):
rc = mec.main(
["--mutants-toml", str(mutants_toml), "--baseline", str(baseline)]
)
return rc, baseline
def test_empty_exclude_re_is_an_error(self):
rc, _ = self.run_main("exclude_re = []\n", baseline_text="")
self.assertEqual(rc, 1)
def test_a_pattern_matching_zero_is_an_error_even_if_in_baseline(self):
rc, _ = self.run_main(
'exclude_re = ["no such pattern anywhere"]\n',
baseline_text="5\tno such pattern anywhere\n",
)
self.assertEqual(rc, 1)
def test_a_pattern_not_yet_in_baseline_is_an_error(self):
rc, _ = self.run_main(
'exclude_re = ["replace < with <= in f"]\n',
baseline_text="",
)
self.assertEqual(rc, 1)
def test_a_pattern_whose_count_changed_is_an_error(self):
rc, _ = self.run_main(
'exclude_re = ["replace > with >= in g"]\n',
baseline_text="1\treplace > with >= in g\n", )
self.assertEqual(rc, 1)
def test_a_pattern_matching_its_tracked_count_passes(self):
rc, _ = self.run_main(
'exclude_re = ["replace > with >= in g"]\n',
baseline_text="2\treplace > with >= in g\n",
)
self.assertEqual(rc, 0)
def test_a_loose_pattern_like_tests_tolerates_a_changed_count(self):
rc, _ = self.run_main(
'exclude_re = ["tests::"]\n',
baseline_text="99\ttests::\n",
lines=[
"src/a.rs:1:1: replace x in tests::helper_one",
"src/a.rs:2:1: replace y in tests::helper_two",
"src/a.rs:3:1: replace z in tests::helper_three",
],
)
self.assertEqual(rc, 0)
def test_a_loose_pattern_like_tests_still_errors_on_zero_matches(self):
rc, _ = self.run_main(
'exclude_re = ["tests::"]\n',
baseline_text="3\ttests::\n",
lines=["src/a.rs:1:1: replace x in production_fn"],
)
self.assertEqual(rc, 1)
def test_a_stale_baseline_entry_no_longer_in_exclude_re_is_an_error(self):
rc, _ = self.run_main(
'exclude_re = ["replace > with >= in g"]\n',
baseline_text="2\treplace > with >= in g\n1\tsome removed entry\n",
)
self.assertEqual(rc, 1)
def test_write_records_the_current_counts(self):
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
mutants_toml = tmp_path / "mutants.toml"
mutants_toml.write_text(
'exclude_re = ["replace > with >= in g", "replace < with <= in f"]\n',
encoding="utf-8",
)
baseline = tmp_path / "baseline.txt"
with patch.object(mec, "full_mutant_list", return_value=FIXTURE_LINES):
rc = mec.main(
[
"--mutants-toml",
str(mutants_toml),
"--baseline",
str(baseline),
"--write",
]
)
self.assertEqual(rc, 0)
written = mec.read_baseline(baseline)
self.assertEqual(written["replace > with >= in g"], 2)
self.assertEqual(written["replace < with <= in f"], 1)
if __name__ == "__main__":
unittest.main()