mirror of
https://github.com/unclebob/crapper.git
synced 2026-11-07 10:33:50 +00:00
Fix the coverage, git, and Clojure bugs, and bring CRAP under 10.
A path in a built-in coverage command is one argument, a failed custom coverage command does not read a stale report, and git failures exit with git's status. The Clojure reader honors discards, metadata, and character literals. Long functions are split so every score is under 10 except the option switch.
This commit is contained in:
+156
-49
@@ -1,11 +1,12 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from crapper.cli import _changed_files, _positionals, _take, main, parse_args, run
|
||||
from crapper.discover import language_of
|
||||
from crapper.cli import GitError, _changed_files, _positionals, _take, main, parse_args, run
|
||||
from crapper.discover import is_test_file, language_of
|
||||
|
||||
|
||||
def test_language_detection():
|
||||
@@ -22,6 +23,11 @@ def test_language_detection():
|
||||
assert language_of("src/crapper/cli.py") == "python"
|
||||
assert language_of("types.d.ts") is None
|
||||
assert language_of("notes.md") is None
|
||||
assert language_of("app.cts") == "typescript"
|
||||
assert language_of("app.test.cts") == "typescript"
|
||||
assert is_test_file("app.test.cts") is True
|
||||
assert is_test_file("app.spec.cts") is True
|
||||
assert is_test_file("app.cts") is False
|
||||
|
||||
|
||||
def _write(root: Path, relative: str, source: str) -> None:
|
||||
@@ -30,6 +36,22 @@ def _write(root: Path, relative: str, source: str) -> None:
|
||||
path.write_text(source, encoding="utf-8")
|
||||
|
||||
|
||||
class _Completed:
|
||||
def __init__(self, returncode: int, stdout: str, stderr: str = "") -> None:
|
||||
self.returncode = returncode
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
|
||||
|
||||
def _git_status(root: Path, status: str):
|
||||
def fake(args, **_kwargs):
|
||||
if "rev-parse" in args:
|
||||
return _Completed(0, f"{root.resolve()}\n")
|
||||
return _Completed(0, status)
|
||||
|
||||
return fake
|
||||
|
||||
|
||||
def test_project_snapshot_is_readable_as_crap4clj_edn(tmp_path):
|
||||
_write(
|
||||
tmp_path,
|
||||
@@ -135,7 +157,10 @@ def test_threshold_exits_when_the_worst_score_is_too_high(tmp_path):
|
||||
|
||||
def test_help_and_unknown_option(capsys):
|
||||
assert run(["--help"]) == 0
|
||||
assert "uml-viewer" in capsys.readouterr().out
|
||||
help_text = capsys.readouterr().out
|
||||
assert "uml-viewer" in help_text
|
||||
assert ".clj-kondo" in help_text
|
||||
assert ".test.cts" in help_text
|
||||
assert run(["--not-an-option"]) == 1
|
||||
assert run(["--root"]) == 1
|
||||
assert run(["--threshold", "nope"]) == 1
|
||||
@@ -151,21 +176,21 @@ def test_selects_directories_filters_and_changed_files(tmp_path, monkeypatch, ca
|
||||
assert run(["--root", str(tmp_path), "--no-coverage", "notes.md"]) == 0
|
||||
assert "No source files" in capsys.readouterr().out
|
||||
|
||||
class Result:
|
||||
returncode = 0
|
||||
stdout = ' M src/demo/core.clj\nR old.clj -> src/demo/core.clj\n?? "src/demo/core.clj"\n'
|
||||
stderr = ""
|
||||
|
||||
monkeypatch.setattr("crapper.cli.subprocess.run", lambda *args, **kwargs: Result())
|
||||
monkeypatch.setattr(
|
||||
"crapper.cli.subprocess.run",
|
||||
_git_status(tmp_path, " M src/demo/core.clj\0"),
|
||||
)
|
||||
assert run(["--root", str(tmp_path), "--no-coverage", "--changed"]) == 0
|
||||
assert ':name "choose"' in (tmp_path / ".metrics" / "crap.edn").read_text(encoding="utf-8")
|
||||
|
||||
class Failed:
|
||||
returncode = 1
|
||||
stdout = ""
|
||||
stderr = "git failed"
|
||||
|
||||
monkeypatch.setattr("crapper.cli.subprocess.run", lambda *args, **kwargs: Failed())
|
||||
assert run(["--root", str(tmp_path), "--no-coverage", "--changed"]) == 0
|
||||
monkeypatch.setattr(
|
||||
"crapper.cli.subprocess.run",
|
||||
lambda *_args, **_kwargs: _Completed(128, "", "fatal: not a git repository"),
|
||||
)
|
||||
assert run(["--root", str(tmp_path), "--no-coverage", "--changed"]) == 128
|
||||
captured = capsys.readouterr()
|
||||
assert "not a git repository" in captured.err
|
||||
assert "No source files" not in captured.out
|
||||
|
||||
|
||||
def test_run_asks_for_coverage_when_it_is_enabled(tmp_path, monkeypatch):
|
||||
@@ -201,43 +226,46 @@ def test_flags_that_only_store_a_boolean():
|
||||
|
||||
def test_changed_files_follow_git_status(tmp_path, monkeypatch, capsys):
|
||||
seen = {}
|
||||
named = tmp_path / "x"
|
||||
named.write_text("x", encoding="utf-8")
|
||||
keep = tmp_path / "sources" / "keep.py"
|
||||
keep.parent.mkdir()
|
||||
keep.write_text("x = 1\n", encoding="utf-8")
|
||||
|
||||
class Result:
|
||||
returncode = 0
|
||||
stdout = "\nM x\n?? sources/keep.py\n"
|
||||
stderr = ""
|
||||
|
||||
def fake(*_args, **kwargs):
|
||||
def fake(args, **kwargs):
|
||||
seen["args"] = args
|
||||
seen.update(kwargs)
|
||||
return Result()
|
||||
if "rev-parse" in args:
|
||||
return _Completed(0, f"{tmp_path.resolve()}\n")
|
||||
return _Completed(0, " M x\0?? sources/keep.py\0")
|
||||
|
||||
monkeypatch.setattr("crapper.cli.subprocess.run", fake)
|
||||
found = _changed_files(tmp_path)
|
||||
assert seen["check"] is False
|
||||
assert seen["capture_output"] is True
|
||||
assert seen["text"] is True
|
||||
assert found == [
|
||||
(tmp_path / "x").resolve(),
|
||||
(tmp_path / "sources" / "keep.py").resolve(),
|
||||
]
|
||||
assert seen["encoding"] == "utf-8"
|
||||
assert "-z" in seen["args"]
|
||||
assert "--no-renames" in seen["args"]
|
||||
assert "--untracked-files=all" in seen["args"]
|
||||
assert found == [named.resolve(), keep.resolve()]
|
||||
|
||||
class Failed:
|
||||
returncode = 1
|
||||
stdout = "?? sources/keep.py\n"
|
||||
stderr = "git failed"
|
||||
monkeypatch.setattr(
|
||||
"crapper.cli.subprocess.run",
|
||||
lambda *_a, **_k: _Completed(1, "", "git failed"),
|
||||
)
|
||||
with pytest.raises(GitError, match="git failed") as failed:
|
||||
_changed_files(tmp_path)
|
||||
assert failed.value.code == 1
|
||||
assert capsys.readouterr().err == ""
|
||||
|
||||
monkeypatch.setattr("crapper.cli.subprocess.run", lambda *_a, **_k: Failed())
|
||||
assert _changed_files(tmp_path) == []
|
||||
assert capsys.readouterr().err.strip() == "git failed"
|
||||
|
||||
class Silent:
|
||||
returncode = 1
|
||||
stdout = ""
|
||||
stderr = ""
|
||||
|
||||
monkeypatch.setattr("crapper.cli.subprocess.run", lambda *_a, **_k: Silent())
|
||||
assert _changed_files(tmp_path) == []
|
||||
assert capsys.readouterr().err.strip() == "git status failed"
|
||||
monkeypatch.setattr(
|
||||
"crapper.cli.subprocess.run",
|
||||
lambda *_a, **_k: _Completed(128, "", ""),
|
||||
)
|
||||
with pytest.raises(GitError, match="git status failed") as silent:
|
||||
_changed_files(tmp_path)
|
||||
assert silent.value.code == 128
|
||||
|
||||
|
||||
def test_relative_paths_are_resolved_under_the_project_root(tmp_path):
|
||||
@@ -255,12 +283,10 @@ def test_changed_selection_ignores_files_git_did_not_name(tmp_path, monkeypatch,
|
||||
_write(tmp_path, "src/kept.clj", "(ns demo.kept)\n\n(defn kept [] 1)\n")
|
||||
_write(tmp_path, "src/other.clj", "(ns demo.other)\n\n(defn other [] 1)\n")
|
||||
|
||||
class Result:
|
||||
returncode = 0
|
||||
stdout = "?? src/kept.clj\n"
|
||||
stderr = ""
|
||||
|
||||
monkeypatch.setattr("crapper.cli.subprocess.run", lambda *_a, **_k: Result())
|
||||
monkeypatch.setattr(
|
||||
"crapper.cli.subprocess.run",
|
||||
_git_status(tmp_path, "?? src/kept.clj\0"),
|
||||
)
|
||||
assert run(["--root", str(tmp_path), "--no-coverage", "--changed"]) == 0
|
||||
text = (tmp_path / ".metrics" / "crap.edn").read_text(encoding="utf-8")
|
||||
assert ':name "kept"' in text
|
||||
@@ -284,6 +310,87 @@ def test_threshold_with_no_scores_does_not_call_max_on_an_empty_list(tmp_path):
|
||||
assert run(["--root", str(tmp_path), "--no-coverage", "--threshold", "1"]) == 0
|
||||
|
||||
|
||||
def test_a_failed_coverage_command_ignores_a_stale_report(tmp_path, monkeypatch):
|
||||
_write(tmp_path, "src/demo/core.clj", "(ns demo.core)\n\n(defn choose [x]\n (if x 1 0))\n")
|
||||
lcov = tmp_path / "target" / "coverage" / "lcov.info"
|
||||
lcov.parent.mkdir(parents=True)
|
||||
lcov.write_text("SF:src/demo/core.clj\nDA:3,1\nDA:4,1\nend_of_record\n", encoding="utf-8")
|
||||
monkeypatch.setattr("crapper.cli.run_coverage", lambda *_args, **_kwargs: 3)
|
||||
code = run(
|
||||
["--root", str(tmp_path), "--coverage-command", "exit 3", "src/demo/core.clj"]
|
||||
)
|
||||
assert code == 0
|
||||
text = (tmp_path / ".metrics" / "crap.edn").read_text(encoding="utf-8")
|
||||
assert ":coverage 0.0" in text
|
||||
assert ":crap 6.0" in text
|
||||
|
||||
monkeypatch.setattr("crapper.cli.run_coverage", lambda *_args, **_kwargs: 0)
|
||||
assert run(
|
||||
["--root", str(tmp_path), "--coverage-command", "true", "src/demo/core.clj"]
|
||||
) == 0
|
||||
text = (tmp_path / ".metrics" / "crap.edn").read_text(encoding="utf-8")
|
||||
assert ":coverage 100.0" in text
|
||||
|
||||
|
||||
def test_changed_files_from_a_real_git_status(tmp_path):
|
||||
_git = ["git", "-C", str(tmp_path)]
|
||||
subprocess.run([*_git, "init"], check=True, capture_output=True)
|
||||
subprocess.run([*_git, "config", "user.email", "[email protected]"], check=True)
|
||||
subprocess.run([*_git, "config", "user.name", "t"], check=True)
|
||||
keep = tmp_path / "keep.go"
|
||||
keep.write_text("package demo\nfunc Keep() int { return 1 }\n", encoding="utf-8")
|
||||
subprocess.run([*_git, "add", "keep.go"], check=True)
|
||||
subprocess.run([*_git, "commit", "-m", "init"], check=True, capture_output=True)
|
||||
keep.unlink()
|
||||
cafe = tmp_path / "café.go"
|
||||
cafe.write_text("package demo\nfunc Cafe() int { return 1 }\n", encoding="utf-8")
|
||||
fresh = tmp_path / "fresh"
|
||||
fresh.mkdir()
|
||||
(fresh / "new.go").write_text("package demo\nfunc New() int { return 1 }\n", encoding="utf-8")
|
||||
|
||||
assert run(["--root", str(tmp_path), "--no-coverage", "--changed"]) == 0
|
||||
text = (tmp_path / ".metrics" / "crap.edn").read_text(encoding="utf-8")
|
||||
assert ':name "Cafe"' in text
|
||||
assert ':name "New"' in text
|
||||
assert "Keep" not in text
|
||||
|
||||
pkg = tmp_path / "pkg"
|
||||
pkg.mkdir()
|
||||
(pkg / "inside.go").write_text("package pkg\nfunc Inside() int { return 1 }\n", encoding="utf-8")
|
||||
(tmp_path / "outside.go").write_text(
|
||||
"package demo\nfunc Outside() int { return 1 }\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert run(["--root", str(pkg), "--no-coverage", "--changed"]) == 0
|
||||
nested = (pkg / ".metrics" / "crap.edn").read_text(encoding="utf-8")
|
||||
assert ':name "Inside"' in nested
|
||||
assert "Outside" not in nested
|
||||
assert "Cafe" not in nested
|
||||
assert "New" not in nested
|
||||
|
||||
|
||||
def test_changed_outside_a_repository_returns_gits_status(tmp_path, capsys):
|
||||
_write(tmp_path, "src/app.py", "def run():\n return 1\n")
|
||||
code = run(["--root", str(tmp_path), "--no-coverage", "--changed"])
|
||||
assert code != 0
|
||||
captured = capsys.readouterr()
|
||||
assert "No source files" not in captured.out
|
||||
assert captured.err.strip()
|
||||
|
||||
|
||||
def test_uml_loader_rejects_a_missing_viewer(tmp_path):
|
||||
script = Path(__file__).resolve().parents[1] / "uml"
|
||||
completed = subprocess.run(
|
||||
[str(script)],
|
||||
env={**os.environ, "UML_VIEWER_ROOT": str(tmp_path / "missing")},
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert completed.returncode == 1
|
||||
assert "uml-viewer checkout not found" in completed.stderr
|
||||
|
||||
|
||||
def test_main_exits_with_the_status(monkeypatch):
|
||||
monkeypatch.setattr("crapper.cli.run", lambda _argv=None: 4)
|
||||
try:
|
||||
|
||||
@@ -101,6 +101,68 @@ def test_strings_comments_and_character_literals_stay_inside_one_form():
|
||||
assert [item["name"] for item in extract_functions(source)] == ["foo"]
|
||||
|
||||
|
||||
def test_discards_and_metadata_cover_each_reader_form():
|
||||
source = r"""
|
||||
#_
|
||||
; gone
|
||||
(defn ghost [] (if x 1 0))
|
||||
#_ "string"
|
||||
#_ \x
|
||||
#_ symbol
|
||||
#_ #_(defn inner [] (if x 1 0))
|
||||
#_(defn boxed []
|
||||
"str"
|
||||
\;
|
||||
; comment
|
||||
(if x 1 0))
|
||||
(defn ^{:doc "hi" :private true} kept [] 1)
|
||||
(defn ^{:flag \; :private true} also [] 1)
|
||||
(defn ^{:private true ; note
|
||||
} third [] 1)
|
||||
( defn spaced [] 1)
|
||||
(defn)
|
||||
(defn ^)
|
||||
"""
|
||||
assert [item["name"] for item in extract_functions(source)] == [
|
||||
"kept",
|
||||
"also",
|
||||
"third",
|
||||
"spaced",
|
||||
]
|
||||
assert extract_functions("(defn ^{:private kept [] 1)") == []
|
||||
|
||||
|
||||
def test_character_literals_do_not_swallow_the_next_defn():
|
||||
source = '(def x #{\\"})\n(defn foo [] 1)'
|
||||
assert [item["name"] for item in extract_functions(source)] == ["foo"]
|
||||
|
||||
|
||||
def test_reader_keeps_the_real_namespace_name_and_decisions():
|
||||
source = r""";; (ns demo.wrong)
|
||||
(ns demo.core)
|
||||
#_(defn dropped [x]
|
||||
(if x 1 0))
|
||||
(defn ^:private kept [x]
|
||||
#_(if x 1 0)
|
||||
1)
|
||||
(defn ^{:private true} also [] 1)
|
||||
(defn quoted []
|
||||
\"
|
||||
(if x 1 0))
|
||||
(defn semi []
|
||||
\;
|
||||
(if true 1 2))
|
||||
"""
|
||||
found = functions_in_source(source, "src/demo/core.clj", "/proj")
|
||||
assert [(item.name, item.complexity) for item in found] == [
|
||||
("kept", 1),
|
||||
("also", 1),
|
||||
("quoted", 2),
|
||||
("semi", 2),
|
||||
]
|
||||
assert {item.namespace for item in found} == {"demo.core"}
|
||||
assert cyclomatic_complexity(r'(defn semi [] \; (if true 1 2))') == 2
|
||||
assert cyclomatic_complexity(r'(defn quoted [] \" (if x 1 0))') == 2
|
||||
assert extract_functions("#_(defn dropped [] (if x 1 0))\n(defn kept [] 1)\n") == [
|
||||
{"name": "kept", "start_line": 2, "end_line": 2, "complexity": 1}
|
||||
]
|
||||
|
||||
@@ -123,6 +123,52 @@ def test_go_coverage_is_read_from_the_bundle():
|
||||
assert bundle.percent_for(function()) == 100.0
|
||||
|
||||
|
||||
def test_the_most_specific_prefix_wins_and_a_tie_matches_nothing():
|
||||
specific = parse_go_profile(
|
||||
"mode: set\n"
|
||||
"other/proj/src/a.go:1.1,2.2 1 0\n"
|
||||
"proj/src/a.go:1.1,2.2 1 1\n"
|
||||
)
|
||||
assert go_percent(specific, "src/a.go", 1, 2) == 100.0
|
||||
tied = parse_go_profile(
|
||||
"mode: set\nproj/a.go:1.1,2.2 1 1\nother/a.go:1.1,2.2 1 0\n"
|
||||
)
|
||||
assert go_percent(tied, "a.go", 1, 2) is None
|
||||
assert go_percent(specific, "/", 1, 2) is None
|
||||
|
||||
|
||||
def test_exact_profile_key_wins_over_a_suffix():
|
||||
profile = parse_go_profile(
|
||||
"mode: set\n"
|
||||
"a/b/main.go:1.1,2.2 1 1\n"
|
||||
"b/main.go:1.1,2.2 1 0\n"
|
||||
)
|
||||
assert go_percent(profile, "b/main.go", 1, 2) == 0.0
|
||||
assert go_percent(profile, "a/b/main.go", 1, 2) == 100.0
|
||||
|
||||
|
||||
def test_a_shorter_file_does_not_take_a_longer_files_coverage():
|
||||
bundle = CoverageBundle(
|
||||
go_profile=parse_go_profile("mode: set\na/b/main.go:1.1,2.2 1 1\n")
|
||||
)
|
||||
bundle.bind_sources(["b/main.go", "a/b/main.go"])
|
||||
short = function(path="b/main.go", start_line=1, end_line=2)
|
||||
long = function(path="a/b/main.go", start_line=1, end_line=2)
|
||||
assert bundle.percent_for(short) is None
|
||||
assert bundle.percent_for(long) == 100.0
|
||||
|
||||
|
||||
def test_a_short_report_key_matches_one_longer_source():
|
||||
bundle = CoverageBundle(
|
||||
go_profile=parse_go_profile("mode: set\nboard.go:4.1,6.2 2 1\n")
|
||||
)
|
||||
bundle.bind_sources(["cmd/board.go"])
|
||||
assert bundle.percent_for(function(path="cmd/board.go")) == 100.0
|
||||
bundle.bind_sources(["cmd/board.go", "other/board.go"])
|
||||
assert bundle.percent_for(function(path="cmd/board.go")) is None
|
||||
assert bundle.percent_for(function(path="other/board.go")) is None
|
||||
|
||||
|
||||
def test_suffix_lookup_finds_a_report_keyed_by_a_longer_path():
|
||||
bundle = CoverageBundle(lcov={"proj/src/demo/core.clj": {3: (1, 1), 4: (0, 1)}})
|
||||
fn = function(
|
||||
@@ -264,6 +310,35 @@ def test_python_function_without_a_name_is_skipped():
|
||||
assert [fn.name for fn in python_functions(source, "src/app.py", "/proj")] == ["choose"]
|
||||
|
||||
|
||||
def test_a_deep_expression_does_not_recurse_off_the_stack():
|
||||
expr = " + ".join(["1"] * 1200)
|
||||
python = python_functions(f"def huge():\n return {expr}\n", "huge.py", None)
|
||||
assert [fn.name for fn in python] == ["huge"]
|
||||
java = java_functions(
|
||||
f"class Huge {{\n int huge() {{ return {expr}; }}\n}}\n",
|
||||
"Huge.java",
|
||||
)
|
||||
assert [fn.name for fn in java] == ["huge"]
|
||||
|
||||
|
||||
def test_analyze_does_not_give_a_short_path_the_longer_files_coverage(tmp_path):
|
||||
body = "package demo\nfunc Place() int { return 1 }\n"
|
||||
short = tmp_path / "b" / "main.go"
|
||||
long = tmp_path / "a" / "b" / "main.go"
|
||||
short.parent.mkdir(parents=True)
|
||||
long.parent.mkdir(parents=True)
|
||||
short.write_text(body, encoding="utf-8")
|
||||
long.write_text(body, encoding="utf-8")
|
||||
bundle = CoverageBundle(
|
||||
go_profile=parse_go_profile("mode: set\na/b/main.go:1.1,2.2 1 1\n")
|
||||
)
|
||||
entries = analyze_files([short, long], tmp_path, bundle)
|
||||
assert [(entry.name, entry.path, entry.coverage) for entry in entries] == [
|
||||
("Place", "b/main.go", 0.0),
|
||||
("Place", "a/b/main.go", 100.0),
|
||||
]
|
||||
|
||||
|
||||
def test_rust_relative_path_and_a_trait_method(tmp_path):
|
||||
(tmp_path / "Cargo.toml").write_text(
|
||||
'[package]\nname = "demo"\nversion = "0.1.0"\n',
|
||||
|
||||
+140
-29
@@ -1,8 +1,11 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from crapper.discover import is_test_file
|
||||
from crapper.runners import (
|
||||
PackageJsonError,
|
||||
_clean_clojure,
|
||||
_clean_dir,
|
||||
_coverage_report,
|
||||
@@ -29,6 +32,12 @@ from crapper.runners import (
|
||||
)
|
||||
|
||||
|
||||
def _as_text(command) -> str:
|
||||
if isinstance(command, list):
|
||||
return " ".join(command)
|
||||
return command
|
||||
|
||||
|
||||
def _write(root: Path, relative: str, source: str) -> Path:
|
||||
path = root / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -48,12 +57,13 @@ def test_vitest_uses_its_own_coverage_instead_of_c8(tmp_path):
|
||||
report_dir = tmp_path / "target" / "coverage" / "typescript"
|
||||
command = typescript_command(tmp_path, [source, tmp_path / "src" / "book.test.ts"], report_dir)
|
||||
assert command is not None
|
||||
assert "c8" not in command
|
||||
assert "vitest run --coverage" in command
|
||||
assert "--coverage.reporter=lcov" in command
|
||||
assert f"--coverage.reportsDirectory={report_dir}" in command
|
||||
assert "--coverage.include=src/book.ts" in command
|
||||
assert "book.test.ts" not in command
|
||||
text = _as_text(command)
|
||||
assert "c8" not in text
|
||||
assert "vitest run --coverage" in text
|
||||
assert "--coverage.reporter=lcov" in text
|
||||
assert f"--coverage.reportsDirectory={report_dir}" in text
|
||||
assert "--coverage.include=src/book.ts" in text
|
||||
assert "book.test.ts" not in text
|
||||
|
||||
|
||||
def test_existing_coverage_script_is_left_alone(tmp_path):
|
||||
@@ -63,14 +73,23 @@ def test_existing_coverage_script_is_left_alone(tmp_path):
|
||||
json.dumps({"scripts": {"coverage": "vitest run --coverage", "test": "vitest run"}}),
|
||||
)
|
||||
command = typescript_command(tmp_path, [], tmp_path / "coverage")
|
||||
assert command == "npm run coverage"
|
||||
assert command == ["npm", "run", "coverage"]
|
||||
|
||||
|
||||
def test_node_test_script_still_uses_c8(tmp_path):
|
||||
_write(tmp_path, "package.json", json.dumps({"scripts": {"test": "node --test"}}))
|
||||
report_dir = tmp_path / "target" / "coverage" / "typescript"
|
||||
command = typescript_command(tmp_path, [], report_dir)
|
||||
assert command == f"npx --yes c8 --reporter=lcov --reports-dir {report_dir} npm test"
|
||||
assert command == [
|
||||
"npx",
|
||||
"--yes",
|
||||
"c8",
|
||||
"--reporter=lcov",
|
||||
"--reports-dir",
|
||||
str(report_dir),
|
||||
"npm",
|
||||
"test",
|
||||
]
|
||||
|
||||
|
||||
def test_typescript_package_is_found_from_a_source_file(tmp_path):
|
||||
@@ -112,10 +131,25 @@ def test_python_project_uses_pytest_and_coverage_lcov(tmp_path):
|
||||
data = tmp_path / "target" / "coverage" / "python" / ".coverage"
|
||||
report = data.parent / "lcov.info"
|
||||
run, lcov = python_coverage_commands("python3", "pytest", data, report, "src")
|
||||
assert run == (
|
||||
f"python3 -m coverage run --data-file={data} --source=src -m pytest"
|
||||
)
|
||||
assert lcov == f"python3 -m coverage lcov --data-file={data} -o {report}"
|
||||
assert run == [
|
||||
"python3",
|
||||
"-m",
|
||||
"coverage",
|
||||
"run",
|
||||
f"--data-file={data}",
|
||||
"--source=src",
|
||||
"-m",
|
||||
"pytest",
|
||||
]
|
||||
assert lcov == [
|
||||
"python3",
|
||||
"-m",
|
||||
"coverage",
|
||||
"lcov",
|
||||
f"--data-file={data}",
|
||||
"-o",
|
||||
str(report),
|
||||
]
|
||||
|
||||
|
||||
def test_python_package_without_pytest_uses_unittest(tmp_path):
|
||||
@@ -123,20 +157,21 @@ def test_python_package_without_pytest_uses_unittest(tmp_path):
|
||||
source = _write(tmp_path, "demo/app.py", "def run():\n return 1\n")
|
||||
assert not uses_pytest(tmp_path)
|
||||
run, _lcov = python_coverage_commands("python3", "unittest", Path("data"), Path("out"), "demo")
|
||||
assert "-m unittest discover -s ." in run
|
||||
assert _as_text(run).endswith("-m unittest discover -s .")
|
||||
assert python_roots(tmp_path, [source]) == [tmp_path.resolve()]
|
||||
|
||||
|
||||
def test_rust_lcov_path_is_absolute():
|
||||
report = Path("/tmp/bookwriter/target/coverage/rust/src-tauri/lcov.info")
|
||||
command = rust_coverage_command("llvm-cov", report)
|
||||
assert command == f"cargo llvm-cov --lcov --output-path {report}"
|
||||
assert command == ["cargo", "llvm-cov", "--lcov", "--output-path", str(report)]
|
||||
|
||||
|
||||
def test_vitest_version_and_provider_install(tmp_path, monkeypatch):
|
||||
def test_vitest_version_and_provider_install(tmp_path, monkeypatch, capsys):
|
||||
assert _vitest_version(tmp_path) is None
|
||||
_write(tmp_path, "node_modules/vitest/package.json", "{not json")
|
||||
assert _vitest_version(tmp_path) is None
|
||||
assert "package.json" in capsys.readouterr().err
|
||||
_write(tmp_path, "node_modules/vitest/package.json", '{"version": "4.1.11"}\n')
|
||||
assert _vitest_version(tmp_path) == "4.1.11"
|
||||
provider = tmp_path / "node_modules" / "@vitest" / "coverage-v8"
|
||||
@@ -176,7 +211,7 @@ def test_rust_kind_installs_llvm_cov_when_it_is_missing(monkeypatch):
|
||||
return None
|
||||
|
||||
def shell(command, cwd):
|
||||
if "cargo install" in command:
|
||||
if "cargo install" in _as_text(command):
|
||||
installed["llvm"] = True
|
||||
return 0
|
||||
|
||||
@@ -189,18 +224,19 @@ def test_rust_kind_installs_llvm_cov_when_it_is_missing(monkeypatch):
|
||||
|
||||
|
||||
def test_coverage_commands_are_issued_per_language(tmp_path, monkeypatch):
|
||||
commands: list[str] = []
|
||||
commands: list[str | list[str]] = []
|
||||
|
||||
def shell(command, cwd):
|
||||
commands.append(command)
|
||||
if "--data-file=" in command:
|
||||
data = Path(command.split("--data-file=", 1)[1].split()[0])
|
||||
text = _as_text(command)
|
||||
if "--data-file=" in text:
|
||||
data = Path(text.split("--data-file=", 1)[1].split()[0])
|
||||
data.parent.mkdir(parents=True, exist_ok=True)
|
||||
data.write_text("x", encoding="utf-8")
|
||||
return 1
|
||||
if command.startswith("clj") or command.startswith("mvn") or command.startswith("go "):
|
||||
if text.startswith("clj") or text.startswith("mvn") or text.startswith("go "):
|
||||
return 1
|
||||
if "vitest" in command or command.startswith("cargo "):
|
||||
if "vitest" in text or text.startswith("cargo "):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
@@ -236,7 +272,7 @@ def test_coverage_commands_are_issued_per_language(tmp_path, monkeypatch):
|
||||
tmp_path / "src/lib.rs",
|
||||
]
|
||||
run_coverage(tmp_path, files, None)
|
||||
text = "\n".join(commands)
|
||||
text = "\n".join(_as_text(command) for command in commands)
|
||||
assert "clj -M:cov --lcov" in text
|
||||
assert "clj -M:cov\n" in text or text.endswith("clj -M:cov") or "clj -M:cov" in text
|
||||
assert "mvn " in text
|
||||
@@ -271,13 +307,22 @@ def test_run_shell_returns_the_child_status_and_reports_a_failed_start(tmp_path,
|
||||
|
||||
|
||||
def test_clean_removes_a_stale_directory_and_ignores_a_missing_one(tmp_path):
|
||||
stale = tmp_path / "target" / "coverage" / "old"
|
||||
coverage = tmp_path / "target" / "coverage"
|
||||
stale = coverage / "old"
|
||||
stale.mkdir(parents=True)
|
||||
(stale / "lcov.info").write_text("x", encoding="utf-8")
|
||||
kept = tmp_path / "target" / "coverage" / "python"
|
||||
(stale / "index.html").write_text("x", encoding="utf-8")
|
||||
(coverage / "notes.txt").write_text("keep", encoding="utf-8")
|
||||
nested = coverage / "notes"
|
||||
nested.mkdir()
|
||||
(nested / "keep.txt").write_text("x", encoding="utf-8")
|
||||
kept = coverage / "python"
|
||||
kept.mkdir()
|
||||
(kept / "index.html").write_text("py", encoding="utf-8")
|
||||
_clean_clojure(tmp_path)
|
||||
assert not stale.exists()
|
||||
assert (coverage / "notes.txt").is_file()
|
||||
assert (nested / "keep.txt").is_file()
|
||||
assert (kept / "index.html").is_file()
|
||||
assert kept.is_dir()
|
||||
_clean_dir(tmp_path / "target" / "coverage")
|
||||
assert not (tmp_path / "target" / "coverage").exists()
|
||||
@@ -288,9 +333,11 @@ def test_clean_removes_a_stale_directory_and_ignores_a_missing_one(tmp_path):
|
||||
def test_package_json_without_a_test_script_has_no_coverage_command(tmp_path):
|
||||
assert typescript_command(tmp_path, [], tmp_path / "cov") is None
|
||||
_write(tmp_path, "package.json", "{")
|
||||
assert typescript_command(tmp_path, [], tmp_path / "cov") is None
|
||||
with pytest.raises(PackageJsonError, match="package.json"):
|
||||
typescript_command(tmp_path, [], tmp_path / "cov")
|
||||
_write(tmp_path, "package.json", "[]")
|
||||
assert typescript_command(tmp_path, [], tmp_path / "cov") is None
|
||||
with pytest.raises(PackageJsonError, match="expected a JSON object"):
|
||||
typescript_command(tmp_path, [], tmp_path / "cov")
|
||||
_write(tmp_path, "package.json", '{"scripts": ["nope"]}')
|
||||
assert typescript_command(tmp_path, [], tmp_path / "cov") is None
|
||||
|
||||
@@ -344,13 +391,13 @@ def test_missing_python_module_is_installed(tmp_path, monkeypatch):
|
||||
|
||||
def shell(command, _cwd):
|
||||
commands.append(command)
|
||||
if command.startswith("python3 -c"):
|
||||
if _as_text(command).startswith("python3 -c"):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
monkeypatch.setattr("crapper.runners.run_shell", shell)
|
||||
assert _ensure_python_module("python3", tmp_path, "coverage") is True
|
||||
assert any("pip install" in command for command in commands)
|
||||
assert any("pip install" in _as_text(command) for command in commands)
|
||||
commands.clear()
|
||||
|
||||
def already(_command, _cwd):
|
||||
@@ -431,3 +478,67 @@ def test_python_coverage_module_missing_is_reported(tmp_path, monkeypatch, capsy
|
||||
source = _write(tmp_path, "src/app.py", "def run():\n return 1\n")
|
||||
run_coverage(tmp_path, [source], None)
|
||||
assert "coverage is missing" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_an_argument_vector_is_not_a_shell(tmp_path, monkeypatch):
|
||||
seen = []
|
||||
|
||||
def fake(command, cwd, shell=False):
|
||||
seen.append((command, shell))
|
||||
|
||||
class Done:
|
||||
returncode = 0
|
||||
|
||||
return Done()
|
||||
|
||||
monkeypatch.setattr("crapper.runners.subprocess.run", fake)
|
||||
assert run_shell("exit 3", tmp_path) == 0
|
||||
marker = tmp_path / "pwned"
|
||||
assert run_shell(["true", f"a;touch {marker}"], tmp_path) == 0
|
||||
assert seen[0] == ("exit 3", True)
|
||||
assert seen[1][1] is False
|
||||
assert seen[1][0] == ["true", f"a;touch {marker}"]
|
||||
|
||||
|
||||
def test_an_argument_vector_cannot_run_a_second_command(tmp_path):
|
||||
marker = tmp_path / "pwned"
|
||||
assert run_shell(["true", f"a;touch {marker}"], tmp_path) == 0
|
||||
assert not marker.exists()
|
||||
|
||||
|
||||
def test_go_coverprofile_stays_one_argument(tmp_path, monkeypatch):
|
||||
root = tmp_path / "my module"
|
||||
root.mkdir()
|
||||
_write(root, "go.mod", "module example.com/demo\n")
|
||||
source = _write(root, "board.go", "package demo\nfunc Place() int { return 1 }\n")
|
||||
seen = []
|
||||
monkeypatch.setattr("crapper.runners.run_shell", lambda command, cwd: seen.append(command) or 0)
|
||||
run_coverage(root, [source], None)
|
||||
command = seen[0]
|
||||
assert command[:3] == ["go", "test", "./..."]
|
||||
assert command[3].startswith("-coverprofile=")
|
||||
assert " " in command[3]
|
||||
assert len(command) == 4
|
||||
|
||||
|
||||
def test_invalid_package_json_is_not_a_missing_test_script(tmp_path, monkeypatch, capsys):
|
||||
monkeypatch.setattr("crapper.runners.run_shell", lambda *_args: 0)
|
||||
_write(tmp_path, "package.json", "{ not json")
|
||||
source = _write(tmp_path, "src/app.ts", "export const n = 1\n")
|
||||
run_coverage(tmp_path, [source], None)
|
||||
err = capsys.readouterr().err
|
||||
assert "package.json" in err
|
||||
assert "No package.json test script" not in err
|
||||
|
||||
|
||||
def test_vitest_install_restores_manifests_when_install_changes_them(tmp_path, monkeypatch):
|
||||
package_json = tmp_path / "package.json"
|
||||
package_json.write_bytes(b'{"name":"demo"}\n')
|
||||
|
||||
def shell(command, cwd):
|
||||
package_json.write_bytes(b'{"name":"changed"}\n')
|
||||
return 1
|
||||
|
||||
monkeypatch.setattr("crapper.runners.run_shell", shell)
|
||||
assert _ensure_vitest_coverage(tmp_path) is False
|
||||
assert package_json.read_bytes() == b'{"name":"demo"}\n'
|
||||
|
||||
+7
-1
@@ -1,4 +1,6 @@
|
||||
from crapper.languages.rust import functions_in_source
|
||||
from pathlib import Path
|
||||
|
||||
from crapper.languages.rust import _file_modules, functions_in_source
|
||||
|
||||
SOURCE = """
|
||||
pub fn choose(x: i32) -> i32 {
|
||||
@@ -80,6 +82,10 @@ def test_module_paths_outside_the_usual_layout(tmp_path):
|
||||
assert bare[0].name == "bare"
|
||||
|
||||
|
||||
def test_a_file_outside_the_crate_root_uses_the_crate_name():
|
||||
assert _file_modules("/tmp/loose.rs", "demo", Path("/projects/demo")) == ["demo"]
|
||||
|
||||
|
||||
def test_nested_module_path(tmp_path):
|
||||
(tmp_path / "Cargo.toml").write_text(
|
||||
'[package]\nname = "demo"\nversion = "0.1.0"\n',
|
||||
|
||||
Reference in New Issue
Block a user