mirror of
https://github.com/unclebob/mutator.git
synced 2026-11-06 18:23:50 +00:00
Count multi-line statement sites as covered from their start line; skip TypeScript type positions
This commit is contained in:
@@ -55,7 +55,7 @@ def _covered(site: Site, lines: set[int] | None, ignore_coverage: bool) -> bool:
|
||||
return True
|
||||
if lines is None:
|
||||
return False
|
||||
return site.line in lines
|
||||
return site.line in lines or site.anchor_line in lines
|
||||
|
||||
|
||||
def _select(
|
||||
@@ -343,7 +343,7 @@ def scan_file(
|
||||
found = [site for site in found if site.line in lines]
|
||||
covered = None
|
||||
if not ignore_coverage and covered_lines is not None:
|
||||
covered = {site.mutation_id: site.line in covered_lines for site in found}
|
||||
covered = _coverage_map(found, covered_lines, ignore_coverage)
|
||||
history = load_history(root, file_key)
|
||||
digests = form_digests(source, path, root)
|
||||
changed = set()
|
||||
|
||||
@@ -171,6 +171,7 @@ def sites_in_file(source: str, path: Path, root: Path, file_key: str) -> list[Si
|
||||
mutation_id=mutation_id(
|
||||
file_key, owner.namespace, form, raw.start, raw.end, raw.original, raw.mutant
|
||||
),
|
||||
anchor_line=raw.anchor_line,
|
||||
)
|
||||
)
|
||||
return found
|
||||
|
||||
@@ -20,6 +20,8 @@ class Site:
|
||||
mutant: str
|
||||
category: str
|
||||
mutation_id: str
|
||||
# The line its statement starts on; coverage on that line counts for the site.
|
||||
anchor_line: int = 0
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
|
||||
+41
-1
@@ -93,8 +93,12 @@ _CLOJURE_HEAD = {
|
||||
|
||||
|
||||
class RawSite:
|
||||
def __init__(self, line: int, start: int, end: int, original: str, mutant: str, category: str):
|
||||
def __init__(
|
||||
self, line: int, start: int, end: int, original: str, mutant: str, category: str, anchor_line: int = 0
|
||||
):
|
||||
self.line = line
|
||||
# The line its statement starts on. LCOV marks only that line of a multi-line statement.
|
||||
self.anchor_line = anchor_line or line
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.original = original
|
||||
@@ -165,10 +169,44 @@ def _add(found: list[RawSite], node, original: str, mutant: str, category: str)
|
||||
original=original,
|
||||
mutant=mutant,
|
||||
category=category,
|
||||
anchor_line=_statement_line(node),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _is_statement(node_type: str) -> bool:
|
||||
return node_type.endswith("_statement") or node_type.endswith("_declaration")
|
||||
|
||||
|
||||
def _statement_line(node) -> int:
|
||||
current = node.parent
|
||||
while current is not None:
|
||||
if _is_statement(current.type):
|
||||
return current.start_point[0] + 1
|
||||
current = current.parent
|
||||
return node.start_point[0] + 1
|
||||
|
||||
|
||||
# A literal in a TypeScript type, such as `{ ok: true }` in a return type, never runs.
|
||||
_TYPE_NODES = {
|
||||
"type_annotation",
|
||||
"type_alias_declaration",
|
||||
"interface_declaration",
|
||||
"type_arguments",
|
||||
"type_parameters",
|
||||
"literal_type",
|
||||
}
|
||||
|
||||
|
||||
def _in_type(node) -> bool:
|
||||
current = node.parent
|
||||
while current is not None:
|
||||
if current.type in _TYPE_NODES:
|
||||
return True
|
||||
current = current.parent
|
||||
return False
|
||||
|
||||
|
||||
def _clojure_head(node, data: bytes) -> str | None:
|
||||
for child in node.children:
|
||||
if child.type == "sym_lit":
|
||||
@@ -299,6 +337,8 @@ def _visit_leaf(node, data: bytes, language: str, found: list[RawSite]) -> None:
|
||||
|
||||
|
||||
def _visit_node(node, data: bytes, language: str, found: list[RawSite]) -> None:
|
||||
if language == "typescript" and _in_type(node):
|
||||
return
|
||||
if node.child_count == 0:
|
||||
_visit_leaf(node, data, language, found)
|
||||
else:
|
||||
|
||||
@@ -921,3 +921,12 @@ def test_snapshots_keep_other_files_empty_directories_and_odd_records(tmp_path):
|
||||
assert not (base / "empty").exists()
|
||||
assert (full / "keep.edn").is_file()
|
||||
_remove_empty_dirs(tmp_path / "missing-metrics")
|
||||
|
||||
|
||||
def test_scan_counts_a_site_inside_a_covered_multi_line_statement(tmp_path):
|
||||
path = tmp_path / "src" / "demo.py"
|
||||
path.parent.mkdir()
|
||||
path.write_text("def add(a, b):\n return (a\n + b)\n", encoding="utf-8")
|
||||
scanned = scan_file(path, tmp_path, covered_lines={2}, ignore_coverage=False, lines=None)
|
||||
assert "uncovered" not in scanned
|
||||
assert "+ -> -" in scanned
|
||||
|
||||
@@ -113,6 +113,29 @@ def test_uncovered_sites_are_not_executed(tmp_path):
|
||||
assert result.forms[0].sites == 4
|
||||
|
||||
|
||||
def test_a_site_inside_a_covered_multi_line_statement_is_run(tmp_path):
|
||||
# LCOV marks only the line a statement starts on.
|
||||
path = tmp_path / "src" / "demo.py"
|
||||
path.parent.mkdir()
|
||||
path.write_text("def add(a, b):\n return (a\n + b)\n", encoding="utf-8")
|
||||
runner = FakeRunner()
|
||||
result = mutate_file(
|
||||
path,
|
||||
tmp_path,
|
||||
runner=runner,
|
||||
covered_lines={2},
|
||||
ignore_coverage=False,
|
||||
mutate_all=True,
|
||||
lines=None,
|
||||
test_command="fake",
|
||||
timeout_factor=10,
|
||||
mutation_warning=50,
|
||||
baselines={},
|
||||
)
|
||||
assert result.forms[0].uncovered == 0
|
||||
assert result.forms[0].killed == 1
|
||||
|
||||
|
||||
def test_a_failed_baseline_does_not_rewrite_metrics(tmp_path):
|
||||
path = tmp_path / "src" / "demo.py"
|
||||
path.parent.mkdir()
|
||||
|
||||
@@ -203,3 +203,34 @@ def test_a_deep_expression_is_walked(tmp_path):
|
||||
assert discover_raw(python, "python", "app.py")
|
||||
go = f"package p\nfunc place() int {{ return {expr} }}\n"
|
||||
assert discover_raw(go, "go", "app.go")
|
||||
|
||||
|
||||
def test_typescript_types_are_not_mutation_sites(tmp_path):
|
||||
source = """type Flag = { on: true };
|
||||
interface Box { open: false }
|
||||
export function check(n: number): { ok: true } | { ok: false; used?: true } {
|
||||
const seen: Promise<{ done: false }> | null = null;
|
||||
return { ok: n > 0 };
|
||||
}
|
||||
"""
|
||||
sites = _sites(tmp_path, "src/demo/check.ts", source)
|
||||
described = {(site.line, site.original, site.mutant) for site in sites}
|
||||
assert described == {(5, ">", ">="), (5, "0", "1")}
|
||||
|
||||
|
||||
def test_a_site_remembers_where_its_statement_starts(tmp_path):
|
||||
source = """export function save(db: Db, now: number) {
|
||||
db
|
||||
.prepare("x")
|
||||
.bind(now + 1)
|
||||
.run();
|
||||
return {
|
||||
ok: true,
|
||||
};
|
||||
}
|
||||
"""
|
||||
sites = _sites(tmp_path, "src/demo/save.ts", source)
|
||||
anchors = {(site.line, site.original, site.anchor_line) for site in sites}
|
||||
assert (4, "+", 2) in anchors
|
||||
assert (4, "1", 2) in anchors
|
||||
assert (7, "true", 6) in anchors
|
||||
|
||||
Reference in New Issue
Block a user