mirror of
https://github.com/unclebob/mutator.git
synced 2026-11-12 04:33:49 +00:00
Read Go coverage by module import path, the name go test gives a file.
Why: every Go run printed "No coverage data for <file>; its sites are uncovered". `go test -coverprofile` keys files by import path (`github.com/acme/tool/cmd/tool/main.go`); mutator looked them up by absolute disk path (`/.../tool/cmd/tool/main.go`). Neither path ends with the other, so no Go file ever matched and only --no-coverage ran sites. What: `_go_lines` asks crapper's `go_import_name` for the profile's name of the file (nearest go.mod module line plus path inside the module) and `_lookup` tries that name as an exact key, then the existing suffix match. Other languages pass no name and are unchanged. Needs crapper c757ec7 or later. Proof: two new specs (module at the root, module in a subdirectory with a different name) failed with None first, now pass; 78 pass. crapper: _go_lines 6.0, covered_lines 5.0, _lookup 4.0. dryer: no candidates. mutator on the changed lines: one equivalent survivor (insert(0) vs insert(1)) removed by appending the name; no survivors left.
This commit is contained in:
+12
-5
@@ -1,6 +1,7 @@
|
|||||||
"""Decide which lines the tests already exercise.
|
"""Decide which lines the tests already exercise.
|
||||||
|
|
||||||
Java uses JaCoCo source lines. Go uses statement profiles. Clojure prefers
|
Java uses JaCoCo source lines. Go uses statement profiles, which name a
|
||||||
|
file by its module import path, the way crapper names it. Clojure prefers
|
||||||
Cloverage's per-line form counts, then LCOV. The other languages use LCOV.
|
Cloverage's per-line form counts, then LCOV. The other languages use LCOV.
|
||||||
A file that does not appear in a report has no coverage data.
|
A file that does not appear in a report has no coverage data.
|
||||||
"""
|
"""
|
||||||
@@ -109,13 +110,17 @@ def _best_value(normalized: dict, sources: list[tuple[str, ...]]):
|
|||||||
return best[0][1]
|
return best[0][1]
|
||||||
|
|
||||||
|
|
||||||
def _lookup(index: dict, source_path: str):
|
def _lookup(index: dict, source_path: str, name: str | None = None):
|
||||||
|
"""The report entry for one source file. `name` is the report's own name for it."""
|
||||||
|
|
||||||
if not index:
|
if not index:
|
||||||
return None
|
return None
|
||||||
crapper = ensure_crapper()
|
crapper = ensure_crapper()
|
||||||
normalize = crapper.coverage.normalize_path
|
normalize = crapper.coverage.normalize_path
|
||||||
normalized = {normalize(key): value for key, value in index.items()}
|
normalized = {normalize(key): value for key, value in index.items()}
|
||||||
candidates = _candidates(normalize, source_path)
|
candidates = _candidates(normalize, source_path)
|
||||||
|
if name:
|
||||||
|
candidates.append(name)
|
||||||
found = _exact(normalized, candidates)
|
found = _exact(normalized, candidates)
|
||||||
if found is not None:
|
if found is not None:
|
||||||
return found
|
return found
|
||||||
@@ -194,10 +199,12 @@ def _jacoco_index(root: Path) -> dict[str, set[int]]:
|
|||||||
return found
|
return found
|
||||||
|
|
||||||
|
|
||||||
def _go_lines(profile, source_path: str) -> set[int] | None:
|
def _go_lines(root: Path, profile, source_path: str) -> set[int] | None:
|
||||||
if not profile:
|
if not profile:
|
||||||
return None
|
return None
|
||||||
segments = _lookup(profile, source_path)
|
crapper = ensure_crapper()
|
||||||
|
name = crapper.coverage.go_import_name(Path(source_path), root.resolve())
|
||||||
|
segments = _lookup(profile, source_path, name)
|
||||||
if segments is None:
|
if segments is None:
|
||||||
return None
|
return None
|
||||||
covered: set[int] = set()
|
covered: set[int] = set()
|
||||||
@@ -218,7 +225,7 @@ def covered_lines(root: Path, source_path: Path, language: str) -> set[int] | No
|
|||||||
return _lookup(_jacoco_index(root), path)
|
return _lookup(_jacoco_index(root), path)
|
||||||
bundle = crapper.coverage.load_bundle(root)
|
bundle = crapper.coverage.load_bundle(root)
|
||||||
if language == "go":
|
if language == "go":
|
||||||
return _go_lines(bundle.go_profile, path)
|
return _go_lines(root, bundle.go_profile, path)
|
||||||
if language == "clojure":
|
if language == "clojure":
|
||||||
html = _covered_from_pairs(_lookup(bundle.form_html, path))
|
html = _covered_from_pairs(_lookup(bundle.form_html, path))
|
||||||
if html is not None:
|
if html is not None:
|
||||||
|
|||||||
@@ -96,3 +96,43 @@ def test_go_profile_does_not_cross_files(tmp_path):
|
|||||||
profile.write_text("mode: set\na/b/widget.go:2.1,2.2 1 1\n", encoding="utf-8")
|
profile.write_text("mode: set\na/b/widget.go:2.1,2.2 1 1\n", encoding="utf-8")
|
||||||
assert covered_lines(tmp_path, short, "go") is None
|
assert covered_lines(tmp_path, short, "go") is None
|
||||||
assert covered_lines(tmp_path, long, "go") == {2}
|
assert covered_lines(tmp_path, long, "go") == {2}
|
||||||
|
|
||||||
|
|
||||||
|
def _go_module(module_dir, module, profile_text):
|
||||||
|
module_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
(module_dir / "go.mod").write_text(f"module {module}\n\ngo 1.21\n", encoding="utf-8")
|
||||||
|
profile = module_dir / "target" / "coverage" / "go" / "coverage.out"
|
||||||
|
profile.parent.mkdir(parents=True)
|
||||||
|
profile.write_text(profile_text, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _go_source(path):
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text("package main\nfunc main() {}\n", encoding="utf-8")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def test_go_profile_keyed_by_import_path_marks_hit_lines(tmp_path):
|
||||||
|
# `go test -coverprofile` names files by module import path, not by disk path.
|
||||||
|
_go_module(
|
||||||
|
tmp_path,
|
||||||
|
"github.com/acme/tool",
|
||||||
|
"mode: set\n"
|
||||||
|
"github.com/acme/tool/cmd/tool/main.go:2.1,3.2 1 1\n"
|
||||||
|
"github.com/acme/tool/cmd/tool/main.go:4.1,4.2 1 0\n"
|
||||||
|
"github.com/acme/tool/main.go:7.1,7.2 1 1\n",
|
||||||
|
)
|
||||||
|
nested = _go_source(tmp_path / "cmd" / "tool" / "main.go")
|
||||||
|
top = _go_source(tmp_path / "main.go")
|
||||||
|
assert covered_lines(tmp_path, nested, "go") == {2, 3}
|
||||||
|
assert covered_lines(tmp_path, top, "go") == {7}
|
||||||
|
|
||||||
|
|
||||||
|
def test_go_profile_of_a_module_in_a_subdirectory(tmp_path):
|
||||||
|
_go_module(
|
||||||
|
tmp_path / "svc",
|
||||||
|
"example.com/api",
|
||||||
|
"mode: set\nexample.com/api/calc/calc.go:4.2,6.1 1 1\n",
|
||||||
|
)
|
||||||
|
source = _go_source(tmp_path / "svc" / "calc" / "calc.go")
|
||||||
|
assert covered_lines(tmp_path, source, "go") == {4, 5, 6}
|
||||||
|
|||||||
Reference in New Issue
Block a user