chore: add per-module code review process and tracking infra
Adapts the code-review procedure, folder layout, template, and tooling from the sibling mxaccessgw repo to lmxopcua. - REVIEW-PROCESS.md: per-module review workflow — a module is one src/ or tests/ project (ZB.MOM.WW.OtOpcUa. prefix stripped); 10-category checklist; finding IDs/severities/statuses; re-review rules. - code-reviews/_template/findings.md: per-module findings template. - code-reviews/regen-readme.py: generates the cross-module README.md index from the per-module findings.md files; --check gates staleness and consistency. - code-reviews/test_regen_readme.py: dependency-free generator tests. - code-reviews/prompt.md: orchestration prompt for clearing the backlog. - code-reviews/README.md: generated index (no modules reviewed yet). - scripts/check-code-reviews-readme.ps1: CI / pre-commit check wrapper. Adapted to this repo: ZB.MOM.WW.OtOpcUa module naming, OtOpcUa conventions checklist (in-process GalaxyDriver + mxaccessgw, contained-name vs tag-name, ACL at DriverNodeManager), single .NET solution build/test commands, and the lmxopcua design docs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
165
code-reviews/test_regen_readme.py
Normal file
165
code-reviews/test_regen_readme.py
Normal file
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for regen-readme.py.
|
||||
|
||||
Dependency-free: run with `python code-reviews/test_regen_readme.py`.
|
||||
Exits 0 if all tests pass, 1 otherwise.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import tempfile
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
|
||||
# regen-readme.py is not an importable module name (hyphen), so load it by path.
|
||||
_spec = importlib.util.spec_from_file_location("regen_readme", HERE / "regen-readme.py")
|
||||
regen = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(regen)
|
||||
|
||||
FIXTURE = """# Code Review — Demo
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Module | `src/Demo` |
|
||||
| Reviewer | Tester |
|
||||
| Review date | 2026-05-18 |
|
||||
| Commit reviewed | `abc1234` |
|
||||
| Status | Reviewed |
|
||||
| Open findings | 1 |
|
||||
|
||||
## Findings
|
||||
|
||||
### Demo-001
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Severity | High |
|
||||
| Category | Security |
|
||||
| Location | `src/Demo/File.cs:10` |
|
||||
| Status | Open |
|
||||
|
||||
**Description:** A first problem that matters.
|
||||
|
||||
**Recommendation:** Fix it.
|
||||
|
||||
**Resolution:** _(open)_
|
||||
|
||||
### Demo-002
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Severity | Low |
|
||||
| Category | Documentation & comments |
|
||||
| Location | `src/Demo/File.cs:20` |
|
||||
| Status | Resolved |
|
||||
|
||||
**Description:** A second, minor problem.
|
||||
|
||||
**Recommendation:** Tidy it.
|
||||
|
||||
**Resolution:** Fixed in def5678 on 2026-05-18.
|
||||
"""
|
||||
|
||||
|
||||
def _parse_fixture() -> dict:
|
||||
"""Write FIXTURE to a temp Demo/findings.md and parse it."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "Demo" / "findings.md"
|
||||
path.parent.mkdir()
|
||||
path.write_text(FIXTURE, encoding="utf-8")
|
||||
return regen.parse_module(path)
|
||||
|
||||
|
||||
def test_first_table_skips_separator_and_field_header():
|
||||
table = regen.first_table("| Field | Value |\n|---|---|\n| Severity | High |\n")
|
||||
assert table == {"Severity": "High"}, table
|
||||
|
||||
|
||||
def test_parse_module_header():
|
||||
m = _parse_fixture()
|
||||
assert m["module"] == "Demo", m["module"]
|
||||
assert m["header"]["Reviewer"] == "Tester"
|
||||
assert m["header"]["Status"] == "Reviewed"
|
||||
assert m["header"]["Open findings"] == "1"
|
||||
|
||||
|
||||
def test_parse_module_findings():
|
||||
m = _parse_fixture()
|
||||
assert len(m["findings"]) == 2, len(m["findings"])
|
||||
first = m["findings"][0]
|
||||
assert first["id"] == "Demo-001"
|
||||
assert first["severity"] == "High"
|
||||
assert first["category"] == "Security"
|
||||
assert first["location"] == "`src/Demo/File.cs:10`"
|
||||
assert first["status"] == "Open"
|
||||
assert first["description"] == "A first problem that matters."
|
||||
assert m["findings"][1]["status"] == "Resolved"
|
||||
|
||||
|
||||
def test_build_readme_splits_pending_and_closed():
|
||||
readme = regen.build_readme([_parse_fixture()])
|
||||
assert "## Pending findings" in readme
|
||||
assert "## Closed findings" in readme
|
||||
pending, closed = readme.split("## Closed findings", 1)
|
||||
assert "Demo-001" in pending # Open -> pending
|
||||
assert "Demo-001" not in closed
|
||||
assert "Demo-002" in closed # Resolved -> closed
|
||||
assert "_No pending findings._" not in pending
|
||||
|
||||
|
||||
def test_build_readme_handles_no_modules():
|
||||
readme = regen.build_readme([])
|
||||
assert "no modules reviewed yet" in readme
|
||||
assert "_No pending findings._" in readme
|
||||
assert "_No closed findings._" in readme
|
||||
|
||||
|
||||
def test_find_inconsistencies_clean_fixture():
|
||||
assert regen.find_inconsistencies([_parse_fixture()]) == []
|
||||
|
||||
|
||||
def test_find_inconsistencies_detects_wrong_open_count():
|
||||
m = _parse_fixture()
|
||||
m["header"]["Open findings"] = "7"
|
||||
issues = regen.find_inconsistencies([m])
|
||||
assert len(issues) == 1 and "Open findings" in issues[0], issues
|
||||
|
||||
|
||||
def test_find_inconsistencies_detects_unknown_status():
|
||||
m = _parse_fixture()
|
||||
m["findings"][0]["status"] = "Bogus"
|
||||
issues = regen.find_inconsistencies([m])
|
||||
# Wrong status also shifts the open count, so expect the status issue present.
|
||||
assert any("unrecognised Status" in i for i in issues), issues
|
||||
|
||||
|
||||
def test_summarize_truncates_long_text():
|
||||
long = "x" * 500
|
||||
out = regen.summarize(long)
|
||||
assert len(out) <= 240 and out.endswith("…"), len(out)
|
||||
assert regen.summarize("short") == "short"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
tests = sorted(
|
||||
(name, fn)
|
||||
for name, fn in globals().items()
|
||||
if name.startswith("test_") and callable(fn)
|
||||
)
|
||||
failed = 0
|
||||
for name, fn in tests:
|
||||
try:
|
||||
fn()
|
||||
print(f"PASS {name}")
|
||||
except Exception: # noqa: BLE001 - test runner reports all failures
|
||||
failed += 1
|
||||
print(f"FAIL {name}")
|
||||
traceback.print_exc()
|
||||
print(f"\n{len(tests) - failed}/{len(tests)} passed.")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user