Add a multi-language CRAP tool that writes .metrics/crap.edn for uml-viewer.

Score Clojure, Java, Go, TypeScript, Rust, and Python with each language's complexity rules and coverage report.
This commit is contained in:
Robert C. Martin
2026-09-30 08:21:44 -05:00
commit a4b5fc2aed
33 changed files with 3913 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
from crapper.coverage import (
CoverageBundle,
go_percent,
load_bundle,
parse_form_coverage,
parse_go_profile,
parse_jacoco_index,
parse_lcov,
)
from crapper.model import Function
def function(**kwargs):
defaults = dict(
name="choose",
namespace="demo.core",
complexity=2,
start_line=3,
end_line=5,
path="src/demo/core.clj",
language="clojure",
jacoco_class=None,
)
defaults.update(kwargs)
return Function(**defaults)
def test_cloverage_form_counts_and_lcov_lines():
html = '<span class="pc" title="1 out of 2 forms covered"> 4&nbsp;&nbsp;(if x 1)</span>'
assert parse_form_coverage(html) == {4: (1, 2)}
lcov = "SF:src/demo/core.clj\nDA:3,1\nDA:4,0\nDA:9,3\nend_of_record\n"
parsed = parse_lcov(lcov)
bundle = CoverageBundle(lcov=parsed, form_html={"src/demo/core.clj": {4: (1, 2)}})
# Form HTML wins over LCOV for Clojure, matching crap4clj.
assert bundle.percent_for(function(start_line=3, end_line=5)) == 50.0
def test_lcov_percentage_when_there_is_no_html():
lcov = parse_lcov("SF:src/demo/core.clj\nDA:3,1\nDA:4,0\nend_of_record\n")
bundle = CoverageBundle(lcov=lcov)
assert bundle.percent_for(function(start_line=3, end_line=4)) == 50.0
assert bundle.percent_for(function(path="src/missing.clj")) is None
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"
)
assert go_percent(profile, "board.go", 4, 6) == 100.0
assert go_percent(profile, "board.go", 8, 9) == 0.0
assert go_percent(profile, "other.go", 1, 3) is None
def test_jacoco_instruction_coverage_joins_on_class_and_method():
xml = """<?xml version="1.0"?>
<report>
<package>
<class name="demo/pkg/Board">
<method name="place" line="4">
<counter type="INSTRUCTION" missed="1" covered="3"/>
</method>
</class>
<class name="demo/pkg/Board$Inner">
<method name="tick" line="9">
<counter type="INSTRUCTION" missed="0" covered="2"/>
</method>
</class>
</package>
</report>
"""
bundle = CoverageBundle(jacoco=parse_jacoco_index(xml))
outer = function(
name="place",
namespace="demo.pkg.Board",
language="java",
jacoco_class="demo.pkg.Board",
start_line=4,
path="src/demo/pkg/Board.java",
)
inner = function(
name="tick",
namespace="demo.pkg.Board.Inner",
language="java",
jacoco_class="demo.pkg.Board$Inner",
start_line=9,
path="src/demo/pkg/Board.java",
)
assert bundle.percent_for(outer) == 75.0
assert bundle.percent_for(inner) == 100.0
def test_jacoco_skips_methods_without_an_instruction_counter():
xml = """<report>
<class name="demo/pkg/Board">
<method name="skip" line="2">
<counter type="LINE" missed="1" covered="0"/>
</method>
<method name="bad" line="nope">
<counter type="INSTRUCTION" missed="1" covered="1"/>
</method>
<counter type="INSTRUCTION" missed="0" covered="1"/>
</class>
</report>"""
found = parse_jacoco_index(xml)
assert list(found) == ["demo.pkg.Board#bad"]
assert found["demo.pkg.Board#bad"][0].line == 0
def test_missing_go_profile_is_na():
assert go_percent(None, "board.go", 1, 3) is None
def test_load_bundle_merges_each_report(tmp_path):
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,0\nend_of_record\n", encoding="utf-8")
go = tmp_path / "target" / "coverage" / "go" / "coverage.out"
go.parent.mkdir(parents=True)
go.write_text("mode: set\nboard.go:1.1,2.2 1 1\n", encoding="utf-8")
jacoco = tmp_path / "target" / "site" / "jacoco" / "jacoco.xml"
jacoco.parent.mkdir(parents=True)
jacoco.write_text(
'<report><class name="demo/Board"><method name="place" line="4">'
'<counter type="INSTRUCTION" missed="0" covered="1"/></method></class></report>',
encoding="utf-8",
)
html = tmp_path / "target" / "coverage" / "src" / "demo" / "core.clj.html"
html.parent.mkdir(parents=True)
html.write_text(
'<span title="1 out of 2 forms covered"> 4&nbsp;&nbsp;(if x 1)</span>',
encoding="utf-8",
)
(tmp_path / "target" / "coverage" / "index.html").write_text("<html></html>", encoding="utf-8")
bundle = load_bundle(tmp_path)
assert bundle.percent_for(function(start_line=3, end_line=4)) == 50.0
assert bundle.go_profile is not None
assert bundle.jacoco is not None