mirror of
https://github.com/unclebob/crapper.git
synced 2026-11-06 18:13:50 +00:00
Add tests for the branches the first mutation run left alive.
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
"""Keep a mutant from launching coverage against the project under test.
|
||||
|
||||
`run()` falls through into `run_coverage` when a mutant skips the help return.
|
||||
That would delete `target/coverage` and recurse into this suite.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def do_not_launch_project_coverage(monkeypatch):
|
||||
monkeypatch.setattr("crapper.cli.run_coverage", lambda *_args, **_kwargs: None)
|
||||
+125
-2
@@ -1,7 +1,10 @@
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from crapper.cli import run
|
||||
import pytest
|
||||
|
||||
from crapper.cli import _changed_files, _positionals, _take, main, parse_args, run
|
||||
from crapper.discover import language_of
|
||||
|
||||
|
||||
@@ -163,5 +166,125 @@ def test_selects_directories_filters_and_changed_files(tmp_path, monkeypatch, ca
|
||||
|
||||
def test_run_asks_for_coverage_when_it_is_enabled(tmp_path, monkeypatch):
|
||||
_write(tmp_path, "src/demo/core.clj", "(ns demo.core)\n\n(defn choose [x]\n x)\n")
|
||||
monkeypatch.setattr("crapper.cli.run_coverage", lambda *args: None)
|
||||
called = []
|
||||
monkeypatch.setattr("crapper.cli.run_coverage", lambda *args: called.append(args))
|
||||
assert run(["--root", str(tmp_path), "--coverage-command", "true"]) == 0
|
||||
assert called
|
||||
|
||||
|
||||
def test_empty_option_value_is_rejected():
|
||||
with pytest.raises(ValueError, match="requires a value"):
|
||||
_take(["ok", "--opt", ""], 1, "--opt")
|
||||
with pytest.raises(ValueError, match="requires a value"):
|
||||
_take(["--opt", "-1"], 0, "--opt")
|
||||
|
||||
|
||||
def test_parse_args_reads_the_process_arguments_after_the_program(monkeypatch):
|
||||
monkeypatch.setattr(sys, "argv", ["crapper", "--threshold", "5"])
|
||||
options = parse_args()
|
||||
assert options.threshold == 5.0
|
||||
assert options.positionals == []
|
||||
assert options.action == "analyze"
|
||||
|
||||
|
||||
def test_flags_that_only_store_a_boolean():
|
||||
assert parse_args(["--use-existing-coverage"]).use_existing_coverage is True
|
||||
assert parse_args(["--changed"]).changed is True
|
||||
assert parse_args(["--no-coverage"]).no_coverage is True
|
||||
conflict = parse_args(["--no-coverage", "--coverage-command", "true"])
|
||||
assert conflict.exit_code == 1
|
||||
|
||||
|
||||
def test_changed_files_follow_git_status(tmp_path, monkeypatch, capsys):
|
||||
seen = {}
|
||||
|
||||
class Result:
|
||||
returncode = 0
|
||||
stdout = "\nM x\n?? sources/keep.py\n"
|
||||
stderr = ""
|
||||
|
||||
def fake(*_args, **kwargs):
|
||||
seen.update(kwargs)
|
||||
return Result()
|
||||
|
||||
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(),
|
||||
]
|
||||
|
||||
class Failed:
|
||||
returncode = 1
|
||||
stdout = "?? sources/keep.py\n"
|
||||
stderr = "git failed"
|
||||
|
||||
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"
|
||||
|
||||
|
||||
def test_relative_paths_are_resolved_under_the_project_root(tmp_path):
|
||||
_write(tmp_path, "src/demo/core.clj", "(ns demo.core)\n\n(defn choose [x]\n (if x 1 0))\n")
|
||||
existing, filters = _positionals(tmp_path, ["src/demo/core.clj"])
|
||||
assert existing == [(tmp_path / "src/demo/core.clj").resolve()]
|
||||
assert filters == []
|
||||
assert run(["--root", str(tmp_path), "--no-coverage", "src/demo/core.clj"]) == 0
|
||||
text = (tmp_path / ".metrics" / "crap.edn").read_text(encoding="utf-8")
|
||||
assert ':name "choose"' in text
|
||||
assert "parse_args" not in text
|
||||
|
||||
|
||||
def test_changed_selection_ignores_files_git_did_not_name(tmp_path, monkeypatch, capsys):
|
||||
_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())
|
||||
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
|
||||
assert "other" not in text
|
||||
assert "No source files" not in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_threshold_equal_to_the_score_is_allowed(tmp_path):
|
||||
_write(tmp_path, "src/demo/core.clj", "(ns demo.core)\n\n(defn choose [] 1)\n")
|
||||
lcov = tmp_path / "target" / "coverage" / "lcov.info"
|
||||
lcov.parent.mkdir(parents=True)
|
||||
lcov.write_text("SF:src/demo/core.clj\nDA:1,1\nDA:3,1\nend_of_record\n", encoding="utf-8")
|
||||
code = run(
|
||||
["--root", str(tmp_path), "--use-existing-coverage", "--threshold", "1", "src/demo/core.clj"]
|
||||
)
|
||||
assert code == 0
|
||||
|
||||
|
||||
def test_threshold_with_no_scores_does_not_call_max_on_an_empty_list(tmp_path):
|
||||
_write(tmp_path, "src/demo/core.clj", "(ns demo.core)\n\n(defn choose [] 1)\n")
|
||||
assert run(["--root", str(tmp_path), "--no-coverage", "--threshold", "1"]) == 0
|
||||
|
||||
|
||||
def test_main_exits_with_the_status(monkeypatch):
|
||||
monkeypatch.setattr("crapper.cli.run", lambda _argv=None: 4)
|
||||
try:
|
||||
main(["--help"])
|
||||
except SystemExit as exc:
|
||||
assert exc.code == 4
|
||||
else:
|
||||
raise AssertionError("main did not exit")
|
||||
|
||||
@@ -37,6 +37,16 @@ def test_unscored_rows_sort_after_scored_rows():
|
||||
assert [row.name for row in rows] == ["worst", "mid", "missing"]
|
||||
|
||||
|
||||
def test_a_low_score_still_sorts_ahead_of_an_unscored_row():
|
||||
rows = sort_entries(
|
||||
[
|
||||
entry(name="plain", crap=1.0, complexity=1),
|
||||
entry(name="large", crap=None, coverage=None, complexity=8),
|
||||
]
|
||||
)
|
||||
assert [row.name for row in rows] == ["plain", "large"]
|
||||
|
||||
|
||||
def test_unscored_rows_sort_by_complexity():
|
||||
rows = sort_entries(
|
||||
[
|
||||
@@ -67,6 +77,20 @@ def test_edn_uses_crap4clj_keys():
|
||||
assert text.endswith("}\n")
|
||||
|
||||
|
||||
def test_edn_of_no_entries_is_an_empty_vector():
|
||||
assert render_edn([]) == "{:entries []}\n"
|
||||
|
||||
|
||||
def test_metrics_directory_can_be_written_twice(tmp_path):
|
||||
from crapper.metrics import write_metrics
|
||||
|
||||
entry_row = entry()
|
||||
first = write_metrics([entry_row], tmp_path)
|
||||
second = write_metrics([entry_row], tmp_path)
|
||||
assert first == second
|
||||
assert first.is_file()
|
||||
|
||||
|
||||
def test_edn_writes_nil_for_missing_coverage():
|
||||
text = render_edn([entry(coverage=None, crap=None)])
|
||||
assert ":coverage nil" in text
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
"""Branches the first mutation run left unexecuted."""
|
||||
|
||||
import runpy
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from crapper.analyze import analyze_files
|
||||
from crapper.coverage import (
|
||||
CoverageBundle,
|
||||
coverage_percent,
|
||||
go_percent,
|
||||
load_bundle,
|
||||
normalize_path,
|
||||
parse_form_coverage,
|
||||
parse_go_profile,
|
||||
parse_jacoco_index,
|
||||
parse_lcov,
|
||||
percent_for_range,
|
||||
suffix_match,
|
||||
)
|
||||
from crapper.discover import is_test_file, iter_source_files
|
||||
from crapper.languages.clojure import _extract_top_level_defns, extract_functions
|
||||
from crapper.languages.golang import functions_in_source as go_functions
|
||||
from crapper.languages.java import functions_in_source as java_functions
|
||||
from crapper.languages.python import functions_in_source as python_functions
|
||||
from crapper.languages.rust import functions_in_source as rust_functions
|
||||
from crapper.languages.treesitter import child_of_type, end_line, parse
|
||||
from crapper.languages.typescript import functions_in_source as ts_functions
|
||||
from crapper.model import Function
|
||||
|
||||
|
||||
def function(**kwargs):
|
||||
defaults = dict(
|
||||
name="place",
|
||||
namespace="demo.Board",
|
||||
complexity=1,
|
||||
start_line=4,
|
||||
end_line=6,
|
||||
path="board.go",
|
||||
language="go",
|
||||
jacoco_class=None,
|
||||
)
|
||||
defaults.update(kwargs)
|
||||
return Function(**defaults)
|
||||
|
||||
|
||||
def test_normalize_path_strips_url_and_dot_prefixes():
|
||||
assert normalize_path("file:./src//demo/core.clj") == "src/demo/core.clj"
|
||||
assert normalize_path("%2E%2Fsrc%2Fcore.clj") == "src/core.clj"
|
||||
|
||||
|
||||
def test_suffix_match_rejects_a_longer_suffix():
|
||||
assert suffix_match("src/demo/core.clj", "demo/core.clj") is True
|
||||
assert suffix_match("core.clj", "src/demo/core.clj") is False
|
||||
assert suffix_match("src/demo/core.clj", "") is False
|
||||
|
||||
|
||||
def test_coverage_percent_is_zero_when_there_is_nothing_to_count():
|
||||
assert coverage_percent(0, 0) == 0.0
|
||||
assert percent_for_range({}, 1, 3) == 0.0
|
||||
assert percent_for_range({1: (1, 1)}, 4, 5) == 0.0
|
||||
|
||||
|
||||
def test_form_coverage_ignores_a_span_with_no_forms():
|
||||
html = '<span title="0 out of 0 forms covered"> 4 </span>'
|
||||
assert parse_form_coverage(html) == {}
|
||||
|
||||
|
||||
def test_lcov_keeps_a_file_that_has_no_end_record():
|
||||
parsed = parse_lcov("SF:a.clj\nDA:1,1\nSF:b.clj\nDA:2,0\n")
|
||||
assert parsed["a.clj"][1] == (1, 1)
|
||||
assert parsed["b.clj"][2] == (0, 1)
|
||||
|
||||
|
||||
def test_go_profile_rejects_a_broken_line():
|
||||
with pytest.raises(ValueError, match="invalid coverage segment"):
|
||||
parse_go_profile("mode: set\nnot a segment\n")
|
||||
|
||||
|
||||
def test_go_percent_is_zero_when_no_segment_overlaps_the_function():
|
||||
profile = parse_go_profile("mode: set\nboard.go:1.1,2.2 1 1\n")
|
||||
assert go_percent(profile, "board.go", 10, 12) == 0.0
|
||||
|
||||
|
||||
def test_go_percent_does_not_accept_the_first_unrelated_file():
|
||||
profile = parse_go_profile(
|
||||
"mode: set\nother.go:4.1,6.2 2 0\nboard.go:4.1,6.2 2 1\n"
|
||||
)
|
||||
assert go_percent(profile, "board.go", 4, 6) == 100.0
|
||||
|
||||
|
||||
def test_jacoco_miss_is_none_and_a_missing_counter_field_is_zero():
|
||||
xml = """<report><class name="demo/Board">
|
||||
<method name="place" line="4">
|
||||
<counter type="INSTRUCTION" covered="4"/>
|
||||
</method>
|
||||
</class></report>"""
|
||||
found = parse_jacoco_index(xml)
|
||||
assert found["demo.Board#place"][0].missed == 0
|
||||
assert found["demo.Board#place"][0].percent == 100.0
|
||||
bundle = CoverageBundle(jacoco=found)
|
||||
assert bundle.percent_for(function(language="java", name="missing", namespace="demo.Board")) is None
|
||||
assert CoverageBundle().percent_for(function(language="java", namespace="demo.Board")) is None
|
||||
|
||||
|
||||
def test_jacoco_uses_the_nearest_method_when_the_line_does_not_match():
|
||||
xml = """<report><class name="demo/Board">
|
||||
<method name="place" line="4">
|
||||
<counter type="INSTRUCTION" missed="0" covered="4"/>
|
||||
</method>
|
||||
<method name="place" line="20">
|
||||
<counter type="INSTRUCTION" missed="4" covered="0"/>
|
||||
</method>
|
||||
</class></report>"""
|
||||
bundle = CoverageBundle(jacoco=parse_jacoco_index(xml))
|
||||
assert bundle.percent_for(function(language="java", name="place", namespace="demo.Board", start_line=18)) == 0.0
|
||||
|
||||
|
||||
def test_go_coverage_is_read_from_the_bundle():
|
||||
profile = parse_go_profile("mode: set\nboard.go:4.1,6.2 2 1\n")
|
||||
bundle = CoverageBundle(go_profile=profile)
|
||||
assert bundle.percent_for(function()) == 100.0
|
||||
|
||||
|
||||
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(
|
||||
language="clojure",
|
||||
path="src/demo/core.clj",
|
||||
start_line=3,
|
||||
end_line=4,
|
||||
namespace="demo.core",
|
||||
name="choose",
|
||||
)
|
||||
assert bundle.percent_for(fn) == 50.0
|
||||
|
||||
|
||||
def test_load_bundle_on_an_empty_tree_has_no_profiles(tmp_path):
|
||||
bundle = load_bundle(tmp_path)
|
||||
assert bundle.go_profile is None
|
||||
assert bundle.jacoco is None
|
||||
assert bundle.lcov == {}
|
||||
|
||||
|
||||
def test_conftest_and_a_tests_directory_are_tests(tmp_path):
|
||||
assert is_test_file("conftest.py") is True
|
||||
assert is_test_file(tmp_path / "tests" / "test_board.py") is True
|
||||
source = tmp_path / "src" / "app.py"
|
||||
source.parent.mkdir(parents=True)
|
||||
source.write_text("def run():\n return 1\n", encoding="utf-8")
|
||||
hidden = tmp_path / "tests" / "hidden.py"
|
||||
hidden.parent.mkdir()
|
||||
hidden.write_text("def hidden():\n return 1\n", encoding="utf-8")
|
||||
skipped = tmp_path / "target" / "gen.py"
|
||||
skipped.parent.mkdir()
|
||||
skipped.write_text("def gen():\n return 1\n", encoding="utf-8")
|
||||
assert iter_source_files([tmp_path]) == [source.resolve()]
|
||||
assert iter_source_files([tmp_path / "missing"]) == []
|
||||
assert iter_source_files([source]) == [source.resolve()]
|
||||
|
||||
|
||||
def test_files_outside_the_project_keep_an_absolute_path(tmp_path):
|
||||
outside = tmp_path / "board.go"
|
||||
outside.write_text("package demo\nfunc Place() int { return 1 }\n", encoding="utf-8")
|
||||
entries = analyze_files([outside], tmp_path / "other", None)
|
||||
assert entries[0].name == "Place"
|
||||
assert entries[0].path == str(outside.resolve())
|
||||
assert analyze_files([tmp_path / "notes.md"], tmp_path, None) == []
|
||||
|
||||
|
||||
def test_module_entry_point_calls_main(monkeypatch):
|
||||
called = {}
|
||||
monkeypatch.setattr("crapper.cli.main", lambda: called.setdefault("ran", True))
|
||||
runpy.run_module("crapper.__main__", run_name="__main__")
|
||||
assert called["ran"] is True
|
||||
|
||||
|
||||
def test_end_line_of_a_trailing_newline_stays_on_the_last_source_line():
|
||||
_data, tree = parse("def choose():\n return 1\n", "python")
|
||||
assert end_line(tree.root_node) == 2
|
||||
assert child_of_type(tree.root_node, "not_a_node") is None
|
||||
|
||||
|
||||
def test_clojure_scanner_survives_end_of_file():
|
||||
assert extract_functions('(defn foo [] \\') == []
|
||||
assert extract_functions("(defn foo [] \\abc") == []
|
||||
assert [item["name"] for item in extract_functions("(defn foo [] ;; comment")] == []
|
||||
assert extract_functions('(defn foo [] "hello') == []
|
||||
found = _extract_top_level_defns('(defn foo []\n ;; comment\n "hello")')
|
||||
assert found[0]["name"] == "foo"
|
||||
assert found[0]["text"].endswith(")")
|
||||
|
||||
|
||||
def test_go_edges(tmp_path):
|
||||
assert [(fn.namespace, fn.name) for fn in go_functions("func Orphan() int { return 1 }\n", "board.go")] == [
|
||||
("", "Orphan")
|
||||
]
|
||||
source = "package p\nfunc Declared(x int)\nfunc Real() int { return 1 }\nfunc () Run() {}\nfunc (w *Widget) () {}\n"
|
||||
assert [(fn.namespace, fn.name) for fn in go_functions(source, "board.go")] == [("p", "Real")]
|
||||
(tmp_path / "go.mod").write_text("module example.com/demo\n", encoding="utf-8")
|
||||
path = tmp_path / "board.go"
|
||||
path.write_text("package demo\nfunc Place() int { return 1 }\n", encoding="utf-8")
|
||||
rooted = go_functions(path.read_text(encoding="utf-8"), str(path), str(tmp_path))
|
||||
assert rooted[0].namespace == "example.com/demo"
|
||||
|
||||
|
||||
def test_a_method_inside_a_lambda_is_not_an_entry():
|
||||
source = """
|
||||
class Sample {
|
||||
Runnable r = () -> {
|
||||
class Local {
|
||||
int score() { if (true) return 1; return 0; }
|
||||
}
|
||||
return null;
|
||||
};
|
||||
int outer() { return 1; }
|
||||
}
|
||||
"""
|
||||
assert [method.name for method in java_functions(source, "Sample.java")] == ["outer"]
|
||||
|
||||
|
||||
def test_typescript_namespaces_and_declarations_without_bodies():
|
||||
body = "export function choose(): number { return 1 }\n"
|
||||
assert ts_functions(body, "/app/src/demo/box.ts", "/app")[0].namespace == "demo.box"
|
||||
assert ts_functions(body, "./demo/box.ts", None)[0].namespace == "demo.box"
|
||||
assert ts_functions(body, "/app/pkg/src/demo/box.mts", None)[0].namespace == "demo.box"
|
||||
source = """
|
||||
function choose(): void;
|
||||
class Box { open(): void; }
|
||||
const plain = 1;
|
||||
const arrow = (n: number) => n;
|
||||
"""
|
||||
functions = ts_functions(source, "src/demo/box.ts", "/proj")
|
||||
assert [(fn.namespace, fn.name) for fn in functions] == [("demo.box", "arrow")]
|
||||
object_method = "const obj = { open() { return 1 } }\n"
|
||||
assert ts_functions(object_method, "src/demo/box.ts", "/proj") == []
|
||||
computed = "class Box { [name]() { return 1 } }\n"
|
||||
assert ts_functions(computed, "src/demo/box.ts", "/proj") == []
|
||||
|
||||
|
||||
def test_python_function_without_a_name_is_skipped():
|
||||
source = "def choose(x):\n return x\n"
|
||||
assert [fn.name for fn in python_functions(source, "src/app.py", "/proj")] == ["choose"]
|
||||
|
||||
|
||||
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',
|
||||
encoding="utf-8",
|
||||
)
|
||||
bare = rust_functions("pub fn bare() {}\n", "notes.rs", None)
|
||||
assert bare[0].name == "bare"
|
||||
assert bare[0].namespace == "crate::notes"
|
||||
source = "trait Foo {\n fn draw(&self) {}\n}\n"
|
||||
path = tmp_path / "src" / "lib.rs"
|
||||
functions = rust_functions(source, str(path), str(tmp_path))
|
||||
assert [(fn.namespace, fn.name) for fn in functions] == [("demo", "draw")]
|
||||
@@ -4,9 +4,15 @@ from pathlib import Path
|
||||
from crapper.discover import is_test_file
|
||||
from crapper.runners import (
|
||||
_clean_clojure,
|
||||
_clean_dir,
|
||||
_coverage_report,
|
||||
_ensure_python_module,
|
||||
_ensure_vitest_coverage,
|
||||
_manifest_snapshot,
|
||||
_prepare_report,
|
||||
_python_executable,
|
||||
_python_kind,
|
||||
_record_python_lcov,
|
||||
_restore_manifests,
|
||||
_rust_kind,
|
||||
_vitest_version,
|
||||
@@ -14,6 +20,7 @@ from crapper.runners import (
|
||||
python_roots,
|
||||
python_sources,
|
||||
run_coverage,
|
||||
run_shell,
|
||||
rust_coverage_command,
|
||||
rust_modules,
|
||||
typescript_command,
|
||||
@@ -250,3 +257,177 @@ def test_missing_build_files_skip_that_language(tmp_path, monkeypatch):
|
||||
_write(tmp_path, "src/lib.rs", "pub fn open() {}\n"),
|
||||
]
|
||||
run_coverage(tmp_path, files, None)
|
||||
|
||||
|
||||
def test_run_shell_returns_the_child_status_and_reports_a_failed_start(tmp_path, monkeypatch, capsys):
|
||||
assert run_shell("exit 3", tmp_path) == 3
|
||||
|
||||
def boom(*_args, **_kwargs):
|
||||
raise OSError("nope")
|
||||
|
||||
monkeypatch.setattr("crapper.runners.subprocess.run", boom)
|
||||
assert run_shell("exit 3", tmp_path) == 127
|
||||
assert "failed to start" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_clean_removes_a_stale_directory_and_ignores_a_missing_one(tmp_path):
|
||||
stale = tmp_path / "target" / "coverage" / "old"
|
||||
stale.mkdir(parents=True)
|
||||
(stale / "lcov.info").write_text("x", encoding="utf-8")
|
||||
kept = tmp_path / "target" / "coverage" / "python"
|
||||
kept.mkdir()
|
||||
_clean_clojure(tmp_path)
|
||||
assert not stale.exists()
|
||||
assert kept.is_dir()
|
||||
_clean_dir(tmp_path / "target" / "coverage")
|
||||
assert not (tmp_path / "target" / "coverage").exists()
|
||||
_clean_clojure(tmp_path)
|
||||
_clean_dir(tmp_path / "missing")
|
||||
|
||||
|
||||
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
|
||||
_write(tmp_path, "package.json", "[]")
|
||||
assert typescript_command(tmp_path, [], tmp_path / "cov") is None
|
||||
_write(tmp_path, "package.json", '{"scripts": ["nope"]}')
|
||||
assert typescript_command(tmp_path, [], tmp_path / "cov") is None
|
||||
|
||||
|
||||
def test_vitest_include_skips_a_file_outside_the_package(tmp_path):
|
||||
from crapper.runners import _vitest_includes
|
||||
|
||||
package = tmp_path / "web"
|
||||
_write(package, "src/app.ts", "export const n = 1\n")
|
||||
outside = Path("/tmp/crapper-not-in-package.ts")
|
||||
assert _vitest_includes(package, [package / "src" / "app.ts", outside]) == ["src/app.ts"]
|
||||
|
||||
|
||||
def test_nested_module_coverage_report_is_slugged(tmp_path):
|
||||
module = tmp_path / "src-tauri"
|
||||
module.mkdir()
|
||||
report = _coverage_report(tmp_path, module, "rust")
|
||||
assert report == tmp_path / "target" / "coverage" / "rust" / "src-tauri" / "lcov.info"
|
||||
|
||||
|
||||
def test_prepare_report_replaces_an_existing_directory(tmp_path):
|
||||
report = tmp_path / "target" / "coverage" / "typescript" / "lcov.info"
|
||||
report.parent.mkdir(parents=True)
|
||||
(report.parent / "old.info").write_text("old", encoding="utf-8")
|
||||
_prepare_report(report)
|
||||
assert report.parent.is_dir()
|
||||
assert not (report.parent / "old.info").exists()
|
||||
|
||||
|
||||
def test_pytest_detection_and_the_unittest_fallback(tmp_path, monkeypatch, capsys):
|
||||
assert uses_pytest(tmp_path) is False
|
||||
_write(tmp_path, "conftest.py", "")
|
||||
assert uses_pytest(tmp_path) is True
|
||||
monkeypatch.setattr("crapper.runners.run_shell", lambda *_args: 0)
|
||||
assert _python_kind("python3", tmp_path) == "pytest"
|
||||
bare = tmp_path / "bare"
|
||||
bare.mkdir()
|
||||
assert _python_kind("python3", bare) == "unittest"
|
||||
monkeypatch.setattr("crapper.runners._ensure_python_module", lambda *_args: False)
|
||||
assert _python_kind("python3", tmp_path) == "unittest"
|
||||
assert "Falling back to unittest" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_python_sources_ignore_a_file_outside_the_package(tmp_path):
|
||||
inside = _write(tmp_path, "src/app.py", "def run():\n return 1\n")
|
||||
assert python_sources(tmp_path, [inside, Path("/tmp/crapper-outside.py")]) == "src"
|
||||
|
||||
|
||||
def test_missing_python_module_is_installed(tmp_path, monkeypatch):
|
||||
commands = []
|
||||
|
||||
def shell(command, _cwd):
|
||||
commands.append(command)
|
||||
if 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)
|
||||
commands.clear()
|
||||
|
||||
def already(_command, _cwd):
|
||||
commands.append("probe")
|
||||
return 0
|
||||
|
||||
monkeypatch.setattr("crapper.runners.run_shell", already)
|
||||
assert _ensure_python_module("python3", tmp_path, "coverage") is True
|
||||
assert commands == ["probe"]
|
||||
|
||||
|
||||
def test_python_lcov_warnings(tmp_path, monkeypatch, capsys):
|
||||
data = tmp_path / ".coverage"
|
||||
monkeypatch.setattr("crapper.runners.run_shell", lambda *_args: 1)
|
||||
_record_python_lcov(tmp_path, 1, data, "lcov")
|
||||
assert "Python coverage exited 1" in capsys.readouterr().err
|
||||
_record_python_lcov(tmp_path, 0, data, "lcov")
|
||||
assert capsys.readouterr().err == ""
|
||||
data.write_text("x", encoding="utf-8")
|
||||
_record_python_lcov(tmp_path, 0, data, "lcov")
|
||||
assert "coverage lcov exited 1" in capsys.readouterr().err
|
||||
monkeypatch.setattr("crapper.runners.run_shell", lambda *_args: 0)
|
||||
_record_python_lcov(tmp_path, 7, data, "lcov")
|
||||
assert "Coverage was still recorded" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_custom_coverage_command_failure_is_reported(tmp_path, capsys):
|
||||
run_coverage(tmp_path, [], "exit 9")
|
||||
assert "exited 9" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_clojure_without_a_build_file_is_skipped(tmp_path, monkeypatch, capsys):
|
||||
monkeypatch.setattr("crapper.runners.run_shell", lambda *_args: 0)
|
||||
source = _write(tmp_path, "src/demo/core.clj", "(ns demo.core)\n(defn x [] 1)\n")
|
||||
run_coverage(tmp_path, [source], None)
|
||||
assert "skipping Clojure coverage" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_rust_kind_none_skips_rust_coverage(tmp_path, monkeypatch, capsys):
|
||||
monkeypatch.setattr("crapper.runners._rust_kind", lambda: None)
|
||||
monkeypatch.setattr("crapper.runners.run_shell", lambda *_args: 0)
|
||||
source = _write(tmp_path, "src/lib.rs", "pub fn open() {}\n")
|
||||
_write(tmp_path, "Cargo.toml", '[package]\nname = "demo"\nversion = "0.1.0"\n')
|
||||
run_coverage(tmp_path, [source], None)
|
||||
assert "cargo" not in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_tarpaulin_command_and_kind(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"crapper.runners.shutil.which",
|
||||
lambda name: "/bin/cargo-tarpaulin" if name == "cargo-tarpaulin" else None,
|
||||
)
|
||||
assert _rust_kind() == "tarpaulin"
|
||||
report = Path("/tmp/book/target/coverage/rust/lcov.info")
|
||||
assert "tarpaulin" in rust_coverage_command("tarpaulin", report)
|
||||
|
||||
|
||||
def test_typescript_without_a_test_script_is_skipped(tmp_path, monkeypatch, capsys):
|
||||
monkeypatch.setattr("crapper.runners.run_shell", lambda *_args: 0)
|
||||
_write(tmp_path, "package.json", '{"scripts": {}}')
|
||||
source = _write(tmp_path, "src/app.ts", "export const n = 1\n")
|
||||
run_coverage(tmp_path, [source], None)
|
||||
assert "No package.json test script" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_vitest_provider_failure_skips_typescript(tmp_path, monkeypatch, capsys):
|
||||
monkeypatch.setattr("crapper.runners.run_shell", lambda *_args: 1)
|
||||
_write(tmp_path, "package.json", '{"scripts": {"test": "vitest run"}}')
|
||||
source = _write(tmp_path, "src/app.ts", "export const n = 1\n")
|
||||
run_coverage(tmp_path, [source], None)
|
||||
assert "coverage provider is missing" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_python_coverage_module_missing_is_reported(tmp_path, monkeypatch, capsys):
|
||||
monkeypatch.setattr("crapper.runners._ensure_python_module", lambda *_args: False)
|
||||
monkeypatch.setattr("crapper.runners.run_shell", lambda *_args: 0)
|
||||
_write(tmp_path, "pyproject.toml", "[tool.pytest.ini_options]\n")
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user