| """Fast hand-rolled mutation scoring for the bundled legacy sample.""" |
|
|
| from __future__ import annotations |
|
|
| import shutil |
| from dataclasses import dataclass |
| from pathlib import Path |
|
|
| from generator import GeneratedSuite, write_suite |
| from runner import RunResult, run_pytest |
|
|
|
|
| @dataclass(frozen=True) |
| class Mutation: |
| id: str |
| file: str |
| find: str |
| replace: str |
| label: str |
|
|
|
|
| @dataclass(frozen=True) |
| class MutationResult: |
| mutation: Mutation |
| killed: bool |
| run: RunResult |
|
|
|
|
| @dataclass(frozen=True) |
| class MutationScore: |
| killed: int |
| total: int |
| results: tuple[MutationResult, ...] |
|
|
| @property |
| def percent(self) -> float: |
| return 0.0 if self.total == 0 else round(self.killed / self.total * 100, 1) |
|
|
| @property |
| def survived(self) -> list[Mutation]: |
| return [result.mutation for result in self.results if not result.killed] |
|
|
| def headline(self) -> str: |
| return f"{self.killed}/{self.total} behavior changes detected" |
|
|
|
|
| MUTATIONS: tuple[Mutation, ...] = ( |
| Mutation( |
| id="bulk_multiply_to_divide", |
| file="pricing.py", |
| find="return round(unit * qty * discount, 2)", |
| replace="return round(unit / qty * discount, 2)", |
| label="pricing.py bulk_price * -> /", |
| ), |
| Mutation( |
| id="discount_minus_to_plus", |
| file="pricing.py", |
| find="return round(price * (1 - pct / 100), 2)", |
| replace="return round(price * (1 + pct / 100), 2)", |
| label="pricing.py apply_discount - -> +", |
| ), |
| Mutation( |
| id="tax_plus_to_minus", |
| file="pricing.py", |
| find="return round(amount * (1 + rate / 100), 2)", |
| replace="return round(amount * (1 - rate / 100), 2)", |
| label="pricing.py with_tax + -> -", |
| ), |
| Mutation( |
| id="line_multiply_to_plus", |
| file="invoice.py", |
| find="return round(qty * unit, 2)", |
| replace="return round(qty + unit, 2)", |
| label="invoice.py line_total * -> +", |
| ), |
| Mutation( |
| id="weekend_ge_to_gt", |
| file="dates.py", |
| find="return d.weekday() >= 5", |
| replace="return d.weekday() > 5", |
| label="dates.py is_weekend >= -> >", |
| ), |
| Mutation( |
| id="days_abs_removed", |
| file="dates.py", |
| find="return abs((b - a).days)", |
| replace="return (b - a).days", |
| label="dates.py days_between remove abs", |
| ), |
| Mutation( |
| id="slug_strip_removed", |
| file="slugify.py", |
| find="return cleaned.strip(\"-\")", |
| replace="return cleaned", |
| label="slugify.py strip removed", |
| ), |
| Mutation( |
| id="truncate_len_lt", |
| file="slugify.py", |
| find="if len(s) <= n:", |
| replace="if len(s) < n:", |
| label="slugify.py <= -> <", |
| ), |
| Mutation( |
| id="bulk_threshold_ge_to_gt", |
| file="pricing.py", |
| find="discount = 0.9 if qty >= 10 else 1.0", |
| replace="discount = 0.9 if qty > 10 else 1.0", |
| label="pricing.py bulk_price >= -> >", |
| ), |
| ) |
|
|
|
|
| def mutation_score( |
| source_parent: str | Path, |
| suite: GeneratedSuite, |
| tmp_root: str | Path, |
| package_name: str = "legacy_repo", |
| mutations: tuple[Mutation, ...] = MUTATIONS, |
| ) -> MutationScore: |
| """Apply each mutation to a copy and run the generated suite.""" |
| source_parent = Path(source_parent).resolve() |
| tmp_root = Path(tmp_root).resolve() |
| tmp_root.mkdir(parents=True, exist_ok=True) |
| results: list[MutationResult] = [] |
|
|
| for index, mutation in enumerate(mutations, start=1): |
| mutant_dir = tmp_root / f"mutant_{index}_{mutation.id}" |
| if mutant_dir.exists(): |
| shutil.rmtree(mutant_dir) |
| shutil.copytree(source_parent, mutant_dir / "samples") |
| write_suite(suite, mutant_dir) |
| applied = _apply_mutation(mutant_dir / "samples" / package_name, mutation) |
| if not applied: |
| run = RunResult( |
| ok=False, |
| passed=0, |
| failed=0, |
| errors=1, |
| coverage=None, |
| stdout="", |
| stderr=f"Mutation not applied: {mutation.label}", |
| returncode=2, |
| ) |
| results.append(MutationResult(mutation=mutation, killed=False, run=run)) |
| continue |
| run = run_pytest(mutant_dir, package_parent=mutant_dir / "samples", package_name=package_name) |
| results.append(MutationResult(mutation=mutation, killed=not run.ok, run=run)) |
|
|
| killed = sum(1 for result in results if result.killed) |
| return MutationScore(killed=killed, total=len(results), results=tuple(results)) |
|
|
|
|
| def apply_single_mutation(package_root: str | Path, mutation_id: str) -> bool: |
| """Apply one catalog mutation in-place for the live Inject button.""" |
| mutation = next(item for item in MUTATIONS if item.id == mutation_id) |
| return _apply_mutation(Path(package_root), mutation) |
|
|
|
|
| def _apply_mutation(package_root: Path, mutation: Mutation) -> bool: |
| path = package_root / mutation.file |
| text = path.read_text(encoding="utf-8") |
| if mutation.find not in text: |
| return False |
| path.write_text(text.replace(mutation.find, mutation.replace, 1), encoding="utf-8") |
| return True |
|
|