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
|
return True
|
||||||
if lines is None:
|
if lines is None:
|
||||||
return False
|
return False
|
||||||
return site.line in lines
|
return site.line in lines or site.anchor_line in lines
|
||||||
|
|
||||||
|
|
||||||
def _select(
|
def _select(
|
||||||
@@ -343,7 +343,7 @@ def scan_file(
|
|||||||
found = [site for site in found if site.line in lines]
|
found = [site for site in found if site.line in lines]
|
||||||
covered = None
|
covered = None
|
||||||
if not ignore_coverage and covered_lines is not 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)
|
history = load_history(root, file_key)
|
||||||
digests = form_digests(source, path, root)
|
digests = form_digests(source, path, root)
|
||||||
changed = set()
|
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(
|
mutation_id=mutation_id(
|
||||||
file_key, owner.namespace, form, raw.start, raw.end, raw.original, raw.mutant
|
file_key, owner.namespace, form, raw.start, raw.end, raw.original, raw.mutant
|
||||||
),
|
),
|
||||||
|
anchor_line=raw.anchor_line,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return found
|
return found
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ class Site:
|
|||||||
mutant: str
|
mutant: str
|
||||||
category: str
|
category: str
|
||||||
mutation_id: str
|
mutation_id: str
|
||||||
|
# The line its statement starts on; coverage on that line counts for the site.
|
||||||
|
anchor_line: int = 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def description(self) -> str:
|
def description(self) -> str:
|
||||||
|
|||||||
+41
-1
@@ -93,8 +93,12 @@ _CLOJURE_HEAD = {
|
|||||||
|
|
||||||
|
|
||||||
class RawSite:
|
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
|
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.start = start
|
||||||
self.end = end
|
self.end = end
|
||||||
self.original = original
|
self.original = original
|
||||||
@@ -165,10 +169,44 @@ def _add(found: list[RawSite], node, original: str, mutant: str, category: str)
|
|||||||
original=original,
|
original=original,
|
||||||
mutant=mutant,
|
mutant=mutant,
|
||||||
category=category,
|
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:
|
def _clojure_head(node, data: bytes) -> str | None:
|
||||||
for child in node.children:
|
for child in node.children:
|
||||||
if child.type == "sym_lit":
|
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:
|
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:
|
if node.child_count == 0:
|
||||||
_visit_leaf(node, data, language, found)
|
_visit_leaf(node, data, language, found)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -921,3 +921,12 @@ def test_snapshots_keep_other_files_empty_directories_and_odd_records(tmp_path):
|
|||||||
assert not (base / "empty").exists()
|
assert not (base / "empty").exists()
|
||||||
assert (full / "keep.edn").is_file()
|
assert (full / "keep.edn").is_file()
|
||||||
_remove_empty_dirs(tmp_path / "missing-metrics")
|
_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
|
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):
|
def test_a_failed_baseline_does_not_rewrite_metrics(tmp_path):
|
||||||
path = tmp_path / "src" / "demo.py"
|
path = tmp_path / "src" / "demo.py"
|
||||||
path.parent.mkdir()
|
path.parent.mkdir()
|
||||||
|
|||||||
@@ -203,3 +203,34 @@ def test_a_deep_expression_is_walked(tmp_path):
|
|||||||
assert discover_raw(python, "python", "app.py")
|
assert discover_raw(python, "python", "app.py")
|
||||||
go = f"package p\nfunc place() int {{ return {expr} }}\n"
|
go = f"package p\nfunc place() int {{ return {expr} }}\n"
|
||||||
assert discover_raw(go, "go", "app.go")
|
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