diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 8c3c27239..2f073419e 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -83,7 +83,7 @@ class Check: return git("show", f"{self.head}:{path}") def code(self): - debt = [(text, links("docs/dev/TechnicalDebt.md", text)) for text in self.show("docs/dev/TechnicalDebt.md").splitlines()] if "docs/dev/TechnicalDebt.md" in self.tree else [] + debt = silent_stub_debt(self.show("docs/dev/TechnicalDebt.md")) if "docs/dev/TechnicalDebt.md" in self.tree else [] exports = {} for path, lines in self.files(lambda p: p.suffix in CPP): core = path.startswith("core/") and "tests" not in PurePosixPath(path).parts @@ -193,6 +193,22 @@ def links(path, text): return result +def silent_stub_debt(text): + debt = [] + section_level = 0 + for line in text.splitlines(): + heading = re.match(r"^(#{1,6})\s+(.+?)\s*$", line) + if heading: + level = len(heading[1]) + if heading[2] == "Silent stubs": + section_level = level + elif level <= section_level: + section_level = 0 + if section_level: + debt.append((line, links("docs/dev/TechnicalDebt.md", line))) + return debt + + def listed(debt, name, path): own = PurePosixPath(path).parts[:4] for text, targets in debt: diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index c4af4a8a0..3bc70b7bd 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -92,5 +92,84 @@ class SilentStubTests(unittest.TestCase): self.assertNotIn("[comment]", result.stdout) +class SilentStubSectionTests(unittest.TestCase): + path = "core/libs/prx/libSceExample/Export.cpp" + entry = f"- [Existing limitation](../../{path}) - console layout is unknown.\n" + + def check_stub(self, debt_text): + checker = Path(__file__).resolve().parents[1] / "check_conventions.py" + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + + def git(*args): + return subprocess.run( + ["git", "-c", f"safe.directory={root}", "-c", "user.name=Conventions Test", + "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + cwd=root, capture_output=True, text=True, check=True, + ) + + git("init", "-q") + debt = root / "docs/dev/TechnicalDebt.md" + debt.parent.mkdir(parents=True) + debt.write_text("# Project technical debt\n", encoding="utf-8") + for library in ("libSceExample", "libSceOther"): + reference = root / "core/libs/prx" / library / "Reference.hpp" + reference.parent.mkdir(parents=True) + reference.write_text("", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "docs: record technical debt") + source = root / self.path + source.write_text("int APS5_VABI sceExample() { return 0; }\n", encoding="utf-8") + debt.write_text(debt_text, encoding="utf-8") + git("add", ".") + git("commit", "-qm", "feat: add example export") + env = {key: value for key, value in os.environ.items() + if key not in ("GITHUB_EVENT_PATH", "GITHUB_STEP_SUMMARY", "GITHUB_ACTIONS")} + env.update(GIT_CONFIG_COUNT="1", GIT_CONFIG_KEY_0="safe.directory", GIT_CONFIG_VALUE_0=str(root)) + return subprocess.run( + [sys.executable, str(checker), "--base", "HEAD~1"], + cwd=root, capture_output=True, text=True, env=env, timeout=10, + ) + + def require_rejected(self, debt): + result = self.check_stub(debt) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("[silent-stub]", result.stdout) + self.assertNotIn("[doc-link]", result.stdout) + + def require_accepted(self, debt): + result = self.check_stub(debt) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("All conventions checks passed", result.stdout) + + def test_debt_outside_silent_stubs_does_not_exempt_export(self): + for section in ("Build", "Unknown function info", "Functional"): + with self.subTest(section=section): + self.require_rejected(f"# Project technical debt\n\n### {section}\n\n{self.entry}") + + def test_entries_after_silent_stubs_do_not_exempt_export(self): + for heading in ("# Other debt", "## Other debt", "### Functional"): + with self.subTest(heading=heading): + self.require_rejected(f"# Project technical debt\n\n### Silent stubs\n\n{heading}\n\n{self.entry}") + + def test_file_entry_in_silent_stubs_exempts_export(self): + self.require_accepted(f"# Project technical debt\n\n### Silent stubs\n\n{self.entry}") + + def test_named_entry_in_same_library_exempts_export(self): + entry = "- sceExample: [reference](../../core/libs/prx/libSceExample/Reference.hpp)\n" + self.require_accepted(f"# Project technical debt\n\n### Silent stubs\n\n{entry}") + + def test_named_entry_in_another_library_does_not_exempt_export(self): + entry = "- sceExample: [reference](../../core/libs/prx/libSceOther/Reference.hpp)\n" + self.require_rejected(f"# Project technical debt\n\n### Silent stubs\n\n{entry}") + + def test_different_function_in_same_library_does_not_exempt_export(self): + entry = "- sceExampleOther: [reference](../../core/libs/prx/libSceExample/Reference.hpp)\n" + self.require_rejected(f"# Project technical debt\n\n### Silent stubs\n\n{entry}") + + def test_subsection_in_silent_stubs_keeps_exemption(self): + self.require_accepted(f"# Project technical debt\n\n### Silent stubs\n\n#### Dialogs\n\n{self.entry}") + + if __name__ == "__main__": unittest.main()