mirror of
https://github.com/unclebob/crapper.git
synced 2026-11-07 10:33:50 +00:00
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:
@@ -0,0 +1,167 @@
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from crapper.cli import run
|
||||
from crapper.discover import language_of
|
||||
|
||||
|
||||
def test_language_detection():
|
||||
assert language_of("src/app/core.clj") == "clojure"
|
||||
assert language_of("src/app/core.cljc") == "clojure"
|
||||
assert language_of("Widget.java") == "java"
|
||||
assert language_of("board.go") == "go"
|
||||
assert language_of("ui/view.tsx") == "typescript"
|
||||
assert language_of("src/lib.rs") == "rust"
|
||||
assert language_of("src/crapper/cli.py") == "python"
|
||||
assert language_of("types.d.ts") is None
|
||||
assert language_of("notes.md") is None
|
||||
|
||||
|
||||
def _write(root: Path, relative: str, source: str) -> None:
|
||||
path = root / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(source, encoding="utf-8")
|
||||
|
||||
|
||||
def test_project_snapshot_is_readable_as_crap4clj_edn(tmp_path):
|
||||
_write(
|
||||
tmp_path,
|
||||
"src/demo/core.clj",
|
||||
"(ns demo.core)\n\n(defn choose [x]\n (if x 1 0))\n",
|
||||
)
|
||||
_write(
|
||||
tmp_path,
|
||||
"src/demo/Board.java",
|
||||
"package demo;\npublic class Board {\n public int place(int x) {\n if (x > 0 && ready) return 1;\n return 0;\n }\n}\n",
|
||||
)
|
||||
_write(
|
||||
tmp_path,
|
||||
"board.go",
|
||||
"package demo\n\nfunc Place(x int) int {\n\tif x > 0 {\n\t\treturn x\n\t}\n\treturn 0\n}\n",
|
||||
)
|
||||
_write(
|
||||
tmp_path,
|
||||
"src/ui/view.ts",
|
||||
"export function view(ok: boolean): number {\n return ok ? 1 : 0;\n}\n",
|
||||
)
|
||||
_write(tmp_path, "Cargo.toml", '[package]\nname = "demo"\nversion = "0.1.0"\n')
|
||||
_write(
|
||||
tmp_path,
|
||||
"src/lib.rs",
|
||||
"pub fn open(ok: bool) -> i32 { if ok { 1 } else { 0 } }\n",
|
||||
)
|
||||
_write(tmp_path, "src/demo/core_test.clj", "(ns demo.core-test)\n(defn should-skip [] 1)\n")
|
||||
_write(tmp_path, "board_test.go", "package demo\nfunc TestPlace(t *testing.T) {}\n")
|
||||
|
||||
code = run(["--root", str(tmp_path), "--no-coverage"])
|
||||
assert code == 0
|
||||
text = (tmp_path / ".metrics" / "crap.edn").read_text(encoding="utf-8")
|
||||
assert ':namespace "demo.core"' in text
|
||||
assert ':name "choose"' in text
|
||||
assert ':namespace "demo.Board"' in text
|
||||
assert ':name "place"' in text
|
||||
assert ':namespace "demo"' in text and ':name "Place"' in text
|
||||
assert ':namespace "ui.view"' in text
|
||||
assert ':namespace "demo"' in text and ':name "open"' in text
|
||||
assert "should-skip" not in text
|
||||
assert "TestPlace" not in text
|
||||
|
||||
reader = subprocess.run(
|
||||
[
|
||||
"bb",
|
||||
"-e",
|
||||
(
|
||||
"(require '[clojure.edn :as edn])"
|
||||
"(let [data (edn/read-string (slurp *in*))]"
|
||||
" (doseq [entry (:entries data)]"
|
||||
" (println (:namespace entry) (:name entry)"
|
||||
" (:complexity entry) (pr-str (:coverage entry)))))"
|
||||
),
|
||||
],
|
||||
input=text,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
assert reader.returncode == 0, reader.stderr
|
||||
assert "demo.core choose 2 nil" in reader.stdout
|
||||
assert "demo.Board place 3 nil" in reader.stdout
|
||||
|
||||
|
||||
def test_existing_coverage_feeds_the_snapshot(tmp_path):
|
||||
_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",
|
||||
)
|
||||
code = run(
|
||||
["--root", str(tmp_path), "--use-existing-coverage", "src/demo/core.clj"]
|
||||
)
|
||||
assert code == 0
|
||||
text = (tmp_path / ".metrics" / "crap.edn").read_text(encoding="utf-8")
|
||||
assert ":coverage 100.0" in text
|
||||
assert ":crap 2.0" in text
|
||||
|
||||
|
||||
def test_threshold_exits_when_the_worst_score_is_too_high(tmp_path):
|
||||
_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,0\nDA:4,0\nend_of_record\n", encoding="utf-8")
|
||||
code = run(
|
||||
[
|
||||
"--root",
|
||||
str(tmp_path),
|
||||
"--use-existing-coverage",
|
||||
"--threshold",
|
||||
"1",
|
||||
]
|
||||
)
|
||||
assert code == 2
|
||||
|
||||
|
||||
def test_help_and_unknown_option(capsys):
|
||||
assert run(["--help"]) == 0
|
||||
assert "uml-viewer" in capsys.readouterr().out
|
||||
assert run(["--not-an-option"]) == 1
|
||||
assert run(["--root"]) == 1
|
||||
assert run(["--threshold", "nope"]) == 1
|
||||
assert run(["--no-coverage", "--coverage-command", "true"]) == 1
|
||||
|
||||
|
||||
def test_selects_directories_filters_and_changed_files(tmp_path, monkeypatch, capsys):
|
||||
_write(tmp_path, "src/demo/core.clj", "(ns demo.core)\n\n(defn choose [x]\n (if x 1 0))\n")
|
||||
_write(tmp_path, "notes.md", "not source\n")
|
||||
assert run(["--root", str(tmp_path), "--no-coverage", "--source-root", "src"]) == 0
|
||||
assert run(["--root", str(tmp_path), "--no-coverage", "src"]) == 0
|
||||
assert run(["--root", str(tmp_path), "--no-coverage", "src/demo/core.clj", "core"]) == 0
|
||||
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())
|
||||
assert run(["--root", str(tmp_path), "--no-coverage", "--changed"]) == 0
|
||||
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
assert run(["--root", str(tmp_path), "--coverage-command", "true"]) == 0
|
||||
@@ -0,0 +1,106 @@
|
||||
from crapper.languages.clojure import (
|
||||
cyclomatic_complexity,
|
||||
extract_functions,
|
||||
functions_in_source,
|
||||
namespace_from_path,
|
||||
)
|
||||
|
||||
|
||||
def test_base_and_branch_forms():
|
||||
assert cyclomatic_complexity("(defn foo [])") == 1
|
||||
assert cyclomatic_complexity("(defn foo [x]\n (+ x 1))") == 1
|
||||
assert cyclomatic_complexity("(defn foo [x]\n (if x 1 0))") == 2
|
||||
assert cyclomatic_complexity("(defn foo [x]\n (if-not x 1 0))") == 2
|
||||
assert cyclomatic_complexity("(defn foo [x]\n (if-let [y x] y 0))") == 2
|
||||
assert cyclomatic_complexity("(defn foo [x]\n (when x 1))") == 2
|
||||
assert cyclomatic_complexity("(defn foo [x]\n (when-first [y x] y))") == 2
|
||||
assert cyclomatic_complexity("(defn foo [x y]\n (and x y))") == 2
|
||||
assert cyclomatic_complexity("(defn foo [x y]\n (or x y))") == 2
|
||||
assert cyclomatic_complexity("(defn foo [x]\n (loop [i 0] (recur (inc i))))") == 2
|
||||
|
||||
|
||||
def test_cond_case_and_threading():
|
||||
assert (
|
||||
cyclomatic_complexity("(defn foo [x]\n (cond\n (= x 1) :one\n :else :other))")
|
||||
== 3
|
||||
)
|
||||
assert cyclomatic_complexity("(defn foo [x]\n (condp = x\n 1 :one\n 2 :two))") == 3
|
||||
assert cyclomatic_complexity("(defn foo [x]\n (case x\n 1 :one\n :other))") == 3
|
||||
assert (
|
||||
cyclomatic_complexity(
|
||||
"(defn foo [x]\n (cond-> x\n (pos? x) inc\n (even? x) (* 2)))"
|
||||
)
|
||||
== 3
|
||||
)
|
||||
assert cyclomatic_complexity("(defn foo [x]\n (some-> x inc dec))") == 3
|
||||
assert (
|
||||
cyclomatic_complexity("(defn foo [x]\n (some->> x (map inc) (filter pos?)))") == 3
|
||||
)
|
||||
|
||||
|
||||
def test_ignores_decisions_in_strings_and_comments():
|
||||
assert cyclomatic_complexity('(defn foo [x]\n "if when cond and or"\n x)') == 1
|
||||
assert cyclomatic_complexity('(defn foo [x]\n "(if true 1 0)"\n x)') == 1
|
||||
assert cyclomatic_complexity('(defn foo [x]\n ;; if when cond\n x)') == 1
|
||||
assert cyclomatic_complexity('(defn foo [x]\n (if x :yes :no) ; and or when cond)') == 2
|
||||
|
||||
|
||||
def test_map_literals_inside_cond_are_not_extra_clauses():
|
||||
source = """(defn foo [cell]
|
||||
(let [contents (:contents cell)]
|
||||
(cond
|
||||
(pred-a? contents)
|
||||
{:type :army :mode :awake :owner (:owner contents) :aboard true}
|
||||
|
||||
(pred-b? contents)
|
||||
{:type :fighter :mode :awake :owner (:owner contents) :fuel 20 :from-carrier true}
|
||||
|
||||
(pred-c? contents) contents
|
||||
|
||||
(pred-d? cell)
|
||||
{:type :fighter :mode :awake :owner :player :fuel 20 :from-airport true}
|
||||
|
||||
:else nil)))"""
|
||||
assert cyclomatic_complexity(source) == 6
|
||||
|
||||
|
||||
def test_extracts_defn_names_lines_and_namespace():
|
||||
source = "(ns demo.core)\n\n(defn choose [x]\n (if x 1 0))\n\n(defn- hidden [y]\n y)\n"
|
||||
found = functions_in_source(source, "src/demo/core.clj", "/proj")
|
||||
assert [(item.name, item.complexity, item.start_line) for item in found] == [
|
||||
("choose", 2, 3),
|
||||
("hidden", 1, 6),
|
||||
]
|
||||
assert {item.namespace for item in found} == {"demo.core"}
|
||||
|
||||
|
||||
def test_top_level_def_does_not_attach_to_the_previous_function():
|
||||
source = (
|
||||
"(defn alpha [x]\n"
|
||||
" (if x 1 0))\n\n"
|
||||
"(def rules\n"
|
||||
" [{:pred (fn [v]\n"
|
||||
" (if v (when (pos? v) true) false))}])\n\n"
|
||||
"(defn omega [y]\n"
|
||||
" y)"
|
||||
)
|
||||
found = {item["name"]: item["complexity"] for item in extract_functions(source)}
|
||||
assert found == {"alpha": 2, "omega": 1}
|
||||
|
||||
|
||||
def test_namespace_falls_back_to_the_path():
|
||||
assert namespace_from_path("src/demo/core_extra.clj", None) == "demo.core-extra"
|
||||
assert namespace_from_path("/proj/src/demo/core.cljc", "/proj") == "demo.core"
|
||||
assert namespace_from_path("pkg/src/demo/core.bb", None) == "demo.core"
|
||||
found = functions_in_source("(defn foo [] 1)\n", "src/demo/core_extra.clj", "/proj")
|
||||
assert found[0].namespace == "demo.core-extra"
|
||||
|
||||
|
||||
def test_strings_comments_and_character_literals_stay_inside_one_form():
|
||||
source = '(defn foo [x]\n "say \\"hi\\"\n (if x 1)"\n \\space\n ;; (defn hidden [])\n x)\n'
|
||||
assert [item["name"] for item in extract_functions(source)] == ["foo"]
|
||||
|
||||
|
||||
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"]
|
||||
@@ -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 (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 (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
|
||||
@@ -0,0 +1,73 @@
|
||||
from crapper.crap import crap_score, sort_entries
|
||||
from crapper.metrics import render_edn
|
||||
from crapper.model import Entry
|
||||
from crapper.report import format_report
|
||||
|
||||
|
||||
def entry(**kwargs):
|
||||
defaults = dict(
|
||||
name="score",
|
||||
namespace="demo.core",
|
||||
complexity=1,
|
||||
coverage=100.0,
|
||||
crap=1.0,
|
||||
path="src/demo/core.clj",
|
||||
language="clojure",
|
||||
)
|
||||
defaults.update(kwargs)
|
||||
return Entry(**defaults)
|
||||
|
||||
|
||||
def test_formula_matches_crap4clj():
|
||||
assert crap_score(5, 100.0) == 5.0
|
||||
assert crap_score(5, 0.0) == 30.0
|
||||
assert crap_score(1, 100.0) == 1.0
|
||||
assert abs(crap_score(8, 45.0) - 18.648) < 0.01
|
||||
assert crap_score(3, None) is None
|
||||
|
||||
|
||||
def test_unscored_rows_sort_after_scored_rows():
|
||||
rows = sort_entries(
|
||||
[
|
||||
entry(name="missing", crap=None, coverage=None),
|
||||
entry(name="mid", crap=5.0),
|
||||
entry(name="worst", crap=30.0),
|
||||
]
|
||||
)
|
||||
assert [row.name for row in rows] == ["worst", "mid", "missing"]
|
||||
|
||||
|
||||
def test_unscored_rows_sort_by_complexity():
|
||||
rows = sort_entries(
|
||||
[
|
||||
entry(name="small", crap=None, coverage=None, complexity=1),
|
||||
entry(name="large", crap=None, coverage=None, complexity=8),
|
||||
]
|
||||
)
|
||||
assert [row.name for row in rows] == ["large", "small"]
|
||||
|
||||
|
||||
def test_report_shows_na_for_missing_coverage():
|
||||
text = format_report([entry(coverage=None, crap=None, complexity=3)])
|
||||
assert "CRAP Report" in text
|
||||
assert "N/A" in text
|
||||
assert "demo.core" in text
|
||||
|
||||
|
||||
def test_edn_uses_crap4clj_keys():
|
||||
text = render_edn(
|
||||
[entry(name="layout", namespace="uml-viewer.layout", complexity=8, coverage=82.0, crap=3.4)]
|
||||
)
|
||||
assert text.startswith("{:entries [")
|
||||
assert ':name "layout"' in text
|
||||
assert ':namespace "uml-viewer.layout"' in text
|
||||
assert ":complexity 8" in text
|
||||
assert ":coverage 82.0" in text
|
||||
assert ":crap 3.4" in text
|
||||
assert text.endswith("}\n")
|
||||
|
||||
|
||||
def test_edn_writes_nil_for_missing_coverage():
|
||||
text = render_edn([entry(coverage=None, crap=None)])
|
||||
assert ":coverage nil" in text
|
||||
assert ":crap nil" in text
|
||||
@@ -0,0 +1,62 @@
|
||||
from crapper.languages.golang import functions_in_source
|
||||
|
||||
BRANCHY = """package sample
|
||||
|
||||
func Branchy(x int, ch chan int) int {
|
||||
if x > 0 && x < 10 {
|
||||
return 1
|
||||
}
|
||||
switch x {
|
||||
case 1, 2:
|
||||
return 2
|
||||
default:
|
||||
return 3
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def test_counts_the_same_decisions_as_crap4go():
|
||||
functions = functions_in_source(BRANCHY, "sample.go")
|
||||
assert [(fn.name, fn.namespace, fn.complexity) for fn in functions] == [
|
||||
("Branchy", "sample", 5)
|
||||
]
|
||||
|
||||
|
||||
def test_methods_are_namespaced_by_receiver_type():
|
||||
source = """package sample
|
||||
|
||||
type Widget struct{}
|
||||
|
||||
func (w *Widget) Run() {
|
||||
for i := 0; i < 10; i++ {
|
||||
}
|
||||
for _, item := range items {
|
||||
}
|
||||
}
|
||||
|
||||
func Simple() int { return 1 }
|
||||
"""
|
||||
functions = functions_in_source(source, "widget.go")
|
||||
assert [(fn.namespace, fn.name, fn.complexity) for fn in functions] == [
|
||||
("sample.Widget", "Run", 3),
|
||||
("sample", "Simple", 1),
|
||||
]
|
||||
|
||||
|
||||
def test_import_path_comes_from_go_mod(tmp_path):
|
||||
(tmp_path / "go.mod").write_text("module github.com/acme/demo\n", encoding="utf-8")
|
||||
source_path = tmp_path / "internal" / "game" / "board.go"
|
||||
source_path.parent.mkdir(parents=True)
|
||||
source_path.write_text(
|
||||
"package game\n\ntype Board struct{}\n\nfunc (b *Board) Place() {\n\tif ready && ok {\n\t\treturn\n\t}\n}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
functions = functions_in_source(
|
||||
source_path.read_text(encoding="utf-8"),
|
||||
"internal/game/board.go",
|
||||
str(tmp_path),
|
||||
)
|
||||
assert [(fn.namespace, fn.name, fn.complexity) for fn in functions] == [
|
||||
("github.com/acme/demo/internal/game.Board", "Place", 3)
|
||||
]
|
||||
@@ -0,0 +1,134 @@
|
||||
from crapper.languages.java import functions_in_source
|
||||
|
||||
SCORE = """
|
||||
class Sample {
|
||||
int score(boolean a, boolean b, int[] values) {
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
}
|
||||
for (int value : values) {
|
||||
}
|
||||
while (a) {
|
||||
a = false;
|
||||
}
|
||||
do {
|
||||
b = false;
|
||||
} while (b);
|
||||
if (a && b || values.length > 0) {
|
||||
}
|
||||
try {
|
||||
return a ? 1 : 0;
|
||||
} catch (RuntimeException ex) {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
NESTED = """
|
||||
class Sample {
|
||||
int nested(boolean a, boolean b, int[] values) {
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
if (a) {
|
||||
}
|
||||
}
|
||||
for (int value : values) {
|
||||
if (b) {
|
||||
}
|
||||
}
|
||||
while (a) {
|
||||
if (b) {
|
||||
}
|
||||
a = false;
|
||||
}
|
||||
do {
|
||||
if (a) {
|
||||
}
|
||||
b = false;
|
||||
} while (b);
|
||||
try {
|
||||
return a ? (b ? 1 : 0) : 2;
|
||||
} catch (RuntimeException ex) {
|
||||
if (values.length > 0) {
|
||||
return values[0];
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
int switched(int value) {
|
||||
switch (value) {
|
||||
case 1:
|
||||
if (value > 0) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
default:
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
ANONYMOUS = """
|
||||
class Sample {
|
||||
int outer() {
|
||||
Runnable runnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (true) {
|
||||
}
|
||||
}
|
||||
};
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def test_counts_the_same_decisions_as_crap4java():
|
||||
methods = functions_in_source(SCORE, "Sample.java")
|
||||
assert [(method.name, method.complexity) for method in methods] == [("score", 10)]
|
||||
|
||||
methods = functions_in_source(NESTED, "Sample.java")
|
||||
assert [(method.name, method.complexity) for method in methods] == [
|
||||
("nested", 13),
|
||||
("switched", 4),
|
||||
]
|
||||
|
||||
|
||||
def test_anonymous_class_methods_are_not_entries_and_do_not_raise_complexity():
|
||||
methods = functions_in_source(ANONYMOUS, "Sample.java")
|
||||
assert [(method.name, method.complexity) for method in methods] == [("outer", 1)]
|
||||
|
||||
|
||||
def test_ignores_constructors_and_names_the_class_for_uml_viewer():
|
||||
source = """
|
||||
package demo.pkg;
|
||||
public class Board {
|
||||
Board() { if (true) {} }
|
||||
public void place(int x) { if (x > 0 && ready) return; }
|
||||
class Inner { void tick() { return; } }
|
||||
}
|
||||
"""
|
||||
methods = functions_in_source(source, "src/demo/pkg/Board.java")
|
||||
assert [(method.namespace, method.name, method.complexity) for method in methods] == [
|
||||
("demo.pkg.Board", "place", 3),
|
||||
("demo.pkg.Board.Inner", "tick", 1),
|
||||
]
|
||||
assert methods[0].jacoco_class == "demo.pkg.Board"
|
||||
assert methods[1].jacoco_class == "demo.pkg.Board$Inner"
|
||||
|
||||
|
||||
def test_keywords_in_comments_and_strings_are_not_decisions():
|
||||
source = """
|
||||
class Sample {
|
||||
int stable() {
|
||||
String text = "if && || ? case default catch";
|
||||
// if && || ? case default catch
|
||||
/* if && || ? case default catch */
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
"""
|
||||
methods = functions_in_source(source, "Sample.java")
|
||||
assert [(method.name, method.complexity) for method in methods] == [("stable", 1)]
|
||||
@@ -0,0 +1,105 @@
|
||||
from crapper.languages.python import functions_in_source
|
||||
|
||||
|
||||
def test_function_method_and_nested_function():
|
||||
source = """
|
||||
def choose(x, ready):
|
||||
if x > 0 and x < 10:
|
||||
return 1
|
||||
elif x == 0 or ready:
|
||||
return 0
|
||||
for i in xs:
|
||||
if i == 2:
|
||||
return i
|
||||
while ready:
|
||||
break
|
||||
try:
|
||||
return x
|
||||
except ValueError:
|
||||
return 0
|
||||
except KeyError:
|
||||
return -1
|
||||
match x:
|
||||
case 1:
|
||||
return 1
|
||||
case 2 | 3:
|
||||
return 2
|
||||
case _:
|
||||
return 0
|
||||
return x if x > 0 else 0
|
||||
|
||||
class Box:
|
||||
def open(self, flag=False):
|
||||
return 1 if flag else 0
|
||||
|
||||
@staticmethod
|
||||
def shut(self):
|
||||
xs = [n for n in range(3) if n]
|
||||
return xs
|
||||
|
||||
def outer():
|
||||
def inner(n):
|
||||
if n:
|
||||
return 1
|
||||
return 0
|
||||
return inner(1)
|
||||
|
||||
async def load():
|
||||
if True:
|
||||
return 1
|
||||
"""
|
||||
functions = functions_in_source(source, "src/demo/box.py", "/proj")
|
||||
assert [(fn.namespace, fn.name, fn.complexity) for fn in functions] == [
|
||||
("demo.box", "choose", 14),
|
||||
("demo.box.Box", "open", 2),
|
||||
("demo.box.Box", "shut", 2),
|
||||
("demo.box", "outer", 2),
|
||||
("demo.box", "load", 2),
|
||||
]
|
||||
|
||||
|
||||
def test_nested_class_method_is_its_own_entry():
|
||||
source = """
|
||||
class Outer:
|
||||
class Inner:
|
||||
def tick(self, ready, ok):
|
||||
if ready and ok:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def factory():
|
||||
class Hidden:
|
||||
def secret(self):
|
||||
if True:
|
||||
return 1
|
||||
return 0
|
||||
return Hidden()
|
||||
"""
|
||||
functions = functions_in_source(source, "src/demo/sample.py", "/proj")
|
||||
assert [(fn.namespace, fn.name, fn.complexity) for fn in functions] == [
|
||||
("demo.sample.Outer.Inner", "tick", 3),
|
||||
("demo.sample", "factory", 1),
|
||||
("demo.sample.Hidden", "secret", 2),
|
||||
]
|
||||
|
||||
|
||||
def test_namespace_strips_roots_src_and_init():
|
||||
source = "def f():\n return 1\n"
|
||||
assert functions_in_source(source, "/proj/src/demo/app.py", "/proj")[0].namespace == "demo.app"
|
||||
assert functions_in_source(source, "./src/demo/app.py", ".")[0].namespace == "demo.app"
|
||||
assert functions_in_source(source, "pkg/src/demo/__init__.py", None)[0].namespace == "demo"
|
||||
assert functions_in_source(source, "__init__.py", None)[0].namespace == "__init__"
|
||||
|
||||
|
||||
def test_strings_comments_and_package_init_do_not_add_decisions():
|
||||
source = '''
|
||||
def literal():
|
||||
"""if and or for while except"""
|
||||
text = "if x and y"
|
||||
# if x: return 1
|
||||
return text
|
||||
'''
|
||||
functions = functions_in_source(source, "src/demo/__init__.py", "/proj")
|
||||
assert [(fn.namespace, fn.name, fn.complexity) for fn in functions] == [
|
||||
("demo", "literal", 1)
|
||||
]
|
||||
@@ -0,0 +1,252 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from crapper.discover import is_test_file
|
||||
from crapper.runners import (
|
||||
_clean_clojure,
|
||||
_ensure_vitest_coverage,
|
||||
_manifest_snapshot,
|
||||
_python_executable,
|
||||
_restore_manifests,
|
||||
_rust_kind,
|
||||
_vitest_version,
|
||||
python_coverage_commands,
|
||||
python_roots,
|
||||
python_sources,
|
||||
run_coverage,
|
||||
rust_coverage_command,
|
||||
rust_modules,
|
||||
typescript_command,
|
||||
typescript_packages,
|
||||
uses_pytest,
|
||||
)
|
||||
|
||||
|
||||
def _write(root: Path, relative: str, source: str) -> Path:
|
||||
path = root / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(source, encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def test_vitest_uses_its_own_coverage_instead_of_c8(tmp_path):
|
||||
_write(
|
||||
tmp_path,
|
||||
"package.json",
|
||||
json.dumps({"scripts": {"test": "vitest run"}}),
|
||||
)
|
||||
_write(tmp_path, "vitest.config.ts", "export default {}\n")
|
||||
source = _write(tmp_path, "src/book.ts", "export function plan() { return 1 }\n")
|
||||
_write(tmp_path, "src/book.test.ts", "import { plan } from './book'\n")
|
||||
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
|
||||
|
||||
|
||||
def test_existing_coverage_script_is_left_alone(tmp_path):
|
||||
_write(
|
||||
tmp_path,
|
||||
"package.json",
|
||||
json.dumps({"scripts": {"coverage": "vitest run --coverage", "test": "vitest run"}}),
|
||||
)
|
||||
command = typescript_command(tmp_path, [], tmp_path / "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"
|
||||
|
||||
|
||||
def test_typescript_package_is_found_from_a_source_file(tmp_path):
|
||||
_write(tmp_path, "web/package.json", json.dumps({"scripts": {"test": "vitest run"}}))
|
||||
source = _write(tmp_path, "web/src/app.ts", "export const n = 1\n")
|
||||
assert typescript_packages(tmp_path, [source]) == [(tmp_path / "web").resolve()]
|
||||
|
||||
|
||||
def test_rust_module_is_the_nearest_cargo_package(tmp_path):
|
||||
_write(tmp_path, "src-tauri/Cargo.toml", '[package]\nname = "bookwriter"\nversion = "0.1.0"\n')
|
||||
lib = _write(tmp_path, "src-tauri/src/lib.rs", "pub fn read_text() {}\n")
|
||||
assert rust_modules(tmp_path, [lib]) == [(tmp_path / "src-tauri").resolve()]
|
||||
|
||||
|
||||
def test_manifests_are_restored_after_a_coverage_install(tmp_path):
|
||||
package_json = tmp_path / "package.json"
|
||||
lock = tmp_path / "package-lock.json"
|
||||
package_json.write_bytes(b'{"name":"demo"}\n')
|
||||
lock.write_bytes(b'{"lockfileVersion":3}\n')
|
||||
snapshot = _manifest_snapshot(tmp_path)
|
||||
package_json.write_bytes(b'{"name":"changed"}\n')
|
||||
lock.unlink()
|
||||
shrink = tmp_path / "npm-shrinkwrap.json"
|
||||
shrink.write_text("{}\n", encoding="utf-8")
|
||||
_restore_manifests(snapshot)
|
||||
assert package_json.read_bytes() == b'{"name":"demo"}\n'
|
||||
assert lock.read_bytes() == b'{"lockfileVersion":3}\n'
|
||||
assert not shrink.exists()
|
||||
|
||||
|
||||
def test_python_project_uses_pytest_and_coverage_lcov(tmp_path):
|
||||
_write(tmp_path, "pyproject.toml", "[tool.pytest.ini_options]\ntestpaths = ['tests']\n")
|
||||
source = _write(tmp_path, "src/demo/box.py", "def choose(x):\n return x\n")
|
||||
_write(tmp_path, "tests/test_box.py", "def test_choose():\n assert True\n")
|
||||
assert python_roots(tmp_path, [source]) == [tmp_path.resolve()]
|
||||
assert uses_pytest(tmp_path)
|
||||
assert python_sources(tmp_path, [source, tmp_path / "tests" / "test_box.py"]) == "src"
|
||||
assert is_test_file(tmp_path / "tests" / "test_box.py")
|
||||
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}"
|
||||
|
||||
|
||||
def test_python_package_without_pytest_uses_unittest(tmp_path):
|
||||
_write(tmp_path, "setup.py", "from setuptools import setup\nsetup(name='demo')\n")
|
||||
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 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}"
|
||||
|
||||
|
||||
def test_vitest_version_and_provider_install(tmp_path, monkeypatch):
|
||||
assert _vitest_version(tmp_path) is None
|
||||
_write(tmp_path, "node_modules/vitest/package.json", "{not json")
|
||||
assert _vitest_version(tmp_path) is None
|
||||
_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"
|
||||
provider.mkdir(parents=True)
|
||||
assert _ensure_vitest_coverage(tmp_path) is True
|
||||
provider.rmdir()
|
||||
monkeypatch.setattr("crapper.runners.run_shell", lambda command, cwd: 1)
|
||||
assert _ensure_vitest_coverage(tmp_path) is False
|
||||
|
||||
|
||||
def test_clean_clojure_keeps_other_language_reports(tmp_path):
|
||||
kept = tmp_path / "target" / "coverage" / "python"
|
||||
kept.mkdir(parents=True)
|
||||
(kept / "lcov.info").write_text("keep", encoding="utf-8")
|
||||
stale = tmp_path / "target" / "coverage" / "lcov.info"
|
||||
stale.write_text("old", encoding="utf-8")
|
||||
_clean_clojure(tmp_path)
|
||||
assert (kept / "lcov.info").is_file()
|
||||
assert not stale.exists()
|
||||
|
||||
|
||||
def test_python_executable_prefers_a_project_venv(tmp_path):
|
||||
binary = tmp_path / ".venv" / "bin" / "python"
|
||||
binary.parent.mkdir(parents=True)
|
||||
binary.write_text("", encoding="utf-8")
|
||||
assert _python_executable(tmp_path) == str(binary)
|
||||
|
||||
|
||||
def test_rust_kind_installs_llvm_cov_when_it_is_missing(monkeypatch):
|
||||
installed = {"llvm": False}
|
||||
|
||||
def which(name):
|
||||
if name == "cargo-llvm-cov" and installed["llvm"]:
|
||||
return "/bin/cargo-llvm-cov"
|
||||
if name == "rustup":
|
||||
return "/bin/rustup"
|
||||
return None
|
||||
|
||||
def shell(command, cwd):
|
||||
if "cargo install" in command:
|
||||
installed["llvm"] = True
|
||||
return 0
|
||||
|
||||
monkeypatch.setattr("crapper.runners.shutil.which", which)
|
||||
monkeypatch.setattr("crapper.runners.run_shell", shell)
|
||||
assert _rust_kind() == "llvm-cov"
|
||||
installed["llvm"] = False
|
||||
monkeypatch.setattr("crapper.runners.run_shell", lambda command, cwd: 1)
|
||||
assert _rust_kind() is None
|
||||
|
||||
|
||||
def test_coverage_commands_are_issued_per_language(tmp_path, monkeypatch):
|
||||
commands: list[str] = []
|
||||
|
||||
def shell(command, cwd):
|
||||
commands.append(command)
|
||||
if "--data-file=" in command:
|
||||
data = Path(command.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 "):
|
||||
return 1
|
||||
if "vitest" in command or command.startswith("cargo "):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
monkeypatch.setattr("crapper.runners.run_shell", shell)
|
||||
monkeypatch.setattr(
|
||||
"crapper.runners.shutil.which",
|
||||
lambda name: "/bin/cargo-llvm-cov" if name == "cargo-llvm-cov" else None,
|
||||
)
|
||||
_write(tmp_path, "deps.edn", "{}\n")
|
||||
_write(tmp_path, "src/demo/core.clj", "(ns demo.core)\n(defn x [])\n")
|
||||
_write(tmp_path, "pom.xml", "<project></project>\n")
|
||||
_write(tmp_path, "src/Board.java", "class Board { int place(){ return 1; } }\n")
|
||||
(tmp_path / "target" / "jacoco.exec").parent.mkdir(parents=True)
|
||||
(tmp_path / "target" / "jacoco.exec").write_text("x", encoding="utf-8")
|
||||
_write(tmp_path, "go.mod", "module example.com/demo\n")
|
||||
_write(tmp_path, "board.go", "package demo\nfunc Place() int { return 1 }\n")
|
||||
profile = tmp_path / "target" / "coverage" / "go" / "coverage.out"
|
||||
profile.parent.mkdir(parents=True)
|
||||
profile.write_text("old", encoding="utf-8")
|
||||
_write(tmp_path, "package.json", json.dumps({"scripts": {"test": "vitest run"}}))
|
||||
_write(tmp_path, "src/ui.ts", "export function view(){ return 1 }\n")
|
||||
(tmp_path / "node_modules" / "@vitest" / "coverage-v8").mkdir(parents=True)
|
||||
_write(tmp_path, "pyproject.toml", "[tool.pytest.ini_options]\n")
|
||||
_write(tmp_path, "src/app.py", "def run():\n return 1\n")
|
||||
_write(tmp_path, "Cargo.toml", '[package]\nname = "demo"\nversion = "0.1.0"\n')
|
||||
_write(tmp_path, "src/lib.rs", "pub fn open() {}\n")
|
||||
files = [
|
||||
tmp_path / "src/demo/core.clj",
|
||||
tmp_path / "src/Board.java",
|
||||
tmp_path / "board.go",
|
||||
tmp_path / "src/ui.ts",
|
||||
tmp_path / "src/app.py",
|
||||
tmp_path / "src/lib.rs",
|
||||
]
|
||||
run_coverage(tmp_path, files, None)
|
||||
text = "\n".join(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
|
||||
assert "go test" in text
|
||||
assert "vitest" in text
|
||||
assert "coverage run" in text
|
||||
assert "cargo llvm-cov" in text
|
||||
run_coverage(tmp_path, files, "echo custom")
|
||||
assert commands[-1] == "echo custom"
|
||||
|
||||
|
||||
def test_missing_build_files_skip_that_language(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr("crapper.runners.run_shell", lambda command, cwd: 0)
|
||||
files = [
|
||||
_write(tmp_path, "src/App.java", "class App {}\n"),
|
||||
_write(tmp_path, "main.go", "package main\n"),
|
||||
_write(tmp_path, "src/app.ts", "export const n = 1\n"),
|
||||
_write(tmp_path, "src/lib.rs", "pub fn open() {}\n"),
|
||||
]
|
||||
run_coverage(tmp_path, files, None)
|
||||
@@ -0,0 +1,95 @@
|
||||
from crapper.languages.rust import functions_in_source
|
||||
|
||||
SOURCE = """
|
||||
pub fn choose(x: i32) -> i32 {
|
||||
if x > 0 && x < 10 {
|
||||
return 1;
|
||||
}
|
||||
match x {
|
||||
1 => 1,
|
||||
2 => 2,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
impl Sample {
|
||||
pub fn run(&self) -> i32 {
|
||||
if self.ok { 1 } else { 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Trait for Sample {
|
||||
fn draw(&mut self) {}
|
||||
}
|
||||
|
||||
mod tests {
|
||||
fn hidden() {
|
||||
if true {}
|
||||
}
|
||||
}
|
||||
|
||||
fn outer() {
|
||||
fn inner() {
|
||||
if true {}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def test_functions_methods_and_skipped_tests(tmp_path):
|
||||
(tmp_path / "Cargo.toml").write_text(
|
||||
'[package]\nname = "demo-game"\nversion = "0.1.0"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
path = tmp_path / "src" / "lib.rs"
|
||||
path.parent.mkdir()
|
||||
path.write_text(SOURCE, encoding="utf-8")
|
||||
functions = functions_in_source(SOURCE, "src/lib.rs", str(tmp_path))
|
||||
assert [(fn.namespace, fn.name, fn.complexity) for fn in functions] == [
|
||||
("demo_game", "choose", 6),
|
||||
("demo_game::Sample", "run", 2),
|
||||
("demo_game::Sample", "draw", 1),
|
||||
("demo_game", "outer", 2),
|
||||
]
|
||||
|
||||
|
||||
def test_impl_for_a_nested_generic_type(tmp_path):
|
||||
(tmp_path / "Cargo.toml").write_text(
|
||||
'[package]\nname = "demo"\nversion = "0.1.0"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
source = "impl Trait for Vec<crate::Sample> {\n fn draw(&self) {}\n}\n"
|
||||
functions = functions_in_source(source, str(tmp_path / "src" / "lib.rs"), str(tmp_path))
|
||||
assert functions[0].name == "draw"
|
||||
assert functions[0].namespace.endswith("Vec") or "Sample" in functions[0].namespace
|
||||
|
||||
|
||||
def test_module_paths_outside_the_usual_layout(tmp_path):
|
||||
(tmp_path / "Cargo.toml").write_text(
|
||||
'[package]\nname = "demo"\nversion = "0.1.0"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
mod = "pub fn open() {}\n"
|
||||
binary = "pub fn main_ok() {}\n"
|
||||
functions_in_source(mod, str(tmp_path / "src" / "game" / "mod.rs"), str(tmp_path))
|
||||
functions = functions_in_source(binary, str(tmp_path / "src" / "bin" / "tool.rs"), str(tmp_path))
|
||||
assert functions[0].namespace == "tool"
|
||||
outside = functions_in_source("pub fn loose() {}\n", "/tmp/loose.rs", str(tmp_path))
|
||||
assert outside[0].name == "loose"
|
||||
bare = functions_in_source("pub fn bare() {}\n", str(tmp_path / "notes.rs"), None)
|
||||
assert bare[0].name == "bare"
|
||||
|
||||
|
||||
def test_nested_module_path(tmp_path):
|
||||
(tmp_path / "Cargo.toml").write_text(
|
||||
'[package]\nname = "demo"\nversion = "0.1.0"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
path = tmp_path / "src" / "game" / "board.rs"
|
||||
path.parent.mkdir(parents=True)
|
||||
source = "pub fn place() -> i32 { if ready { 1 } else { 0 } }\n"
|
||||
path.write_text(source, encoding="utf-8")
|
||||
functions = functions_in_source(source, "src/game/board.rs", str(tmp_path))
|
||||
assert [(fn.namespace, fn.name, fn.complexity) for fn in functions] == [
|
||||
("demo::game::board", "place", 2)
|
||||
]
|
||||
@@ -0,0 +1,53 @@
|
||||
from crapper.languages.typescript import functions_in_source
|
||||
|
||||
|
||||
def test_function_class_method_and_arrow_are_separate_entries():
|
||||
source = """
|
||||
export function choose(x: number): number {
|
||||
if (x > 0 && x < 10) return 1;
|
||||
for (let i = 0; i < x; i++) {
|
||||
if (i === 2) return i;
|
||||
}
|
||||
try {
|
||||
return x;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
return x > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
export class Box {
|
||||
open(flag?: boolean): number {
|
||||
return flag ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
const arrow = (n: number) => (n > 0 ? n : 0);
|
||||
|
||||
function outer() {
|
||||
function inner(n: number) {
|
||||
if (n) return 1;
|
||||
return 0;
|
||||
}
|
||||
return inner(1);
|
||||
}
|
||||
"""
|
||||
functions = functions_in_source(source, "src/demo/box.ts", "/proj")
|
||||
assert [(fn.namespace, fn.name, fn.complexity) for fn in functions] == [
|
||||
("demo.box", "choose", 7),
|
||||
("demo.box.Box", "open", 2),
|
||||
("demo.box", "arrow", 2),
|
||||
("demo.box", "outer", 2),
|
||||
]
|
||||
|
||||
|
||||
def test_tsx_counts_jsx_logic():
|
||||
source = """
|
||||
export function View(ok: boolean, ready: boolean) {
|
||||
return ok && ready ? 1 : 0;
|
||||
}
|
||||
"""
|
||||
functions = functions_in_source(source, "src/ui/view.tsx", "/proj")
|
||||
assert [(fn.namespace, fn.name, fn.complexity) for fn in functions] == [
|
||||
("ui.view", "View", 3)
|
||||
]
|
||||
Reference in New Issue
Block a user