2317 lines
94 KiB
Python
2317 lines
94 KiB
Python
#!/usr/bin/env python3
|
|
"""Unit tests for readability_audit.py.
|
|
|
|
Run with:
|
|
python3 -m unittest scripts.readability_audit_test
|
|
|
|
Tests use temporary fixture trees, temporary git repositories, and in-memory
|
|
baselines to verify:
|
|
- tracked inputs sorting and exclusion
|
|
- language-aware file-kind classification and threshold boundaries
|
|
- multi-language function identity (raw/multiline/comment/heredoc aware)
|
|
- ratchet identity where level drops pass and new/increased identities fail
|
|
- strict baseline schema validation
|
|
- task read-set configuration failures, budget/reason, and total ratchet
|
|
- CLI exit codes and rendered failure output
|
|
- input mode: tracked vs worktree projection (modified/deleted/untracked)
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import textwrap
|
|
import unittest
|
|
|
|
# Ensure the audit module is importable.
|
|
SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, SCRIPTS_DIR)
|
|
import readability_audit as ra
|
|
|
|
AUDIT_SCRIPT = os.path.join(SCRIPTS_DIR, "readability_audit.py")
|
|
|
|
|
|
class _RepoFixture(unittest.TestCase):
|
|
"""Base fixture: a temporary directory plus optional git repository."""
|
|
|
|
prefix = "ra-test-"
|
|
|
|
def setUp(self):
|
|
self.tmpdir = tempfile.mkdtemp(prefix=self.prefix)
|
|
|
|
def tearDown(self):
|
|
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
|
|
|
def _write(self, relpath: str, content: str) -> None:
|
|
full = os.path.join(self.tmpdir, relpath)
|
|
os.makedirs(os.path.dirname(full) or self.tmpdir, exist_ok=True)
|
|
with open(full, "w") as f:
|
|
f.write(content)
|
|
|
|
def _git(self, *args: str) -> subprocess.CompletedProcess:
|
|
return subprocess.run(["git", *args], check=True, cwd=self.tmpdir,
|
|
capture_output=True)
|
|
|
|
def _git_init(self) -> None:
|
|
subprocess.run(["git", "init", "-q", self.tmpdir], check=True,
|
|
capture_output=True)
|
|
self._git("config", "user.email", "test@test")
|
|
self._git("config", "user.name", "Test")
|
|
self._git("add", "-A")
|
|
self._git("commit", "-q", "-m", "init")
|
|
|
|
|
|
# ============================================================
|
|
# TestTrackedInputsSortedAndExcluded
|
|
# ============================================================
|
|
|
|
|
|
class TestTrackedInputsSortedAndExcluded(unittest.TestCase):
|
|
"""Input filtering: tracked languages only, excluded dirs removed, sorted."""
|
|
|
|
def test_tracked_languages_only_and_excluded(self) -> None:
|
|
original = ra._run_git_ls_files_z
|
|
ra._run_git_ls_files_z = lambda: [
|
|
"src/main.go", "src/util.py",
|
|
"proto/gen/generate.pb.go",
|
|
"apps/client/lib/gen/api.dart",
|
|
"build/vendor/lib.go",
|
|
"scripts/run.sh",
|
|
"src/lib.kt",
|
|
"src/view.swift",
|
|
"src/app.dart",
|
|
"src/data.json",
|
|
]
|
|
try:
|
|
tracked = [f for f in ra._run_git_ls_files_z() if not ra._should_exclude(f)]
|
|
tracked_lang = sorted(f for f in tracked if ra._detect_language(f))
|
|
self.assertEqual(tracked_lang, [
|
|
"scripts/run.sh", "src/app.dart", "src/lib.kt",
|
|
"src/main.go", "src/util.py", "src/view.swift",
|
|
])
|
|
self.assertNotIn("src/data.json", tracked_lang)
|
|
finally:
|
|
ra._run_git_ls_files_z = original
|
|
|
|
def test_exclusion_patterns(self) -> None:
|
|
self.assertTrue(ra._should_exclude("proto/gen/foo.go"))
|
|
self.assertTrue(ra._should_exclude("apps/client/lib/gen/bar.dart"))
|
|
self.assertTrue(ra._should_exclude("build/vendor/x.go"))
|
|
self.assertTrue(ra._should_exclude("agent-ops/rules/common/rules-roadmap.md"))
|
|
self.assertTrue(ra._should_exclude("agent-ops/skills/common/plan/SKILL.md"))
|
|
self.assertFalse(ra._should_exclude("agent-ops/skills/project/e2e-smoke/SKILL.md"))
|
|
self.assertFalse(ra._should_exclude("src/main.go"))
|
|
self.assertFalse(ra._should_exclude("scripts/run.sh"))
|
|
|
|
|
|
# ============================================================
|
|
# TestPolicyThresholdsByFileKind [REVIEW_REVIEW_REFACTOR-2]
|
|
# ============================================================
|
|
|
|
|
|
class TestPolicyThresholdsByFileKind(_RepoFixture):
|
|
"""File-kind policy: language-aware test classification and boundaries."""
|
|
|
|
prefix = "ra-test-policy-"
|
|
|
|
def test_classify_file(self) -> None:
|
|
self.assertEqual(ra.classify_file("apps/edge/main.go"), "production")
|
|
self.assertEqual(ra.classify_file("apps/edge/test/foo_test.go"), "test")
|
|
self.assertEqual(ra.classify_file("apps/edge/foo_test.go"), "test")
|
|
self.assertEqual(ra.classify_file("agent-ops/skills/my-skill/SKILL.md"), "skill_entrypoint")
|
|
self.assertEqual(ra.classify_file("agent-ops/skills/other/SKILL.md"), "skill_entrypoint")
|
|
|
|
def test_classify_file_language_aware_test_names(self) -> None:
|
|
"""Every instrumented language reports its own test naming convention."""
|
|
self.assertEqual(ra.classify_file("scripts/readability_audit_test.py"), "test")
|
|
self.assertEqual(ra.classify_file("scripts/test_readability.py"), "test")
|
|
self.assertEqual(ra.classify_file("apps/client/lib/widget_test.dart"), "test")
|
|
self.assertEqual(ra.classify_file("android/app/src/RepoTest.kt"), "test")
|
|
self.assertEqual(ra.classify_file("android/app/src/RepoTests.kt"), "test")
|
|
self.assertEqual(ra.classify_file("ios/Runner/AppTests.swift"), "test")
|
|
self.assertEqual(ra.classify_file("scripts/e2e_test.sh"), "test")
|
|
self.assertEqual(ra.classify_file("scripts/tests/helper.sh"), "test")
|
|
# production files that merely mention test must stay production
|
|
self.assertEqual(ra.classify_file("apps/edge/testing.go"), "production")
|
|
self.assertEqual(ra.classify_file("scripts/readability_audit.py"), "production")
|
|
|
|
def test_classify_project_shell_test_entrypoints(self) -> None:
|
|
"""Real scripts/e2e-*.sh entrypoints are tests; other shell stays production."""
|
|
repo_root = os.path.dirname(SCRIPTS_DIR)
|
|
for path in ("scripts/e2e-smoke.sh", "scripts/e2e-long-context-admission-smoke.sh"):
|
|
self.assertTrue(os.path.isfile(os.path.join(repo_root, path)), path)
|
|
self.assertEqual(ra.classify_file(path), "test", path)
|
|
self.assertEqual(ra._file_threshold_for_kind(ra.classify_file(path)),
|
|
ra.FILE_POLICY["test"], path)
|
|
for path in ("scripts/dev/edge.sh", "agent-ops/bin/sync.sh"):
|
|
self.assertTrue(os.path.isfile(os.path.join(repo_root, path)), path)
|
|
self.assertEqual(ra.classify_file(path), "production", path)
|
|
self.assertEqual(ra._file_threshold_for_kind(ra.classify_file(path)),
|
|
ra.FILE_POLICY["production"], path)
|
|
# the pattern is anchored: neither a bare name nor a nested copy matches
|
|
self.assertEqual(ra.classify_file("scripts/e2e.sh"), "production")
|
|
self.assertEqual(ra.classify_file("tools/scripts/e2e-smoke.sh"), "production")
|
|
|
|
def test_shell_entrypoint_uses_test_threshold_end_to_end(self) -> None:
|
|
"""An 810-line e2e entrypoint reports test 'warning', not production's."""
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
body = "\n".join(f"echo {i}" for i in range(810)) + "\n"
|
|
self._write("scripts/e2e-fixture-smoke.sh", body)
|
|
self._write("scripts/dev/fixture.sh", body)
|
|
result = ra.audit(["scripts/e2e-fixture-smoke.sh", "scripts/dev/fixture.sh"])
|
|
kinds = {e["path"]: e["kind"] for e in result["files"]}
|
|
levels = {v["path"]: v["level"] for v in result["violations"]
|
|
if v["metric"] == "file_loc"}
|
|
self.assertEqual(kinds["scripts/e2e-fixture-smoke.sh"], "test")
|
|
self.assertEqual(kinds["scripts/dev/fixture.sh"], "production")
|
|
self.assertEqual(levels["scripts/e2e-fixture-smoke.sh"], "warning")
|
|
self.assertEqual(levels["scripts/dev/fixture.sh"], "split_review")
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_python_test_file_uses_test_policy(self) -> None:
|
|
"""A 700-line *_test.py is under the test warning threshold, not production's."""
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
body = "\n".join(f"# line {i}" for i in range(700)) + "\n"
|
|
self._write("pkg/thing_test.py", body)
|
|
self._write("pkg/thing.py", body)
|
|
result = ra.audit(["pkg/thing_test.py", "pkg/thing.py"])
|
|
levels = {v["path"]: v["level"] for v in result["violations"]
|
|
if v["metric"] == "file_loc"}
|
|
self.assertNotIn("pkg/thing_test.py", levels)
|
|
self.assertEqual(levels["pkg/thing.py"], "warning")
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_production_file_threshold_exact_and_one_over(self) -> None:
|
|
"""production: warning=500, split_review=800, exception=1000."""
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
for count in (500, 501, 800, 801, 1000, 1001):
|
|
lines = "\n".join(f"line {i}" for i in range(count)) + "\n"
|
|
self._write(f"src/f_{count}.go", lines)
|
|
|
|
files = [f"src/f_{c}.go" for c in (500, 501, 800, 801, 1000, 1001)]
|
|
result = ra.audit(files)
|
|
file_violations = [v for v in result["violations"] if v["metric"] == "file_loc"]
|
|
self.assertEqual(len(file_violations), 5)
|
|
levels_by_path = {v["path"]: v["level"] for v in file_violations}
|
|
self.assertNotIn("src/f_500.go", levels_by_path)
|
|
self.assertEqual(levels_by_path["src/f_501.go"], "warning")
|
|
self.assertEqual(levels_by_path["src/f_800.go"], "warning")
|
|
self.assertEqual(levels_by_path["src/f_801.go"], "split_review")
|
|
self.assertEqual(levels_by_path["src/f_1000.go"], "split_review")
|
|
self.assertEqual(levels_by_path["src/f_1001.go"], "exception")
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_test_file_threshold_exact_and_one_over(self) -> None:
|
|
"""test: warning=800, split_review=1000."""
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
for count in (800, 801, 1000, 1001):
|
|
lines = "\n".join(f"line {i}" for i in range(count)) + "\n"
|
|
self._write(f"test/f_{count}_test.go", lines)
|
|
|
|
files = [f"test/f_{c}_test.go" for c in (800, 801, 1000, 1001)]
|
|
result = ra.audit(files)
|
|
file_violations = [v for v in result["violations"] if v["metric"] == "file_loc"]
|
|
self.assertEqual(len(file_violations), 3)
|
|
levels_by_path = {v["path"]: v["level"] for v in file_violations}
|
|
self.assertNotIn("test/f_800_test.go", levels_by_path)
|
|
self.assertEqual(levels_by_path["test/f_801_test.go"], "warning")
|
|
self.assertEqual(levels_by_path["test/f_1000_test.go"], "warning")
|
|
self.assertEqual(levels_by_path["test/f_1001_test.go"], "split_review")
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_skill_entrypoint_threshold(self) -> None:
|
|
"""skill_entrypoint: warning=300, split_review=500."""
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
for name, count in (("foo", 300), ("bar", 301), ("baz", 500), ("qux", 501)):
|
|
lines = "\n".join(f"line {i}" for i in range(count)) + "\n"
|
|
self._write(f"agent-ops/skills/{name}/SKILL.md", lines)
|
|
|
|
files = [f"agent-ops/skills/{n}/SKILL.md" for n in ("foo", "bar", "baz", "qux")]
|
|
result = ra.audit(files)
|
|
file_violations = [v for v in result["violations"] if v["metric"] == "file_loc"]
|
|
self.assertEqual(len(file_violations), 3)
|
|
levels_by_path = {v["path"]: v["level"] for v in file_violations}
|
|
self.assertNotIn("agent-ops/skills/foo/SKILL.md", levels_by_path)
|
|
self.assertEqual(levels_by_path["agent-ops/skills/bar/SKILL.md"], "warning")
|
|
self.assertEqual(levels_by_path["agent-ops/skills/baz/SKILL.md"], "warning")
|
|
self.assertEqual(levels_by_path["agent-ops/skills/qux/SKILL.md"], "split_review")
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_function_threshold(self) -> None:
|
|
"""function: warning=80, split_review=120."""
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
for count in (79, 81, 120, 121):
|
|
body = "\n".join(f" line{i}" for i in range(count))
|
|
self._write(f"src/fn_{count}.go",
|
|
f"package p\n\nfunc fn{count}() {{\n{body}\n}}\n")
|
|
|
|
files = [f"src/fn_{c}.go" for c in (79, 81, 120, 121)]
|
|
result = ra.audit(files)
|
|
func_violations = [v for v in result["violations"] if v["metric"] == "function_loc"]
|
|
self.assertEqual(len(func_violations), 4)
|
|
levels_by_path = {v["path"]: v["level"] for v in func_violations}
|
|
self.assertEqual(levels_by_path["src/fn_79.go"], "warning")
|
|
self.assertEqual(levels_by_path["src/fn_81.go"], "warning")
|
|
self.assertEqual(levels_by_path["src/fn_120.go"], "split_review")
|
|
self.assertEqual(levels_by_path["src/fn_121.go"], "split_review")
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
|
|
# ============================================================
|
|
# TestMultiLanguageFunctionIdentity [REVIEW_REVIEW_REFACTOR-4]
|
|
# ============================================================
|
|
|
|
|
|
class TestMultiLanguageFunctionIdentity(unittest.TestCase):
|
|
"""Raw/multiline/comment/heredoc aware measurement and qualified identity."""
|
|
|
|
def test_go_qualified_name_with_receiver(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
package main
|
|
|
|
type MyType struct{}
|
|
|
|
func (m *MyType) DoSomething() {
|
|
x := 1
|
|
y := 2
|
|
}
|
|
|
|
func Standalone() {
|
|
z := 3
|
|
}
|
|
""")
|
|
names = {f["name"] for f in ra._extract_functions_go(source, "test.go")}
|
|
self.assertIn("MyType.DoSomething", names)
|
|
self.assertIn("Standalone", names)
|
|
|
|
def test_go_brace_in_string_not_counted(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
package main
|
|
|
|
func WithBraceInString() string {
|
|
s := "hello } world"
|
|
return s
|
|
}
|
|
""")
|
|
funcs = ra._extract_functions_go(source, "test.go")
|
|
self.assertEqual(len(funcs), 1)
|
|
self.assertEqual(funcs[0]["name"], "WithBraceInString")
|
|
self.assertEqual(funcs[0]["loc"], 4)
|
|
|
|
def test_go_raw_string_does_not_end_function_early(self) -> None:
|
|
"""A backtick raw string with a brace must not terminate the function."""
|
|
source = textwrap.dedent("""\
|
|
package main
|
|
|
|
func WithRawString() string {
|
|
s := `line one
|
|
} not a brace
|
|
line three`
|
|
return s
|
|
}
|
|
""")
|
|
funcs = ra._extract_functions_go(source, "test.go")
|
|
self.assertEqual(len(funcs), 1)
|
|
self.assertEqual(funcs[0]["name"], "WithRawString")
|
|
self.assertEqual(funcs[0]["loc"], 6)
|
|
|
|
def test_go_block_comment_ignored(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
package main
|
|
|
|
func WithBlockComment() {
|
|
/* this is a { block comment } with braces */
|
|
x := 1
|
|
}
|
|
""")
|
|
funcs = ra._extract_functions_go(source, "test.go")
|
|
self.assertEqual(len(funcs), 1)
|
|
self.assertEqual(funcs[0]["name"], "WithBlockComment")
|
|
self.assertEqual(funcs[0]["loc"], 4)
|
|
|
|
def test_go_block_comment_does_not_nest(self) -> None:
|
|
"""Go block comments do not nest: the first */ closes the comment."""
|
|
source = textwrap.dedent("""\
|
|
package main
|
|
|
|
func WithComment() {
|
|
/* outer /* inner */
|
|
x := 1
|
|
}
|
|
""")
|
|
funcs = ra._extract_functions_go(source, "test.go")
|
|
self.assertEqual(len(funcs), 1)
|
|
self.assertEqual(funcs[0]["name"], "WithComment")
|
|
self.assertEqual(funcs[0]["loc"], 4)
|
|
|
|
def test_kotlin_nested_block_comment_ignored(self) -> None:
|
|
"""Kotlin comments nest, so an inner */ must not resume brace counting."""
|
|
source = textwrap.dedent("""\
|
|
class Repo {
|
|
fun query(): String {
|
|
/* outer /* inner } */ still comment } */
|
|
return "ok"
|
|
}
|
|
}
|
|
""")
|
|
funcs = ra._extract_functions_kotlin(source, "test.kt")
|
|
self.assertEqual(len(funcs), 1)
|
|
self.assertEqual(funcs[0]["name"], "Repo.query")
|
|
self.assertEqual(funcs[0]["loc"], 4)
|
|
|
|
def test_swift_nested_block_comment_ignored(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
extension Foo {
|
|
func render() -> String {
|
|
/* outer /* inner } */ still comment } */
|
|
return "ok"
|
|
}
|
|
}
|
|
""")
|
|
funcs = ra._extract_functions_swift(source, "test.swift")
|
|
self.assertEqual(len(funcs), 1)
|
|
self.assertEqual(funcs[0]["name"], "Foo.render")
|
|
self.assertEqual(funcs[0]["loc"], 4)
|
|
|
|
def test_shell_multiline_quote_holds_awk_body(self) -> None:
|
|
"""A quoted awk body spans newlines: its braces and callables are text."""
|
|
source = textwrap.dedent("""\
|
|
summarize() {
|
|
awk '
|
|
function reset() {
|
|
count = 0
|
|
}
|
|
/^}/ { reset(); print }
|
|
' "$1"
|
|
echo done
|
|
}
|
|
|
|
helper() {
|
|
echo "hi"
|
|
}
|
|
""")
|
|
funcs = ra._extract_functions_shell(source, "test.sh")
|
|
names = {f["name"] for f in funcs}
|
|
self.assertEqual(names, {"summarize", "helper"})
|
|
by_name = {f["name"]: f for f in funcs}
|
|
self.assertEqual(by_name["summarize"]["loc"], 9)
|
|
self.assertEqual(by_name["helper"]["loc"], 3)
|
|
|
|
def test_shell_multiline_double_quote_preserves_lines(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
emit() {
|
|
printf "%s
|
|
} not a brace
|
|
" "$1"
|
|
echo done
|
|
}
|
|
""")
|
|
funcs = ra._extract_functions_shell(source, "test.sh")
|
|
self.assertEqual(len(funcs), 1)
|
|
self.assertEqual(funcs[0]["name"], "emit")
|
|
self.assertEqual(funcs[0]["loc"], 6)
|
|
|
|
def test_dart_generic_async_function(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
import 'dart:async';
|
|
|
|
Future<void> myAsyncFunc() async {
|
|
await doSomething();
|
|
print('done');
|
|
}
|
|
|
|
String simpleFunc() {
|
|
return 'hello';
|
|
}
|
|
""")
|
|
names = {f["name"] for f in ra._extract_functions_dart(source, "test.dart")}
|
|
self.assertIn("myAsyncFunc", names)
|
|
self.assertIn("simpleFunc", names)
|
|
|
|
def test_dart_string_braces_ignored(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
void main() {
|
|
var s = "hello } world";
|
|
print(s);
|
|
}
|
|
""")
|
|
funcs = ra._extract_functions_dart(source, "test.dart")
|
|
self.assertEqual(len(funcs), 1)
|
|
self.assertEqual(funcs[0]["name"], "main")
|
|
|
|
def test_dart_multiline_string_braces_ignored(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
void withMultiline() {
|
|
var s = '''
|
|
} not a brace
|
|
''';
|
|
print(s);
|
|
}
|
|
""")
|
|
funcs = ra._extract_functions_dart(source, "test.dart")
|
|
self.assertEqual(len(funcs), 1)
|
|
self.assertEqual(funcs[0]["name"], "withMultiline")
|
|
self.assertEqual(funcs[0]["loc"], 6)
|
|
|
|
def test_dart_class_scope_identity(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
class Repo {
|
|
void save() {
|
|
print('a');
|
|
}
|
|
}
|
|
|
|
class Cache {
|
|
void save() {
|
|
print('b');
|
|
}
|
|
}
|
|
""")
|
|
names = {f["name"] for f in ra._extract_functions_dart(source, "test.dart")}
|
|
self.assertEqual(names, {"Repo.save", "Cache.save"})
|
|
|
|
def test_dart_callback_type_is_not_a_function(self) -> None:
|
|
"""`void Function(...)` declares a callback type, never a callable named Function."""
|
|
source = textwrap.dedent("""\
|
|
class Button {
|
|
void Function() onTap;
|
|
void Function(String value)? onChanged;
|
|
|
|
void register(void Function() cb) {
|
|
onTap = cb;
|
|
}
|
|
}
|
|
""")
|
|
funcs = ra._extract_functions_dart(source, "test.dart")
|
|
names = {f["name"] for f in funcs}
|
|
self.assertEqual(names, {"Button.register"})
|
|
self.assertEqual(funcs[0]["loc"], 3)
|
|
|
|
def test_dart_body_less_declaration_is_not_a_function(self) -> None:
|
|
"""An abstract declaration has no body to measure."""
|
|
source = textwrap.dedent("""\
|
|
abstract class Repo {
|
|
void save();
|
|
Future<void> load();
|
|
}
|
|
|
|
class FileRepo extends Repo {
|
|
void save() {
|
|
print('saved');
|
|
}
|
|
}
|
|
""")
|
|
names = {f["name"] for f in ra._extract_functions_dart(source, "test.dart")}
|
|
self.assertEqual(names, {"FileRepo.save"})
|
|
|
|
def test_dart_expression_body_is_measured(self) -> None:
|
|
"""A `=>` body is a real callable and spans to its terminating `;`."""
|
|
source = textwrap.dedent("""\
|
|
class View {
|
|
Widget build() => Column(
|
|
children: [
|
|
Text('a'),
|
|
],
|
|
);
|
|
}
|
|
""")
|
|
funcs = ra._extract_functions_dart(source, "test.dart")
|
|
self.assertEqual([f["name"] for f in funcs], ["View.build"])
|
|
self.assertEqual(funcs[0]["loc"], 5)
|
|
self.assertEqual((funcs[0]["start"], funcs[0]["end"]), (2, 6))
|
|
|
|
def test_shell_body_less_call_is_not_a_function(self) -> None:
|
|
"""`name()` with no body is not a declaration; the real one still is."""
|
|
source = textwrap.dedent("""\
|
|
#!/bin/sh
|
|
usage() {
|
|
echo "usage"
|
|
}
|
|
|
|
if [ -z "$1" ]; then
|
|
usage
|
|
fi
|
|
""")
|
|
names = {f["name"] for f in ra._extract_functions_shell(source, "test.sh")}
|
|
self.assertEqual(names, {"usage"})
|
|
|
|
def test_shell_next_line_brace_body(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
run_all()
|
|
{
|
|
echo "a"
|
|
}
|
|
""")
|
|
funcs = ra._extract_functions_shell(source, "test.sh")
|
|
self.assertEqual(len(funcs), 1)
|
|
self.assertEqual(funcs[0]["name"], "run_all")
|
|
self.assertEqual(funcs[0]["loc"], 4)
|
|
|
|
def test_next_line_type_brace_keeps_scope_identity(self) -> None:
|
|
"""A type whose body opener is on a later line still qualifies its methods."""
|
|
dart = textwrap.dedent("""\
|
|
class Repo
|
|
extends Base
|
|
{
|
|
void save() {
|
|
print('a');
|
|
}
|
|
}
|
|
""")
|
|
self.assertEqual({f["name"] for f in ra._extract_functions_dart(dart, "t.dart")},
|
|
{"Repo.save"})
|
|
|
|
kotlin = textwrap.dedent("""\
|
|
class Repo(
|
|
private val db: Db,
|
|
) {
|
|
fun save(): Int {
|
|
return 1
|
|
}
|
|
}
|
|
""")
|
|
self.assertEqual({f["name"] for f in ra._extract_functions_kotlin(kotlin, "t.kt")},
|
|
{"Repo.save"})
|
|
|
|
swift = textwrap.dedent("""\
|
|
struct Repo
|
|
{
|
|
func save() {
|
|
print("a")
|
|
}
|
|
}
|
|
""")
|
|
self.assertEqual({f["name"] for f in ra._extract_functions_swift(swift, "t.swift")},
|
|
{"Repo.save"})
|
|
|
|
def test_go_multiline_signature_with_receiver(self) -> None:
|
|
"""A receiver clause is not the header: the parameter list is."""
|
|
source = textwrap.dedent("""\
|
|
package main
|
|
|
|
func (m *MyType) DoSomething(
|
|
a int,
|
|
b string,
|
|
) error {
|
|
return nil
|
|
}
|
|
""")
|
|
funcs = ra._extract_functions_go(source, "test.go")
|
|
self.assertEqual([f["name"] for f in funcs], ["MyType.DoSomething"])
|
|
self.assertEqual(funcs[0]["loc"], 6)
|
|
|
|
def test_kotlin_function_name_and_property_excluded(self) -> None:
|
|
"""Kotlin properties are not functions; only fun declarations count."""
|
|
source = textwrap.dedent("""\
|
|
package com.example
|
|
|
|
fun calculate(a: Int, b: Int): Int {
|
|
return a + b
|
|
}
|
|
|
|
val constant = 42
|
|
var mutable = 0
|
|
""")
|
|
names = {f["name"] for f in ra._extract_functions_kotlin(source, "test.kt")}
|
|
self.assertEqual(names, {"calculate"})
|
|
|
|
def test_kotlin_multiline_raw_string_and_scope_identity(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
class Repo {
|
|
fun query(): String {
|
|
val sql = \"\"\"
|
|
SELECT } FROM t
|
|
\"\"\"
|
|
return sql
|
|
}
|
|
}
|
|
""")
|
|
funcs = ra._extract_functions_kotlin(source, "test.kt")
|
|
self.assertEqual(len(funcs), 1)
|
|
self.assertEqual(funcs[0]["name"], "Repo.query")
|
|
self.assertEqual(funcs[0]["loc"], 6)
|
|
|
|
def test_swift_function_name_and_property_excluded(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
func fetchData() {
|
|
print("fetching")
|
|
}
|
|
|
|
var counter = 0
|
|
let limit = 10
|
|
""")
|
|
names = {f["name"] for f in ra._extract_functions_swift(source, "test.swift")}
|
|
self.assertEqual(names, {"fetchData"})
|
|
|
|
def test_swift_multiline_string_and_extension_identity(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
extension Foo {
|
|
func render() -> String {
|
|
let text = \"\"\"
|
|
} not a brace
|
|
\"\"\"
|
|
return text
|
|
}
|
|
}
|
|
""")
|
|
funcs = ra._extract_functions_swift(source, "test.swift")
|
|
self.assertEqual(len(funcs), 1)
|
|
self.assertEqual(funcs[0]["name"], "Foo.render")
|
|
self.assertEqual(funcs[0]["loc"], 6)
|
|
|
|
def test_shell_function_name(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
#!/bin/sh
|
|
my_function() {
|
|
echo "hello"
|
|
}
|
|
|
|
another_function() {
|
|
echo "world"
|
|
}
|
|
""")
|
|
names = {f["name"] for f in ra._extract_functions_shell(source, "test.sh")}
|
|
self.assertIn("my_function", names)
|
|
self.assertIn("another_function", names)
|
|
|
|
def test_shell_heredoc_braces_ignored(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
generate_config() {
|
|
cat <<EOF
|
|
{
|
|
"a": 1
|
|
}
|
|
EOF
|
|
echo done
|
|
}
|
|
""")
|
|
funcs = ra._extract_functions_shell(source, "test.sh")
|
|
self.assertEqual(len(funcs), 1)
|
|
self.assertEqual(funcs[0]["name"], "generate_config")
|
|
self.assertEqual(funcs[0]["loc"], 8)
|
|
|
|
def test_kotlin_overload_identity_is_signature_qualified(self) -> None:
|
|
"""Colliding overloads split by signature; unique callables keep scope.name."""
|
|
source = textwrap.dedent("""\
|
|
class C {
|
|
fun run(value: Int) {
|
|
println(value)
|
|
}
|
|
|
|
fun run(value: String) {
|
|
println(value)
|
|
}
|
|
|
|
fun once() {
|
|
println(1)
|
|
}
|
|
}
|
|
""")
|
|
names = {f["name"] for f in ra._extract_functions_kotlin(source, "t.kt")}
|
|
self.assertEqual(names, {"C.run(value: Int)", "C.run(value: String)", "C.once"})
|
|
|
|
def test_swift_overload_identity_is_signature_qualified(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
struct C {
|
|
func run(value: Int) {
|
|
print(value)
|
|
}
|
|
|
|
func run(value: String) {
|
|
print(value)
|
|
}
|
|
}
|
|
""")
|
|
names = {f["name"] for f in ra._extract_functions_swift(source, "t.swift")}
|
|
self.assertEqual(names, {"C.run(value: Int)", "C.run(value: String)"})
|
|
|
|
def test_dart_overload_identity_is_signature_qualified(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
class C {
|
|
void run(int value) {
|
|
print(value);
|
|
}
|
|
|
|
void run(String value) {
|
|
print(value);
|
|
}
|
|
}
|
|
""")
|
|
names = {f["name"] for f in ra._extract_functions_dart(source, "t.dart")}
|
|
self.assertEqual(names, {"C.run(int value)", "C.run(String value)"})
|
|
|
|
def test_go_receiver_overload_identity_excludes_receiver_clause(self) -> None:
|
|
"""Same method name on two receivers stays distinct without a signature."""
|
|
source = textwrap.dedent("""\
|
|
package main
|
|
|
|
func (a *A) Run(x int) {
|
|
_ = x
|
|
}
|
|
|
|
func (b *B) Run(x string) {
|
|
_ = x
|
|
}
|
|
""")
|
|
names = {f["name"] for f in ra._extract_functions_go(source, "t.go")}
|
|
self.assertEqual(names, {"A.Run", "B.Run"})
|
|
|
|
def test_overload_identity_is_deterministic(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
class C {
|
|
fun run(value: Int) {
|
|
println(value)
|
|
}
|
|
|
|
fun run(value: String) {
|
|
println(value)
|
|
}
|
|
}
|
|
""")
|
|
rendered = {json.dumps(ra._extract_functions_kotlin(source, "t.kt"), sort_keys=True)
|
|
for _ in range(3)}
|
|
self.assertEqual(len(rendered), 1)
|
|
|
|
def test_python_property_and_setter_identity_is_split(self) -> None:
|
|
"""A property/setter pair shares scope.name and must not collide."""
|
|
source = textwrap.dedent("""\
|
|
class C:
|
|
@property
|
|
def value(self):
|
|
return self._v
|
|
|
|
@value.setter
|
|
def value(self, v):
|
|
self._v = v
|
|
""")
|
|
names = {f["name"] for f in ra._extract_functions_python(source, "t.py")}
|
|
self.assertEqual(names, {"C.value(self)", "C.value(self, v)"})
|
|
|
|
def test_identical_signature_collision_gets_stable_ordinal(self) -> None:
|
|
"""When signatures tie too, position breaks the tie deterministically."""
|
|
source = textwrap.dedent("""\
|
|
class C:
|
|
if FLAG:
|
|
def run(self):
|
|
return 1
|
|
else:
|
|
def run(self):
|
|
return 2
|
|
""")
|
|
funcs = ra._extract_functions_python(source, "t.py")
|
|
self.assertEqual(sorted(f["name"] for f in funcs),
|
|
["C.run(self)#1", "C.run(self)#2"])
|
|
first = next(f for f in funcs if f["name"] == "C.run(self)#1")
|
|
second = next(f for f in funcs if f["name"] == "C.run(self)#2")
|
|
self.assertLess(first["start"], second["start"])
|
|
|
|
def test_extracted_functions_expose_no_internal_keys(self) -> None:
|
|
"""The signature is an identity input, not part of the report schema."""
|
|
source = "class C {\n fun run(a: Int) {\n println(a)\n }\n}\n"
|
|
for fn in ra._extract_functions_kotlin(source, "t.kt"):
|
|
self.assertEqual(set(fn), {"name", "loc", "start", "end"})
|
|
|
|
def test_python_scope_qualified_identity(self) -> None:
|
|
"""Python classes are not functions and methods carry their class scope."""
|
|
source = textwrap.dedent("""\
|
|
class Foo:
|
|
def bar(self):
|
|
pass
|
|
|
|
def baz(self):
|
|
return 1
|
|
|
|
def standalone():
|
|
x = 1
|
|
y = 2
|
|
""")
|
|
names = {f["name"] for f in ra._extract_functions_python(source, "test.py")}
|
|
self.assertNotIn("Foo", names)
|
|
self.assertEqual(names, {"Foo.bar", "Foo.baz", "standalone"})
|
|
|
|
def test_python_duplicate_method_names_stay_distinct(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
class A:
|
|
def run(self):
|
|
pass
|
|
|
|
class B:
|
|
def run(self):
|
|
x = 1
|
|
return x
|
|
|
|
def run():
|
|
pass
|
|
""")
|
|
names = {f["name"] for f in ra._extract_functions_python(source, "test.py")}
|
|
self.assertEqual(names, {"A.run", "B.run", "run"})
|
|
|
|
def test_python_string_braces_ignored(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
def with_brace():
|
|
s = "hello } world"
|
|
return s
|
|
""")
|
|
funcs = ra._extract_functions_python(source, "test.py")
|
|
self.assertEqual(len(funcs), 1)
|
|
self.assertEqual(funcs[0]["name"], "with_brace")
|
|
|
|
def test_kotlin_multiline_expression_body_spans_full_expression(self) -> None:
|
|
"""An `=` body ends at the expression, not at the declaration line."""
|
|
source = textwrap.dedent("""\
|
|
class C {
|
|
fun compute() =
|
|
listOf(
|
|
1,
|
|
2,
|
|
)
|
|
|
|
fun five() = 5
|
|
|
|
fun call() = compute()
|
|
.size
|
|
}
|
|
""")
|
|
by_name = {f["name"]: f
|
|
for f in ra._extract_functions_kotlin(source, "test.kt")}
|
|
self.assertEqual(set(by_name), {"C.compute", "C.five", "C.call"})
|
|
|
|
self.assertEqual(by_name["C.compute"]["start"], 2)
|
|
self.assertEqual(by_name["C.compute"]["end"], 6)
|
|
self.assertEqual(by_name["C.compute"]["loc"], 5)
|
|
|
|
# A one-line expression body stays exactly one line.
|
|
self.assertEqual(by_name["C.five"]["start"], 8)
|
|
self.assertEqual(by_name["C.five"]["end"], 8)
|
|
self.assertEqual(by_name["C.five"]["loc"], 1)
|
|
|
|
# A leading chain line continues the declaration-line expression.
|
|
self.assertEqual(by_name["C.call"]["start"], 10)
|
|
self.assertEqual(by_name["C.call"]["end"], 11)
|
|
self.assertEqual(by_name["C.call"]["loc"], 2)
|
|
|
|
def test_kotlin_depth_zero_expression_continuations_span_full_expression(self) -> None:
|
|
"""Depth-0 chain and operator continuations span to the real end line."""
|
|
source = textwrap.dedent("""\
|
|
class C {
|
|
fun call() = compute()
|
|
.size
|
|
fun total() = 1 +
|
|
2
|
|
fun done() = 3
|
|
}
|
|
""")
|
|
by_name = {f["name"]: f
|
|
for f in ra._extract_functions_kotlin(source, "test.kt")}
|
|
self.assertEqual(set(by_name), {"C.call", "C.total", "C.done"})
|
|
|
|
# Leading `.size` chain belongs to the declaration-line expression.
|
|
self.assertEqual(by_name["C.call"]["start"], 2)
|
|
self.assertEqual(by_name["C.call"]["end"], 3)
|
|
self.assertEqual(by_name["C.call"]["loc"], 2)
|
|
|
|
# A trailing binary operator keeps the expression open one more line.
|
|
self.assertEqual(by_name["C.total"]["start"], 4)
|
|
self.assertEqual(by_name["C.total"]["end"], 5)
|
|
self.assertEqual(by_name["C.total"]["loc"], 2)
|
|
|
|
# The sibling after a continuation is never swallowed.
|
|
self.assertEqual(by_name["C.done"]["start"], 6)
|
|
self.assertEqual(by_name["C.done"]["end"], 6)
|
|
self.assertEqual(by_name["C.done"]["loc"], 1)
|
|
|
|
def test_kotlin_depth_zero_operator_grammar_continuations_span_full_expression(self) -> None:
|
|
"""Comparison/word/range/infix and leading operators span to the end line."""
|
|
source = textwrap.dedent("""\
|
|
class C {
|
|
infix fun Int.join(other: Int) = this + other
|
|
infix fun String.then(other: Int) = other
|
|
fun less() = 1 <
|
|
2
|
|
fun lessNoSpace() = 1<
|
|
2
|
|
fun greaterNoSpace() = 2>
|
|
1
|
|
fun contained() = 1 in
|
|
listOf(1)
|
|
fun typed(value: Any) = value is
|
|
String
|
|
fun cast(value: Any) = value as
|
|
String
|
|
fun range() = 1..<
|
|
3
|
|
fun custom() = 1 join
|
|
2
|
|
fun stringInfix() = "x" then
|
|
1
|
|
fun leadingLogical() = true
|
|
&& false
|
|
fun leadingCast(value: Any) = value
|
|
as String
|
|
fun genericCast(value: Any) = value as List<Int>
|
|
fun genericTyped(value: Any) = value is Map<String, List<Int>>
|
|
fun plain(value: Any) = value
|
|
fun done() = 3
|
|
}
|
|
""")
|
|
by_name = {f["name"]: f
|
|
for f in ra._extract_functions_kotlin(source, "test.kt")}
|
|
self.assertEqual(set(by_name), {
|
|
"C.Int.join", "C.String.then", "C.less", "C.lessNoSpace",
|
|
"C.greaterNoSpace", "C.contained", "C.typed", "C.cast",
|
|
"C.range", "C.custom", "C.stringInfix", "C.leadingLogical",
|
|
"C.leadingCast", "C.genericCast", "C.genericTyped", "C.plain",
|
|
"C.done",
|
|
})
|
|
|
|
# Trailing comparison (with or without lexical spacing), containment/
|
|
# type test/cast, open range, and custom infix with identifier or
|
|
# string literal left operand, plus newline-before logical/cast
|
|
# operators, each keep the expression open exactly one more line.
|
|
expected_spans = {
|
|
"C.less": (4, 5),
|
|
"C.lessNoSpace": (6, 7),
|
|
"C.greaterNoSpace": (8, 9),
|
|
"C.contained": (10, 11),
|
|
"C.typed": (12, 13),
|
|
"C.cast": (14, 15),
|
|
"C.range": (16, 17),
|
|
"C.custom": (18, 19),
|
|
"C.stringInfix": (20, 21),
|
|
"C.leadingLogical": (22, 23),
|
|
"C.leadingCast": (24, 25),
|
|
}
|
|
for name, (start, end) in expected_spans.items():
|
|
self.assertEqual(by_name[name]["start"], start, name)
|
|
self.assertEqual(by_name[name]["end"], end, name)
|
|
self.assertEqual(by_name[name]["loc"], 2, name)
|
|
|
|
# Complete infix/generic/simple bodies stay one line, and no sibling
|
|
# after either a generic closer or comparison is swallowed.
|
|
expected_single_line_spans = {
|
|
"C.Int.join": 2,
|
|
"C.String.then": 3,
|
|
"C.genericCast": 26,
|
|
"C.genericTyped": 27,
|
|
"C.plain": 28,
|
|
"C.done": 29,
|
|
}
|
|
for name, line in expected_single_line_spans.items():
|
|
actual = (by_name[name]["start"], by_name[name]["end"], by_name[name]["loc"])
|
|
self.assertEqual(actual, (line, line, 1), name)
|
|
|
|
def test_kotlin_generic_type_projection_closers_preserve_siblings(self) -> None:
|
|
"""Complete Kotlin type projections do not turn their `>` into comparisons."""
|
|
source = textwrap.dedent("""\
|
|
class C {
|
|
fun functionType(value: Any) = value as List<(Int) -> String>
|
|
fun parenthesized(value: Any) = value as List<(String)>
|
|
fun annotated(value: Any) = value as List<@Ann String>
|
|
fun nonNull(value: Any) = value as List<T & Any>
|
|
fun escaped(value: Any) = value as List<`when`>
|
|
fun genericComparison(value: Any) = value as List<Int> >
|
|
value
|
|
fun done() = 3
|
|
}
|
|
""")
|
|
by_name = {f["name"]: f
|
|
for f in ra._extract_functions_kotlin(source, "test.kt")}
|
|
expected = {
|
|
"C.functionType": (2, 2, 1),
|
|
"C.parenthesized": (3, 3, 1),
|
|
"C.annotated": (4, 4, 1),
|
|
"C.nonNull": (5, 5, 1),
|
|
"C.escaped": (6, 6, 1),
|
|
"C.genericComparison": (7, 8, 2),
|
|
"C.done": (9, 9, 1),
|
|
}
|
|
self.assertEqual(set(by_name), set(expected))
|
|
for name, span in expected.items():
|
|
actual = (by_name[name]["start"], by_name[name]["end"], by_name[name]["loc"])
|
|
self.assertEqual(actual, span, name)
|
|
|
|
def test_kotlin_qualified_annotation_type_projection_closer_preserves_siblings(self) -> None:
|
|
"""Qualified annotations keep their outer generic closer local."""
|
|
source = textwrap.dedent("""\
|
|
class C {
|
|
fun qualified(value: Any) = value as @com.example.Ann("reason") List<String>
|
|
fun safeCast(value: Any) = value as?@com.example.Ann("reason") List<String>
|
|
fun typed(value: Any) = value is @com.example.Ann("reason") List<String>
|
|
fun simple(value: Any) = value as @com.example.Ann List<String>
|
|
fun simpleSafeCast(value: Any) = value as?@com.example.Ann List<String>
|
|
fun simpleTyped(value: Any) = value is @com.example.Ann List<String>
|
|
fun simpleMultiple(value: Any) = value as @Ann @com.example.Other List<String>
|
|
fun constructorThenSimple(value: Any) = value as @Ann("reason") @com.example.Other List<String>
|
|
fun simpleThenConstructor(value: Any) = value as @Ann @com.example.Other("reason") List<String>
|
|
fun constructorMultiple(value: Any) = value as @Ann("one") @com.example.Other("two") List<String>
|
|
fun annotationArgument(value: Any) = value as List<@com.example.Ann([value as String, 1 > 0]) String>
|
|
fun afterOperator(value: Any) = value as
|
|
@com.example.Ann List<String>
|
|
fun afterAnnotation(value: Any) = value as? @com.example.Ann
|
|
List<String>
|
|
fun betweenAnnotations(value: Any) = value is @Ann
|
|
@com.example.Other List<String>
|
|
fun afterConstructor(value: Any) = value !is @com.example.Ann("reason")
|
|
List<String>
|
|
fun mixedMultiline(value: Any) = value as @Ann
|
|
@com.example.Other("reason")
|
|
List<String>
|
|
fun done() = 3
|
|
}
|
|
""")
|
|
by_name = {f["name"]: f
|
|
for f in ra._extract_functions_kotlin(source, "test.kt")}
|
|
expected = {
|
|
"C.qualified": (2, 2, 1),
|
|
"C.safeCast": (3, 3, 1),
|
|
"C.typed": (4, 4, 1),
|
|
"C.simple": (5, 5, 1),
|
|
"C.simpleSafeCast": (6, 6, 1),
|
|
"C.simpleTyped": (7, 7, 1),
|
|
"C.simpleMultiple": (8, 8, 1),
|
|
"C.constructorThenSimple": (9, 9, 1),
|
|
"C.simpleThenConstructor": (10, 10, 1),
|
|
"C.constructorMultiple": (11, 11, 1),
|
|
"C.annotationArgument": (12, 12, 1),
|
|
"C.afterOperator": (13, 14, 2),
|
|
"C.afterAnnotation": (15, 16, 2),
|
|
"C.betweenAnnotations": (17, 18, 2),
|
|
"C.afterConstructor": (19, 20, 2),
|
|
"C.mixedMultiline": (21, 23, 3),
|
|
"C.done": (24, 24, 1),
|
|
}
|
|
self.assertEqual(set(by_name), set(expected))
|
|
for name, span in expected.items():
|
|
actual = (by_name[name]["start"], by_name[name]["end"], by_name[name]["loc"])
|
|
self.assertEqual(actual, span, name)
|
|
|
|
def test_kotlin_adjacent_safe_cast_generic_closer_preserves_sibling(self) -> None:
|
|
"""`as?` accepts an adjacent type name but identifier tails do not."""
|
|
source = textwrap.dedent("""\
|
|
class C {
|
|
fun adjacent(value: Any) = value as?List<Int>
|
|
fun spaced(value: Any) = value as? List<Int>
|
|
fun done() = 3
|
|
}
|
|
""")
|
|
by_name = {f["name"]: f
|
|
for f in ra._extract_functions_kotlin(source, "test.kt")}
|
|
expected = {
|
|
"C.adjacent": (2, 2, 1),
|
|
"C.spaced": (3, 3, 1),
|
|
"C.done": (4, 4, 1),
|
|
}
|
|
self.assertEqual(set(by_name), set(expected))
|
|
for name, span in expected.items():
|
|
actual = (by_name[name]["start"], by_name[name]["end"], by_name[name]["loc"])
|
|
self.assertEqual(actual, span, name)
|
|
self.assertFalse(ra._kotlin_trailing_generic_type_closer("value asList<Int>"))
|
|
self.assertFalse(ra._kotlin_trailing_generic_type_closer("value isReady<Int>"))
|
|
|
|
def test_kotlin_multiline_generic_type_arguments_preserve_span_and_siblings(self) -> None:
|
|
"""Kotlin generic arguments stay open through their outer closer."""
|
|
source = textwrap.dedent("""\
|
|
class C {
|
|
fun simple(value: Any) = value as List<
|
|
Int
|
|
>
|
|
fun nested(value: Any) = value as Map<
|
|
String,
|
|
List<Int>
|
|
>
|
|
fun done() = 3
|
|
}
|
|
""")
|
|
by_name = {f["name"]: f
|
|
for f in ra._extract_functions_kotlin(source, "test.kt")}
|
|
expected = {
|
|
"C.simple": (2, 4, 3),
|
|
"C.nested": (5, 8, 4),
|
|
"C.done": (9, 9, 1),
|
|
}
|
|
self.assertEqual(set(by_name), set(expected))
|
|
for name, span in expected.items():
|
|
actual = (by_name[name]["start"], by_name[name]["end"], by_name[name]["loc"])
|
|
self.assertEqual(actual, span, name)
|
|
|
|
def test_shell_escaped_and_operator_comment_braces_are_ignored(self) -> None:
|
|
"""`:;# }` is a comment and `\\}` is a literal: neither closes a body."""
|
|
source = textwrap.dedent("""\
|
|
close_brace() {
|
|
:;# }
|
|
echo done
|
|
}
|
|
|
|
escaped() {
|
|
echo \\}
|
|
echo done
|
|
}
|
|
""")
|
|
by_name = {f["name"]: f
|
|
for f in ra._extract_functions_shell(source, "test.sh")}
|
|
self.assertEqual(set(by_name), {"close_brace", "escaped"})
|
|
|
|
self.assertEqual(by_name["close_brace"]["start"], 1)
|
|
self.assertEqual(by_name["close_brace"]["end"], 4)
|
|
self.assertEqual(by_name["close_brace"]["loc"], 4)
|
|
|
|
self.assertEqual(by_name["escaped"]["start"], 6)
|
|
self.assertEqual(by_name["escaped"]["end"], 9)
|
|
self.assertEqual(by_name["escaped"]["loc"], 4)
|
|
|
|
def test_shell_parameter_expansion_and_quotes_still_scan(self) -> None:
|
|
"""Token-aware blanking leaves `${...}` and quoted `#` lexically intact."""
|
|
source = textwrap.dedent("""\
|
|
expand() {
|
|
echo "${VAR}"
|
|
echo "${OTHER:-#default}"
|
|
local hash='#not-a-comment'
|
|
echo "$hash"
|
|
}
|
|
""")
|
|
funcs = ra._extract_functions_shell(source, "test.sh")
|
|
self.assertEqual(len(funcs), 1)
|
|
self.assertEqual(funcs[0]["name"], "expand")
|
|
self.assertEqual(funcs[0]["loc"], 6)
|
|
|
|
def test_shell_hash_comment_boundary_preserves_function_close(self) -> None:
|
|
"""A non-comment `#` never erases a same-line function close."""
|
|
source = textwrap.dedent("""\
|
|
hash_word() {
|
|
echo foo#bar; }
|
|
trim() {
|
|
echo ${value#x}; }
|
|
next() {
|
|
echo done
|
|
}
|
|
""")
|
|
by_name = {f["name"]: f
|
|
for f in ra._extract_functions_shell(source, "test.sh")}
|
|
self.assertEqual(set(by_name), {"hash_word", "trim", "next"})
|
|
|
|
# `#` inside a word is a token, so `; }` still closes the body.
|
|
self.assertEqual(by_name["hash_word"]["start"], 1)
|
|
self.assertEqual(by_name["hash_word"]["end"], 2)
|
|
self.assertEqual(by_name["hash_word"]["loc"], 2)
|
|
|
|
# `#` inside `${...}` is an expansion operator, not a comment.
|
|
self.assertEqual(by_name["trim"]["start"], 3)
|
|
self.assertEqual(by_name["trim"]["end"], 4)
|
|
self.assertEqual(by_name["trim"]["loc"], 2)
|
|
|
|
# The sibling after the same-line closes keeps its own identity.
|
|
self.assertEqual(by_name["next"]["start"], 5)
|
|
self.assertEqual(by_name["next"]["end"], 7)
|
|
self.assertEqual(by_name["next"]["loc"], 3)
|
|
|
|
def test_go_type_braces_do_not_open_function_body(self) -> None:
|
|
"""Generic constraint and result-type braces are not the body opener."""
|
|
source = textwrap.dedent("""\
|
|
package p
|
|
|
|
func Map[T interface{ ~int | ~string }](in []T) []T {
|
|
return in
|
|
}
|
|
|
|
func Meta() (out struct{ A int }) {
|
|
return out
|
|
}
|
|
""")
|
|
by_name = {f["name"]: f for f in ra._extract_functions_go(source, "test.go")}
|
|
self.assertEqual(set(by_name), {"Map", "Meta"})
|
|
|
|
# The generic parameter list is not the signature; the value list is.
|
|
lines = source.splitlines()
|
|
self.assertEqual(
|
|
ra._declaration_signature(lines, 2, lines[2].index("[")), "(in []T)")
|
|
self.assertEqual(
|
|
ra._declaration_signature(lines, 6, lines[6].index("(")), "()")
|
|
|
|
self.assertEqual(by_name["Map"]["start"], 3)
|
|
self.assertEqual(by_name["Map"]["end"], 5)
|
|
self.assertEqual(by_name["Map"]["loc"], 3)
|
|
|
|
self.assertEqual(by_name["Meta"]["start"], 7)
|
|
self.assertEqual(by_name["Meta"]["end"], 9)
|
|
self.assertEqual(by_name["Meta"]["loc"], 3)
|
|
|
|
|
|
# ============================================================
|
|
# TestAllowlistIdentityAndReasonRatchet [REVIEW_REVIEW_REFACTOR-2]
|
|
# ============================================================
|
|
|
|
|
|
def _baseline(file_thresholds=None, function_thresholds=None, task_totals=None):
|
|
return {
|
|
"file_thresholds": file_thresholds or [],
|
|
"function_thresholds": function_thresholds or [],
|
|
"task_read_set_totals": task_totals or [],
|
|
}
|
|
|
|
|
|
def _current(violations):
|
|
return {
|
|
"violations": violations,
|
|
"summary": {"total_files": 1, "total_lines_of_code": 1000,
|
|
"total_functions": 1, "violations_count": len(violations)},
|
|
}
|
|
|
|
|
|
class TestAllowlistIdentityAndReasonRatchet(_RepoFixture):
|
|
"""Stable (path, metric, function) identity: drops pass, new/increases fail."""
|
|
|
|
prefix = "ra-test-ratchet-"
|
|
|
|
def test_report_function_identities_are_unique_per_path(self) -> None:
|
|
"""Ratchet identity is (path, metric, function): duplicates would collide."""
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("src/overload.kt", textwrap.dedent("""\
|
|
class C {
|
|
fun run(value: Int) {
|
|
println(value)
|
|
}
|
|
|
|
fun run(value: String) {
|
|
println(value)
|
|
}
|
|
}
|
|
"""))
|
|
self._write("src/prop.py", textwrap.dedent("""\
|
|
class C:
|
|
@property
|
|
def value(self):
|
|
return self._v
|
|
|
|
@value.setter
|
|
def value(self, v):
|
|
self._v = v
|
|
"""))
|
|
result = ra.audit(["src/overload.kt", "src/prop.py"])
|
|
identities = [(entry["path"], fn["name"])
|
|
for entry in result["files"]
|
|
for fn in entry.get("functions", [])]
|
|
self.assertEqual(len(identities), 4)
|
|
self.assertEqual(len(identities), len(set(identities)))
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_same_value_with_identity_passes(self) -> None:
|
|
baseline = _baseline(
|
|
file_thresholds=[
|
|
{"path": "src/big.go", "metric": "file_loc", "level": "exception",
|
|
"value": 1000, "reason": "existing large file"},
|
|
],
|
|
function_thresholds=[
|
|
{"path": "src/big.go", "function": "MyType.DoSomething",
|
|
"metric": "function_loc", "level": "split_review",
|
|
"value": 120, "reason": "existing large function"},
|
|
],
|
|
)
|
|
current = _current([
|
|
{"path": "src/big.go", "metric": "file_loc", "level": "exception",
|
|
"value": 1000, "reason": "file exceeds threshold"},
|
|
{"path": "src/big.go", "metric": "function_loc", "level": "split_review",
|
|
"value": 120, "function": "MyType.DoSomething",
|
|
"reason": "function exceeds threshold"},
|
|
])
|
|
self.assertEqual(ra.ratchet_check(current, baseline, None), [])
|
|
|
|
def test_new_function_identity_fails(self) -> None:
|
|
current = _current([
|
|
{"path": "src/new.go", "metric": "function_loc", "level": "split_review",
|
|
"value": 150, "function": "NewFunc", "reason": "new oversized function"},
|
|
])
|
|
new = ra.ratchet_check(current, _baseline(), None)
|
|
self.assertEqual(len(new), 1)
|
|
self.assertEqual(new[0]["function"], "NewFunc")
|
|
self.assertIn("new violation", new[0]["reason"])
|
|
|
|
def test_level_drop_passes(self) -> None:
|
|
"""An improvement that lowers the level is not a new violation."""
|
|
baseline = _baseline(file_thresholds=[
|
|
{"path": "src/big.go", "metric": "file_loc", "level": "exception",
|
|
"value": 1200, "reason": "existing large file"},
|
|
])
|
|
current = _current([
|
|
{"path": "src/big.go", "metric": "file_loc", "level": "split_review",
|
|
"value": 900, "reason": "file exceeds threshold"},
|
|
])
|
|
self.assertEqual(ra.ratchet_check(current, baseline, None), [])
|
|
|
|
def test_level_increase_fails(self) -> None:
|
|
baseline = _baseline(file_thresholds=[
|
|
{"path": "src/big.go", "metric": "file_loc", "level": "split_review",
|
|
"value": 900, "reason": "existing large file"},
|
|
])
|
|
current = _current([
|
|
{"path": "src/big.go", "metric": "file_loc", "level": "exception",
|
|
"value": 1200, "reason": "file exceeds threshold"},
|
|
])
|
|
new = ra.ratchet_check(current, baseline, None)
|
|
self.assertEqual(len(new), 1)
|
|
self.assertIn("level increased", new[0]["reason"])
|
|
|
|
def test_value_increase_fails(self) -> None:
|
|
baseline = _baseline(file_thresholds=[
|
|
{"path": "src/big.go", "metric": "file_loc", "level": "exception",
|
|
"value": 1100, "reason": "smaller baseline"},
|
|
])
|
|
current = _current([
|
|
{"path": "src/big.go", "metric": "file_loc", "level": "exception",
|
|
"value": 1150, "reason": "got bigger"},
|
|
])
|
|
new = ra.ratchet_check(current, baseline, None)
|
|
self.assertEqual(len(new), 1)
|
|
self.assertIn("value increased", new[0]["reason"])
|
|
|
|
def test_empty_reason_in_baseline_fails(self) -> None:
|
|
baseline = _baseline(file_thresholds=[
|
|
{"path": "src/big.go", "metric": "file_loc", "level": "exception",
|
|
"value": 1000, "reason": ""},
|
|
])
|
|
new = ra.ratchet_check(_current([]), baseline, None)
|
|
self.assertEqual(len(new), 1)
|
|
self.assertEqual(new[0]["path"], "<baseline>")
|
|
self.assertIn("reason", new[0]["reason"])
|
|
|
|
def test_duplicate_function_in_baseline_fails(self) -> None:
|
|
baseline = _baseline(function_thresholds=[
|
|
{"path": "src/fn.go", "function": "DoSomething", "metric": "function_loc",
|
|
"level": "split_review", "value": 120, "reason": "first entry"},
|
|
{"path": "src/fn.go", "function": "DoSomething", "metric": "function_loc",
|
|
"level": "split_review", "value": 120, "reason": "duplicate entry"},
|
|
])
|
|
new = ra.ratchet_check(_current([]), baseline, None)
|
|
self.assertEqual(new[0]["path"], "<baseline>")
|
|
self.assertIn("duplicate identity", new[0]["reason"])
|
|
|
|
def test_same_identity_across_levels_is_duplicate(self) -> None:
|
|
"""One identity may hold exactly one allowlisted level."""
|
|
baseline = _baseline(file_thresholds=[
|
|
{"path": "src/fn.go", "metric": "file_loc", "level": "warning",
|
|
"value": 600, "reason": "entry a"},
|
|
{"path": "src/fn.go", "metric": "file_loc", "level": "exception",
|
|
"value": 1200, "reason": "entry b"},
|
|
])
|
|
new = ra.ratchet_check(_current([]), baseline, None)
|
|
self.assertEqual(new[0]["path"], "<baseline>")
|
|
self.assertIn("duplicate identity", new[0]["reason"])
|
|
|
|
def test_malformed_metric_fails(self) -> None:
|
|
baseline = _baseline(file_thresholds=[
|
|
{"path": "src/big.go", "metric": "function_loc", "level": "exception",
|
|
"value": 1000, "reason": "wrong metric"},
|
|
])
|
|
new = ra.ratchet_check(_current([]), baseline, None)
|
|
self.assertIn("invalid 'metric'", new[0]["reason"])
|
|
|
|
def test_malformed_level_fails(self) -> None:
|
|
baseline = _baseline(function_thresholds=[
|
|
{"path": "src/big.go", "function": "Foo", "metric": "function_loc",
|
|
"level": "exception", "value": 130, "reason": "functions have no exception level"},
|
|
])
|
|
new = ra.ratchet_check(_current([]), baseline, None)
|
|
self.assertIn("invalid 'level'", new[0]["reason"])
|
|
|
|
def test_malformed_value_fails(self) -> None:
|
|
baseline = _baseline(file_thresholds=[
|
|
{"path": "src/big.go", "metric": "file_loc", "level": "exception",
|
|
"value": "1000", "reason": "string value"},
|
|
])
|
|
new = ra.ratchet_check(_current([]), baseline, None)
|
|
self.assertIn("invalid 'value'", new[0]["reason"])
|
|
|
|
def test_unknown_key_fails(self) -> None:
|
|
baseline = _baseline(file_thresholds=[
|
|
{"path": "src/big.go", "metric": "file_loc", "level": "exception",
|
|
"value": 1000, "reason": "ok", "owner": "team"},
|
|
])
|
|
new = ra.ratchet_check(_current([]), baseline, None)
|
|
self.assertIn("unknown keys", new[0]["reason"])
|
|
|
|
def test_missing_function_key_fails(self) -> None:
|
|
baseline = _baseline(function_thresholds=[
|
|
{"path": "src/big.go", "metric": "function_loc", "level": "split_review",
|
|
"value": 130, "reason": "no function identity"},
|
|
])
|
|
new = ra.ratchet_check(_current([]), baseline, None)
|
|
self.assertIn("missing keys", new[0]["reason"])
|
|
|
|
|
|
# ============================================================
|
|
# TestTaskReadSetTotalsAndBudget [REVIEW_REVIEW_REFACTOR-3]
|
|
# ============================================================
|
|
|
|
|
|
class TestTaskReadSetTotalsAndBudget(_RepoFixture):
|
|
"""Read-set config failures, budget/reason, and baseline total ratchet."""
|
|
|
|
prefix = "ra-test-rs-"
|
|
|
|
def _config(self, files, max_total_loc=100, reason="test budget", task_id="test-task",
|
|
version="2.0", generated_at="2026-07-16",
|
|
description="fixture task"):
|
|
return {"version": version, "generated_at": generated_at, "tasks": [{
|
|
"task_id": task_id, "description": description,
|
|
"files": files,
|
|
"budget": {"max_total_loc": max_total_loc, "reason": reason},
|
|
}]}
|
|
|
|
def test_ordered_path_totals(self) -> None:
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("a.go", "package p\n\nfunc main() {}\n") # 3 lines
|
|
self._write("b.py", "def f(): pass\n") # 1 line
|
|
self._write("c.md", "line1\nline2\n") # 2 lines
|
|
|
|
errors, task_read_sets = ra.audit_read_sets(
|
|
None, self._config(["b.py", "a.go", "c.md"]))
|
|
self.assertEqual(errors, [])
|
|
self.assertEqual(len(task_read_sets), 1)
|
|
entry = task_read_sets[0]
|
|
self.assertEqual(entry["task_id"], "test-task")
|
|
self.assertEqual([f["path"] for f in entry["files"]], ["b.py", "a.go", "c.md"])
|
|
self.assertEqual(entry["total_loc"], 6)
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_missing_path_is_configuration_failure(self) -> None:
|
|
"""A read-set path that does not exist is a configuration failure, not 0 LOC."""
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("exists.go", "package p\n")
|
|
errors, task_read_sets = ra.audit_read_sets(
|
|
None, self._config(["exists.go", "nonexistent.go"]))
|
|
self.assertEqual(len(errors), 1)
|
|
self.assertEqual(errors[0]["metric"], "read_set_config")
|
|
self.assertIn("missing read_set path 'nonexistent.go'", errors[0]["reason"])
|
|
self.assertEqual([f["path"] for f in task_read_sets[0]["files"]], ["exists.go"])
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_duplicate_path_error(self) -> None:
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("a.go", "package p\n")
|
|
errors, _ = ra.audit_read_sets(None, self._config(["a.go", "a.go"]))
|
|
self.assertEqual(len(errors), 1)
|
|
self.assertIn("duplicate", errors[0]["reason"])
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_duplicate_task_id_error(self) -> None:
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("a.go", "package p\n")
|
|
config = {"version": "2.0", "generated_at": "2026-07-16", "tasks": [
|
|
{"task_id": "dup", "description": "fixture task", "files": ["a.go"],
|
|
"budget": {"max_total_loc": 10, "reason": "r"}},
|
|
{"task_id": "dup", "description": "fixture task", "files": ["a.go"],
|
|
"budget": {"max_total_loc": 10, "reason": "r"}},
|
|
]}
|
|
errors, task_read_sets = ra.audit_read_sets(None, config)
|
|
self.assertEqual(len(errors), 1)
|
|
self.assertIn("duplicate task_id", errors[0]["reason"])
|
|
self.assertEqual(len(task_read_sets), 1)
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_read_set_schema_requires_typed_metadata(self) -> None:
|
|
"""Untyped/absent root metadata and task description are config errors."""
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("a.go", "package p\n")
|
|
task = {"task_id": "t", "description": "fixture task", "files": ["a.go"],
|
|
"budget": {"max_total_loc": 100, "reason": "r"}}
|
|
valid = {"version": "2.0", "generated_at": "2026-07-16", "tasks": [task]}
|
|
|
|
# The contrast case: the exact schema is satisfied, so no issue.
|
|
errors, task_read_sets = ra.audit_read_sets(None, valid)
|
|
self.assertEqual(errors, [])
|
|
self.assertEqual(len(task_read_sets), 1)
|
|
|
|
cases = [
|
|
("missing version",
|
|
{k: v for k, v in valid.items() if k != "version"},
|
|
"missing required root key 'version'"),
|
|
("missing generated_at",
|
|
{k: v for k, v in valid.items() if k != "generated_at"},
|
|
"missing required root key 'generated_at'"),
|
|
("wrong-type version", {**valid, "version": 2.0},
|
|
"root 'version' must be a non-empty string"),
|
|
("empty generated_at", {**valid, "generated_at": " "},
|
|
"root 'generated_at' must be a non-empty string"),
|
|
("unsupported version", {**valid, "version": "1.0"},
|
|
"unsupported version '1.0'"),
|
|
("missing description",
|
|
{**valid, "tasks": [{k: v for k, v in task.items()
|
|
if k != "description"}]},
|
|
"missing required non-empty 'description'"),
|
|
("wrong-type description",
|
|
{**valid, "tasks": [{**task, "description": ["fixture"]}]},
|
|
"missing required non-empty 'description'"),
|
|
("empty description",
|
|
{**valid, "tasks": [{**task, "description": ""}]},
|
|
"missing required non-empty 'description'"),
|
|
]
|
|
for label, config, expected in cases:
|
|
with self.subTest(case=label):
|
|
errors, _ = ra.audit_read_sets(None, config)
|
|
self.assertEqual(len(errors), 1, f"{label}: {errors}")
|
|
self.assertEqual(errors[0]["metric"], "read_set_config")
|
|
self.assertIn(expected, errors[0]["reason"])
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_invalid_budget_schema_error(self) -> None:
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("a.go", "package p\n")
|
|
config = {"version": "2.0", "generated_at": "2026-07-16", "tasks": [
|
|
{"task_id": "t", "description": "fixture task", "files": ["a.go"],
|
|
"budget": {"max_total_loc": "100", "reason": "r"}}]}
|
|
errors, _ = ra.audit_read_sets(None, config)
|
|
self.assertEqual(len(errors), 1)
|
|
self.assertIn("invalid 'max_total_loc'", errors[0]["reason"])
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_unknown_task_key_error(self) -> None:
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("a.go", "package p\n")
|
|
config = {"version": "2.0", "generated_at": "2026-07-16", "tasks": [
|
|
{"task_id": "t", "description": "fixture task", "files": ["a.go"], "total_loc": 1,
|
|
"budget": {"max_total_loc": 100, "reason": "r"}}]}
|
|
errors, _ = ra.audit_read_sets(None, config)
|
|
self.assertEqual(len(errors), 1)
|
|
self.assertIn("unknown keys ['total_loc']", errors[0]["reason"])
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_exact_budget_passes_and_one_over_without_reason_fails(self) -> None:
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("ten.py", "\n".join(f"# {i}" for i in range(10)) + "\n")
|
|
errors, task_read_sets = ra.audit_read_sets(
|
|
None, self._config(["ten.py"], max_total_loc=10, reason=""))
|
|
self.assertEqual(errors, [])
|
|
self.assertEqual(task_read_sets[0]["total_loc"], 10)
|
|
|
|
errors, _ = ra.audit_read_sets(
|
|
None, self._config(["ten.py"], max_total_loc=9, reason=""))
|
|
self.assertEqual(len(errors), 1)
|
|
self.assertEqual(errors[0]["metric"], "read_set_budget")
|
|
self.assertEqual(errors[0]["value"], 10)
|
|
self.assertIn("empty reason", errors[0]["reason"])
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_over_budget_with_reason_needs_baseline_total(self) -> None:
|
|
"""Over budget with a reason still has to be allowlisted in the baseline."""
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("big.py", "\n".join(f"# {i}" for i in range(200)) + "\n")
|
|
config = self._config(["big.py"], max_total_loc=100, reason="known large file")
|
|
errors, task_read_sets = ra.audit_read_sets(None, config)
|
|
self.assertEqual(errors, [])
|
|
self.assertEqual(task_read_sets[0]["total_loc"], 200)
|
|
|
|
issues = ra.ratchet_read_set_totals(task_read_sets, _baseline())
|
|
self.assertEqual(len(issues), 1)
|
|
self.assertEqual(issues[0]["metric"], "read_set_total")
|
|
self.assertEqual(issues[0]["value"], 200)
|
|
self.assertIn("not allowlisted", issues[0]["reason"])
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_task_total_same_or_lower_than_baseline_passes(self) -> None:
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("big.py", "\n".join(f"# {i}" for i in range(200)) + "\n")
|
|
config = self._config(["big.py"], max_total_loc=100, reason="known large file")
|
|
_, task_read_sets = ra.audit_read_sets(None, config)
|
|
|
|
same = _baseline(task_totals=[{"task_id": "test-task", "metric": "read_set_total",
|
|
"value": 200, "reason": "allowlisted total"}])
|
|
self.assertEqual(ra.ratchet_read_set_totals(task_read_sets, same), [])
|
|
|
|
higher = _baseline(task_totals=[{"task_id": "test-task", "metric": "read_set_total",
|
|
"value": 300, "reason": "allowlisted total"}])
|
|
self.assertEqual(ra.ratchet_read_set_totals(task_read_sets, higher), [])
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_task_total_increase_fails(self) -> None:
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("big.py", "\n".join(f"# {i}" for i in range(200)) + "\n")
|
|
config = self._config(["big.py"], max_total_loc=100, reason="known large file")
|
|
_, task_read_sets = ra.audit_read_sets(None, config)
|
|
|
|
baseline = _baseline(task_totals=[{"task_id": "test-task", "metric": "read_set_total",
|
|
"value": 150, "reason": "allowlisted total"}])
|
|
issues = ra.ratchet_read_set_totals(task_read_sets, baseline)
|
|
self.assertEqual(len(issues), 1)
|
|
self.assertIn("task total increased from 150 to 200", issues[0]["reason"])
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_task_total_baseline_schema_validated(self) -> None:
|
|
baseline = _baseline(task_totals=[{"task_id": "t", "metric": "read_set_total",
|
|
"value": 10, "reason": ""}])
|
|
new = ra.ratchet_check(_current([]), baseline, None)
|
|
self.assertEqual(new[0]["path"], "<baseline>")
|
|
self.assertIn("task_read_set_totals[0]", new[0]["reason"])
|
|
|
|
def test_ratchet_check_covers_read_set_totals(self) -> None:
|
|
"""When read-sets are supplied, one ratchet_check call covers task totals."""
|
|
current = _current([])
|
|
current["task_read_sets"] = [{
|
|
"task_id": "test-task",
|
|
"files": [{"path": "big.py", "loc": 200}],
|
|
"total_loc": 200,
|
|
"budget": {"max_total_loc": 100, "reason": "known large file"},
|
|
"reason": "known large file",
|
|
}]
|
|
self.assertEqual(ra.ratchet_check(current, _baseline(), None), [])
|
|
new = ra.ratchet_check(current, _baseline(), {"tasks": []})
|
|
self.assertEqual(len(new), 1)
|
|
self.assertEqual(new[0]["path"], "<read-set:test-task>")
|
|
|
|
def test_required_config_load_reports_each_failure(self) -> None:
|
|
"""load_required_json_file separates missing, malformed, and valid configs."""
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
config, error = ra.load_required_json_file("absent.json")
|
|
self.assertIsNone(config)
|
|
self.assertIn("required config file not found", error)
|
|
|
|
self._write("broken.json", '{"tasks": [ truncated\n')
|
|
config, error = ra.load_required_json_file("broken.json")
|
|
self.assertIsNone(config)
|
|
self.assertIn("malformed JSON", error)
|
|
|
|
self._write("list.json", "[]\n")
|
|
config, error = ra.load_required_json_file("list.json")
|
|
self.assertIsNone(config)
|
|
self.assertIn("not a JSON object", error)
|
|
|
|
self._write("valid.json", '{"tasks": []}\n')
|
|
config, error = ra.load_required_json_file("valid.json")
|
|
self.assertEqual(config, {"tasks": []})
|
|
self.assertIsNone(error)
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_absent_config_is_a_configuration_failure(self) -> None:
|
|
"""A read-set config of None must fail rather than report zero issues."""
|
|
errors, task_read_sets = ra.audit_read_sets(None, None)
|
|
self.assertEqual(len(errors), 1)
|
|
self.assertEqual(errors[0]["metric"], "read_set_config")
|
|
self.assertEqual(task_read_sets, [])
|
|
|
|
def test_unknown_root_key_error(self) -> None:
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("a.go", "package p\n")
|
|
config = dict(self._config(["a.go"]), task_totals=[])
|
|
errors, task_read_sets = ra.audit_read_sets(None, config)
|
|
self.assertEqual(len(errors), 1)
|
|
self.assertIn("unknown root keys ['task_totals']", errors[0]["reason"])
|
|
self.assertEqual(task_read_sets, [])
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_documented_root_keys_are_accepted(self) -> None:
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("a.go", "package p\n")
|
|
config = dict(self._config(["a.go"]), version="2.0", generated_at="2026-07-16")
|
|
errors, task_read_sets = ra.audit_read_sets(None, config)
|
|
self.assertEqual(errors, [])
|
|
self.assertEqual(len(task_read_sets), 1)
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_read_set_determinism(self) -> None:
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("a.go", "package p\nfunc main() {}\n")
|
|
self._write("b.py", "def f(): pass\n")
|
|
config = self._config(["b.py", "a.go"])
|
|
rendered = set()
|
|
for _ in range(3):
|
|
_, task_read_sets = ra.audit_read_sets(None, config)
|
|
rendered.add(json.dumps(task_read_sets, sort_keys=True))
|
|
self.assertEqual(len(rendered), 1)
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
|
|
# ============================================================
|
|
# TestCliExitCodes [REVIEW_REVIEW_REFACTOR-3, REVIEW_REVIEW_REFACTOR-5]
|
|
# ============================================================
|
|
|
|
|
|
class TestCliExitCodes(_RepoFixture):
|
|
"""The CLI renders every failure and exits with a defined status."""
|
|
|
|
prefix = "ra-test-cli-"
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self._write("a.go", "package p\n\nfunc main() {}\n")
|
|
self._write_json("scripts/readability_baseline.json", _baseline())
|
|
self._write_json("scripts/readability_read_sets.json", {
|
|
"version": "2.0", "generated_at": "2026-07-16", "tasks": [{
|
|
"task_id": "t", "description": "fixture task",
|
|
"files": ["a.go"],
|
|
"budget": {"max_total_loc": 100, "reason": "fixture budget"},
|
|
}]})
|
|
self._git_init()
|
|
|
|
def _write_json(self, relpath: str, payload) -> None:
|
|
self._write(relpath, json.dumps(payload, indent=2) + "\n")
|
|
|
|
def _run_cli(self, *args: str) -> subprocess.CompletedProcess:
|
|
return subprocess.run([sys.executable, AUDIT_SCRIPT, *args],
|
|
cwd=self.tmpdir, capture_output=True, text=True)
|
|
|
|
def test_clean_check_exits_zero(self) -> None:
|
|
proc = self._run_cli("--check", "--input-mode", "worktree")
|
|
self.assertEqual(proc.returncode, 0, proc.stderr)
|
|
self.assertIn("RATCHET OK", proc.stderr)
|
|
self.assertIn("readability-audit:", proc.stdout)
|
|
|
|
def test_missing_read_set_path_exits_two(self) -> None:
|
|
self._write_json("scripts/readability_read_sets.json", {
|
|
"version": "2.0", "generated_at": "2026-07-16", "tasks": [{
|
|
"task_id": "t", "description": "fixture task",
|
|
"files": ["a.go", "gone.go"],
|
|
"budget": {"max_total_loc": 100, "reason": "fixture budget"},
|
|
}]})
|
|
proc = self._run_cli("--check", "--input-mode", "worktree")
|
|
self.assertEqual(proc.returncode, 2, proc.stderr)
|
|
self.assertIn("READ-SET CONFIGURATION FAIL", proc.stderr)
|
|
self.assertIn("missing read_set path 'gone.go'", proc.stderr)
|
|
|
|
def test_reasonless_over_budget_exits_four(self) -> None:
|
|
self._write_json("scripts/readability_read_sets.json", {
|
|
"version": "2.0", "generated_at": "2026-07-16", "tasks": [{
|
|
"task_id": "t", "description": "fixture task",
|
|
"files": ["a.go"],
|
|
"budget": {"max_total_loc": 1, "reason": ""},
|
|
}]})
|
|
proc = self._run_cli("--check", "--input-mode", "worktree")
|
|
self.assertEqual(proc.returncode, 4, proc.stderr)
|
|
self.assertIn("RATCHET FAIL", proc.stderr)
|
|
self.assertIn("read_set_budget=3", proc.stderr)
|
|
|
|
def test_unallowlisted_over_budget_total_exits_four(self) -> None:
|
|
self._write_json("scripts/readability_read_sets.json", {
|
|
"version": "2.0", "generated_at": "2026-07-16", "tasks": [{
|
|
"task_id": "t", "description": "fixture task",
|
|
"files": ["a.go"],
|
|
"budget": {"max_total_loc": 1, "reason": "documented"},
|
|
}]})
|
|
proc = self._run_cli("--check", "--input-mode", "worktree")
|
|
self.assertEqual(proc.returncode, 4, proc.stderr)
|
|
self.assertIn("<read-set:t>: read_set_total=3", proc.stderr)
|
|
|
|
def test_missing_read_set_config_exits_two(self) -> None:
|
|
"""Deleting the required config must not silently skip the total ratchet."""
|
|
os.remove(os.path.join(self.tmpdir, "scripts/readability_read_sets.json"))
|
|
proc = self._run_cli("--check", "--input-mode", "worktree")
|
|
self.assertEqual(proc.returncode, 2, proc.stderr)
|
|
self.assertIn("READ-SET CONFIGURATION FAIL", proc.stderr)
|
|
self.assertIn("required config file not found", proc.stderr)
|
|
self.assertNotIn("RATCHET OK", proc.stderr)
|
|
|
|
def test_malformed_read_set_config_exits_two(self) -> None:
|
|
self._write("scripts/readability_read_sets.json", '{"tasks": [ truncated\n')
|
|
proc = self._run_cli("--check", "--input-mode", "worktree")
|
|
self.assertEqual(proc.returncode, 2, proc.stderr)
|
|
self.assertIn("READ-SET CONFIGURATION FAIL", proc.stderr)
|
|
self.assertIn("malformed JSON", proc.stderr)
|
|
self.assertNotIn("RATCHET OK", proc.stderr)
|
|
|
|
def test_non_object_read_set_config_root_exits_two(self) -> None:
|
|
self._write_json("scripts/readability_read_sets.json", [{"task_id": "t"}])
|
|
proc = self._run_cli("--check", "--input-mode", "worktree")
|
|
self.assertEqual(proc.returncode, 2, proc.stderr)
|
|
self.assertIn("is not a JSON object", proc.stderr)
|
|
|
|
def test_unknown_read_set_root_key_exits_two(self) -> None:
|
|
self._write_json("scripts/readability_read_sets.json", {
|
|
"tasks": [{"task_id": "t", "files": ["a.go"],
|
|
"budget": {"max_total_loc": 100, "reason": "fixture budget"}}],
|
|
"task_totals": [],
|
|
})
|
|
proc = self._run_cli("--check", "--input-mode", "worktree")
|
|
self.assertEqual(proc.returncode, 2, proc.stderr)
|
|
self.assertIn("unknown root keys ['task_totals']", proc.stderr)
|
|
|
|
def test_valid_read_set_config_reaches_total_ratchet(self) -> None:
|
|
"""The contrast case: a valid config is always ratcheted, and can fail."""
|
|
self._write_json("scripts/readability_read_sets.json", {
|
|
"version": "2.0",
|
|
"generated_at": "2026-07-16",
|
|
"tasks": [{"task_id": "t", "description": "fixture task", "files": ["a.go"],
|
|
"budget": {"max_total_loc": 1, "reason": "documented"}}],
|
|
})
|
|
proc = self._run_cli("--check", "--input-mode", "worktree")
|
|
self.assertEqual(proc.returncode, 4, proc.stderr)
|
|
self.assertIn("<read-set:t>: read_set_total=3", proc.stderr)
|
|
|
|
def test_read_set_schema_error_exits_two(self) -> None:
|
|
"""Every schema error is a configuration failure, not a silent pass."""
|
|
valid = {
|
|
"version": "2.0", "generated_at": "2026-07-16",
|
|
"tasks": [{"task_id": "t", "description": "fixture task",
|
|
"files": ["a.go"],
|
|
"budget": {"max_total_loc": 100, "reason": "fixture budget"}}],
|
|
}
|
|
cases = [
|
|
("missing version", {k: v for k, v in valid.items() if k != "version"},
|
|
"missing required root key 'version'"),
|
|
("wrong-type generated_at", {**valid, "generated_at": 20260716},
|
|
"root 'generated_at' must be a non-empty string"),
|
|
("unsupported version", {**valid, "version": "3.0"},
|
|
"unsupported version '3.0'"),
|
|
("missing description", {**valid, "tasks": [
|
|
{"task_id": "t", "files": ["a.go"],
|
|
"budget": {"max_total_loc": 100, "reason": "fixture budget"}}]},
|
|
"missing required non-empty 'description'"),
|
|
("wrong-type description", {**valid, "tasks": [
|
|
{"task_id": "t", "description": 7, "files": ["a.go"],
|
|
"budget": {"max_total_loc": 100, "reason": "fixture budget"}}]},
|
|
"missing required non-empty 'description'"),
|
|
]
|
|
for label, config, expected in cases:
|
|
with self.subTest(case=label):
|
|
self._write_json("scripts/readability_read_sets.json", config)
|
|
proc = self._run_cli("--check", "--input-mode", "worktree")
|
|
self.assertEqual(proc.returncode, 2, proc.stderr)
|
|
self.assertIn("READ-SET CONFIGURATION FAIL:", proc.stderr)
|
|
self.assertIn(expected, proc.stderr)
|
|
|
|
def test_canonical_read_set_config_satisfies_exact_schema(self) -> None:
|
|
"""The tracked read-set config is itself a valid exact-schema config."""
|
|
with open(os.path.join(SCRIPTS_DIR, "readability_read_sets.json"),
|
|
encoding="utf-8") as handle:
|
|
config = json.load(handle)
|
|
self._write_json("scripts/readability_read_sets.json", {
|
|
**config,
|
|
"tasks": [{**task, "files": ["a.go"]} for task in config["tasks"]],
|
|
})
|
|
proc = self._run_cli("--input-mode", "worktree")
|
|
self.assertEqual(proc.returncode, 0, proc.stderr)
|
|
self.assertNotIn("READ-SET CONFIGURATION FAIL:", proc.stderr)
|
|
|
|
def test_missing_baseline_exits_three(self) -> None:
|
|
os.remove(os.path.join(self.tmpdir, "scripts/readability_baseline.json"))
|
|
proc = self._run_cli("--check", "--input-mode", "worktree")
|
|
self.assertEqual(proc.returncode, 3, proc.stderr)
|
|
self.assertIn("baseline not found", proc.stderr)
|
|
|
|
def test_report_is_written_even_when_ratchet_fails(self) -> None:
|
|
"""The projection is evidence, so it is written before the ratchet verdict."""
|
|
self._write_json("scripts/readability_read_sets.json", {
|
|
"version": "2.0", "generated_at": "2026-07-16", "tasks": [{
|
|
"task_id": "t", "description": "fixture task",
|
|
"files": ["a.go"],
|
|
"budget": {"max_total_loc": 1, "reason": ""},
|
|
}]})
|
|
out = os.path.join(self.tmpdir, "out", "report.json")
|
|
proc = self._run_cli("--check", "--input-mode", "worktree", "--output", out)
|
|
self.assertEqual(proc.returncode, 4, proc.stderr)
|
|
with open(out, encoding="utf-8") as handle:
|
|
report = json.load(handle)
|
|
self.assertEqual([e["path"] for e in report["files"]], ["a.go"])
|
|
self.assertEqual(report["input_mode"], "worktree")
|
|
|
|
|
|
# ============================================================
|
|
# TestInputModeTrackedVsWorktree [REVIEW_REVIEW_REFACTOR-1]
|
|
# ============================================================
|
|
|
|
|
|
class TestInputModeTrackedVsWorktree(_RepoFixture):
|
|
"""worktree is the post-commit projection: modified files stay, deletions go."""
|
|
|
|
prefix = "ra-test-mode-"
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self._write("tracked.go", "package p\nfunc main() {}\n")
|
|
self._write("other.go", "package p\n")
|
|
self._git_init()
|
|
|
|
def test_tracked_mode_only_index(self) -> None:
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("untracked.go", "package q\n")
|
|
files = ra.collect_files("tracked")
|
|
self.assertIn("tracked.go", files)
|
|
self.assertNotIn("untracked.go", files)
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_worktree_mode_includes_untracked(self) -> None:
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("untracked.go", "package q\n")
|
|
files = ra.collect_files("worktree")
|
|
self.assertIn("tracked.go", files)
|
|
self.assertIn("untracked.go", files)
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_worktree_mode_includes_modified_tracked(self) -> None:
|
|
"""A modified tracked file is still part of the projection."""
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("tracked.go", "package p\nfunc main() {}\nfunc extra() {}\n")
|
|
self._write("other.go", "package p\nfunc staged() {}\n")
|
|
self._git("add", "other.go")
|
|
|
|
files = ra.collect_files("worktree")
|
|
self.assertIn("tracked.go", files)
|
|
self.assertIn("other.go", files)
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_worktree_mode_excludes_unstaged_deletion(self) -> None:
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
os.remove(os.path.join(self.tmpdir, "other.go"))
|
|
files = ra.collect_files("worktree")
|
|
self.assertIn("tracked.go", files)
|
|
self.assertNotIn("other.go", files)
|
|
# tracked mode still reports the index entry
|
|
self.assertIn("other.go", ra.collect_files("tracked"))
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_worktree_mode_excludes_staged_deletion(self) -> None:
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._git("rm", "-q", "other.go")
|
|
files = ra.collect_files("worktree")
|
|
self.assertIn("tracked.go", files)
|
|
self.assertNotIn("other.go", files)
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_worktree_mode_excludes_deleted(self) -> None:
|
|
"""git rm --cached leaves the file on disk but stages its removal."""
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("todelete.go", "package r\n")
|
|
self._git("add", "todelete.go")
|
|
self._git("commit", "-q", "-m", "add")
|
|
self._git("rm", "--cached", "-q", "todelete.go")
|
|
|
|
files = ra.collect_files("worktree")
|
|
self.assertIn("tracked.go", files)
|
|
self.assertNotIn("todelete.go", files)
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_worktree_mode_excludes_excluded_patterns(self) -> None:
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("proto/gen/generated.go", "package gen\n")
|
|
self.assertNotIn("proto/gen/generated.go", ra.collect_files("worktree"))
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_tracked_and_worktree_produce_same_for_clean_repo(self) -> None:
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self.assertEqual(ra.collect_files("tracked"), ra.collect_files("worktree"))
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_worktree_matches_tracked_after_commit(self) -> None:
|
|
"""The worktree projection equals what tracked reports once committed."""
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
self._write("tracked.go", "package p\nfunc main() {}\nfunc extra() {}\n")
|
|
self._write("added.go", "package q\n")
|
|
os.remove(os.path.join(self.tmpdir, "other.go"))
|
|
projected = ra.collect_files("worktree")
|
|
|
|
self._git("add", "-A")
|
|
self._git("commit", "-q", "-m", "apply worktree")
|
|
self.assertEqual(ra.collect_files("tracked"), projected)
|
|
self.assertEqual(ra.collect_files("worktree"), projected)
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
|
|
# ============================================================
|
|
# TestThresholdBoundaries
|
|
# ============================================================
|
|
|
|
|
|
class TestThresholdBoundaries(_RepoFixture):
|
|
"""Threshold enforcement at boundary values (production defaults)."""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
lines_1000 = "\n".join(f"line {i}" for i in range(1000)) + "\n"
|
|
self._write("src/at_boundary.go", lines_1000)
|
|
lines_1001 = "\n".join(f"line {i}" for i in range(1001)) + "\n"
|
|
self._write("src/over_boundary.go", lines_1001)
|
|
body_118 = "\n".join(f" line{i}" for i in range(118))
|
|
self._write("src/fn_at.go", f"func fn_at() {{\n{body_118}\n}}\n")
|
|
body_119 = "\n".join(f" line{i}" for i in range(119))
|
|
self._write("src/fn_over.go", f"func fn_over() {{\n{body_119}\n}}\n")
|
|
|
|
def test_exact_boundary_passes(self) -> None:
|
|
"""1000 LOC production → split_review only; 120 LOC function → warning only."""
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
result = ra.audit(["src/at_boundary.go", "src/fn_at.go"])
|
|
violations = result["violations"]
|
|
self.assertEqual(len(violations), 2)
|
|
vmap = {(v["path"], v["metric"]): v for v in violations}
|
|
self.assertEqual(vmap[("src/at_boundary.go", "file_loc")]["level"], "split_review")
|
|
self.assertEqual(vmap[("src/fn_at.go", "function_loc")]["level"], "warning")
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
def test_one_over_violates(self) -> None:
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
result = ra.audit(["src/over_boundary.go", "src/fn_over.go"])
|
|
file_violations = [v for v in result["violations"] if v["metric"] == "file_loc"]
|
|
func_violations = [v for v in result["violations"] if v["metric"] == "function_loc"]
|
|
self.assertEqual(len(file_violations), 1)
|
|
self.assertEqual(file_violations[0]["path"], "src/over_boundary.go")
|
|
self.assertEqual(file_violations[0]["level"], "exception")
|
|
self.assertEqual(len(func_violations), 1)
|
|
self.assertEqual(func_violations[0]["path"], "src/fn_over.go")
|
|
self.assertEqual(func_violations[0]["level"], "split_review")
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
|
|
# ============================================================
|
|
# TestNewOrIncreasedViolationFails
|
|
# ============================================================
|
|
|
|
|
|
class TestNewOrIncreasedViolationFails(unittest.TestCase):
|
|
"""Ratchet: new or increased violations should FAIL."""
|
|
|
|
def test_new_violation_fails(self) -> None:
|
|
current = _current([
|
|
{"path": "src/new_big.go", "metric": "file_loc", "level": "exception",
|
|
"value": 1500, "reason": "new violation"},
|
|
])
|
|
new = ra.ratchet_check(current, _baseline(), None)
|
|
self.assertEqual(len(new), 1)
|
|
self.assertEqual(new[0]["path"], "src/new_big.go")
|
|
self.assertIn("new", new[0]["reason"])
|
|
|
|
def test_increased_violation_fails(self) -> None:
|
|
baseline = _baseline(file_thresholds=[
|
|
{"path": "src/big.go", "metric": "file_loc", "level": "exception",
|
|
"value": 1100, "reason": "old"},
|
|
])
|
|
current = _current([
|
|
{"path": "src/big.go", "metric": "file_loc", "level": "exception",
|
|
"value": 1200, "reason": "now bigger"},
|
|
])
|
|
new = ra.ratchet_check(current, baseline, None)
|
|
self.assertEqual(len(new), 1)
|
|
self.assertEqual(new[0]["path"], "src/big.go")
|
|
self.assertIn("increased", new[0]["reason"])
|
|
|
|
|
|
# ============================================================
|
|
# TestReadSetDeterministic
|
|
# ============================================================
|
|
|
|
|
|
class TestReadSetDeterministic(_RepoFixture):
|
|
"""Read-set (import) extraction is deterministic across runs."""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self._write("pkg/a.go", textwrap.dedent("""\
|
|
package pkg
|
|
|
|
import "fmt"
|
|
import "io"
|
|
|
|
func A() {
|
|
fmt.Println("hello")
|
|
}
|
|
"""))
|
|
self._write("pkg/b.py", textwrap.dedent("""\
|
|
import os
|
|
import sys
|
|
|
|
def b_func():
|
|
pass
|
|
"""))
|
|
self._write("pkg/c.dart", textwrap.dedent("""\
|
|
import 'package:foo/bar.dart';
|
|
import 'utils.dart';
|
|
|
|
void c_func() {}
|
|
"""))
|
|
|
|
def test_read_set_totals_are_deterministic(self) -> None:
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
files = ["pkg/a.go", "pkg/b.py", "pkg/c.dart"]
|
|
results = [ra.audit(files) for _ in range(3)]
|
|
self.assertEqual(len({json.dumps(r, sort_keys=True) for r in results}), 1)
|
|
entries = {e["path"]: e for e in results[0]["files"]}
|
|
self.assertEqual(entries["pkg/a.go"]["read_set"], ["fmt", "io"])
|
|
self.assertEqual(entries["pkg/b.py"]["read_set"], ["os", "sys"])
|
|
self.assertEqual(len(entries["pkg/c.dart"]["read_set"]), 2)
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
|
|
# ============================================================
|
|
# TestFunctionExtraction
|
|
# ============================================================
|
|
|
|
|
|
class TestFunctionExtraction(unittest.TestCase):
|
|
"""Verify function extraction across languages."""
|
|
|
|
def test_python_function_extraction(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
class Foo:
|
|
def bar(self):
|
|
pass
|
|
|
|
def baz():
|
|
x = 1
|
|
y = 2
|
|
return x + y
|
|
""")
|
|
funcs = ra._extract_functions_python(source, "test.py")
|
|
names = {f["name"] for f in funcs}
|
|
self.assertNotIn("Foo", names)
|
|
self.assertEqual(names, {"Foo.bar", "baz"})
|
|
baz = next(f for f in funcs if f["name"] == "baz")
|
|
self.assertEqual(baz["loc"], 4)
|
|
|
|
def test_go_function_extraction(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
package main
|
|
|
|
func main() {
|
|
fmt.Println("hello")
|
|
}
|
|
|
|
func helper() {
|
|
x := 1
|
|
y := 2
|
|
z := 3
|
|
return x + y + z
|
|
}
|
|
""")
|
|
names = {f["name"] for f in ra._extract_functions_go(source, "main.go")}
|
|
self.assertEqual(names, {"main", "helper"})
|
|
|
|
def test_dart_function_extraction(self) -> None:
|
|
source = textwrap.dedent("""\
|
|
void main() {
|
|
print('hello');
|
|
}
|
|
|
|
int compute(int a, int b) {
|
|
return a + b;
|
|
}
|
|
""")
|
|
names = {f["name"] for f in ra._extract_functions_dart(source, "test.dart")}
|
|
self.assertEqual(names, {"main", "compute"})
|
|
|
|
|
|
# ============================================================
|
|
# TestJSONDeterminism
|
|
# ============================================================
|
|
|
|
|
|
class TestJSONDeterminism(_RepoFixture):
|
|
"""Full audit output is deterministic and byte-identical across runs."""
|
|
|
|
prefix = "ra-test-json-"
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self._write("a.go", "package p\n\nfunc main() {}\n")
|
|
self._write("b.py", "def f(): pass\n")
|
|
|
|
def test_audit_output_deterministic(self) -> None:
|
|
old_cwd = os.getcwd()
|
|
os.chdir(self.tmpdir)
|
|
try:
|
|
r1 = ra.audit(["a.go", "b.py"])
|
|
r2 = ra.audit(["a.go", "b.py"])
|
|
self.assertEqual(json.dumps(r1, sort_keys=True), json.dumps(r2, sort_keys=True))
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|