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:
Robert C. Martin
2026-10-03 17:27:59 -05:00
parent f0cb973110
commit 9f1bead298
20 changed files with 1287 additions and 513 deletions
+156 -49
View File
@@ -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:
+62
View File
@@ -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}
]
+75
View File
@@ -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
View File
@@ -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
View File
@@ -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',