Count ?? and ?., Express handlers, and JavaScript files, and score missing coverage as 0%.

This commit is contained in:
Robert C. Martin
2026-10-03 08:49:10 -05:00
parent b1c0fa4104
commit f0cb973110
13 changed files with 467 additions and 44 deletions
+4
View File
@@ -14,6 +14,10 @@ def test_language_detection():
assert language_of("Widget.java") == "java"
assert language_of("board.go") == "go"
assert language_of("ui/view.tsx") == "typescript"
assert language_of("src/app.js") == "typescript"
assert language_of("src/app.mjs") == "typescript"
assert language_of("src/app.cjs") == "typescript"
assert language_of("src/app.jsx") == "typescript"
assert language_of("src/lib.rs") == "rust"
assert language_of("src/crapper/cli.py") == "python"
assert language_of("types.d.ts") is None
+23
View File
@@ -42,6 +42,29 @@ def test_lcov_percentage_when_there_is_no_html():
assert bundle.percent_for(function(path="src/missing.clj")) is None
def test_lcov_branch_records_override_line_hits():
text = "SF:src/demo/app.ts\nDA:2,1\nDA:3,1\nBRDA:3,0,0,1\nBRDA:3,0,1,-\nend_of_record\n"
parsed = parse_lcov(text)
assert parsed["src/demo/app.ts"][2] == (1, 1)
assert parsed["src/demo/app.ts"].branches[3] == (1, 2)
bundle = CoverageBundle(lcov=parsed)
scored = function(
path="src/demo/app.ts",
language="typescript",
start_line=2,
end_line=3,
namespace="demo.app",
)
assert bundle.percent_for(scored) == 50.0
def test_a_branchless_span_keeps_line_coverage():
text = "SF:src/demo/app.ts\nDA:2,1\nDA:3,0\nBRDA:8,0,0,1\nBRDA:8,0,1,0\nend_of_record\n"
bundle = CoverageBundle(lcov=parse_lcov(text))
scored = function(path="src/demo/app.ts", language="typescript", start_line=2, end_line=3)
assert bundle.percent_for(scored) == 50.0
def test_go_profile_matches_crap4go_statement_ranges():
profile = parse_go_profile(
"mode: set\ngithub.com/acme/demo/board.go:4.1,6.2 2 1\ngithub.com/acme/demo/board.go:8.1,9.2 1 0\n"
+20
View File
@@ -145,6 +145,8 @@ def test_load_bundle_on_an_empty_tree_has_no_profiles(tmp_path):
def test_conftest_and_a_tests_directory_are_tests(tmp_path):
assert is_test_file("conftest.py") is True
assert is_test_file("src/app.test.js") is True
assert is_test_file("src/app.spec.mjs") 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)
@@ -160,6 +162,24 @@ def test_conftest_and_a_tests_directory_are_tests(tmp_path):
assert iter_source_files([source]) == [source.resolve()]
def test_missing_coverage_scores_zero_and_sorts_ahead_of_a_covered_function(tmp_path):
low = tmp_path / "src" / "low.ts"
high = tmp_path / "src" / "high.ts"
low.parent.mkdir(parents=True)
body = "export function {name}(x: number) {{\n if (x) return 1;\n return 0;\n}}\n"
low.write_text(body.format(name="low"), encoding="utf-8")
high.write_text(body.format(name="high"), encoding="utf-8")
bundle = CoverageBundle(lcov={"src/low.ts": {1: (1, 1), 2: (1, 1), 3: (1, 1)}})
entries = analyze_files([low, high], tmp_path, bundle)
assert [(entry.name, entry.coverage, entry.crap) for entry in entries] == [
("high", 0.0, 6.0),
("low", 100.0, 2.0),
]
unscored = analyze_files([high], tmp_path, None)
assert unscored[0].coverage is None
assert unscored[0].crap is None
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")
+81
View File
@@ -51,3 +51,84 @@ export function View(ok: boolean, ready: boolean) {
assert [(fn.namespace, fn.name, fn.complexity) for fn in functions] == [
("ui.view", "View", 3)
]
def test_nullish_and_optional_chains_are_branches():
source = """
export function choose(a, b, c) {
return a ?? b?.c ?? c?.() ?? c?.[0];
}
export function text() {
return "a ?? b?.c";
}
"""
functions = functions_in_source(source, "src/demo/box.ts", "/proj")
assert [(fn.name, fn.complexity) for fn in functions] == [
("choose", 7),
("text", 1),
]
def test_express_handlers_are_entries_and_leave_the_parent():
source = """
export function mount(app) {
const extra = [1].map((n) => (n ? 1 : 0));
app.get("/users", (req, res) => {
if (req.query.q) return 1;
return 0;
});
app.post("/users", function (req, res) {
return req.body ?? {};
});
app.use((req, res, next) => next());
app.route("/items").get((req, res) => res.send(1)).post((req, res) => res.send(2));
return extra;
}
app.get("/health", async (req, res) => res.send(req.query.ok ?? "no"));
app.get("/users", (req, res, next) => next(), (req, res) => res.send(req.id ?? 0));
app.delete(`/users/${id}`, ((req, res) => res.send(1)));
"""
functions = functions_in_source(source, "src/demo/routes.ts", "/proj")
assert [(fn.name, fn.complexity) for fn in functions] == [
("mount", 2),
("GET /users", 2),
("POST /users", 2),
("USE", 1),
("GET /items", 1),
("POST /items", 1),
("GET /health", 2),
("GET /users#2", 1),
("GET /users#3", 2),
("DELETE /users/${id}", 1),
]
assert {fn.namespace for fn in functions} == {"demo.routes"}
def test_a_nested_callback_that_is_not_a_route_stays_inside_its_function():
source = """
function outer() {
const values = [1].map((n) => (n ? 1 : 0));
return values;
}
"""
functions = functions_in_source(source, "src/demo/box.ts", "/proj")
assert [(fn.name, fn.complexity) for fn in functions] == [("outer", 2)]
def test_javascript_files_use_the_same_rules():
source = """
export function choose(a, b, c) {
return a ?? b?.c ?? c?.() ?? c?.[0];
}
app.get("/users", (req, res) => req.id ?? 0);
"""
functions = functions_in_source(source, "src/demo/app.mjs", "/proj")
assert [(fn.namespace, fn.name, fn.complexity) for fn in functions] == [
("demo.app", "choose", 7),
("demo.app", "GET /users", 2),
]
js = functions_in_source(source, "src/demo/app.js", "/proj")
assert [(fn.name, fn.complexity) for fn in js] == [
("choose", 7),
("GET /users", 2),
]