import contextlib import io import os import subprocess import sys import tempfile import unittest from pathlib import Path from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from check_conventions import STUB_BODY, SYSTEM_DEPENDENCY, Check, function_body, links, output class FunctionBodyTests(unittest.TestCase): def test_line_comments_preserve_following_statements(self): sources = ( "int APS5_VABI sceExample() {\n// pending implementation\nreturn 0;\n}", "int APS5_VABI sceExample() // pending implementation\n{\nreturn 0;\n}", "int APS5_VABI sceExample() {\n(void)arg; // unused argument\nreturn 0;\n}", "int APS5_VABI sceExample() {\n// ignored } ; { tokens\nreturn 0;\n}", ) for source in sources: with self.subTest(source=source): body = function_body(source.splitlines(), 0) self.assertIsNotNone(body) self.assertIsNotNone(STUB_BODY.fullmatch(body)) def test_line_comment_does_not_turn_an_implementation_into_a_stub(self): source = "int APS5_VABI sceExample() {\n// validate the request\nvalidate();\nreturn 0;\n}" body = function_body(source.splitlines(), 0) self.assertEqual(body, "validate();return0;") self.assertIsNone(STUB_BODY.fullmatch(body)) def test_block_comments_preserve_following_statements(self): source = "int APS5_VABI sceExample() {\n/* pending\nimplementation */\nreturn 0;\n}" self.assertEqual(function_body(source.splitlines(), 0), "return0;") def test_comment_markers_and_braces_in_strings_are_ignored(self): source = 'int APS5_VABI sceExample() {\nuse("// }", "/* { */");\nreturn 0;\n}' self.assertEqual(function_body(source.splitlines(), 0), 'use("","");return0;') def test_raw_literal_braces_and_comments_preserve_following_statements(self): source = 'int APS5_VABI sceExample() {\nuse(R"json({"brace":"}","comment":"/* hidden */"})json");\nvalidate();\nreturn 0;\n}' body = function_body(source.splitlines(), 0) self.assertEqual(body, 'use("");validate();return0;') self.assertIsNone(STUB_BODY.fullmatch(body)) def test_raw_literal_return_keeps_a_literal_sentinel(self): source = 'const char* APS5_VABI sceExample() { return R"json({"url":"https://example.test"})json"; }' body = function_body(source.splitlines(), 0) self.assertEqual(body, 'return"";') self.assertIsNone(STUB_BODY.fullmatch(body)) def test_apostrophes_in_comments_do_not_open_character_literals(self): source = "int APS5_VABI sceExample() {\n// don't fail here\nvalidate();\n// it's fine\nreturn 0;\n}" body = function_body(source.splitlines(), 0) self.assertEqual(body, "validate();return0;") self.assertIsNone(STUB_BODY.fullmatch(body)) source = "int APS5_VABI sceExample() {\n/* don't */\nreturn 0;\n}\nstatic const char Separator = '/';" self.assertEqual(function_body(source.splitlines(), 0), "return0;") source = "int APS5_VABI sceExample() {\nuse(\"http://x\"); // isn't a stub\nreturn 0;\n}" self.assertEqual(function_body(source.splitlines(), 0), 'use("");return0;') def test_declaration_has_no_body(self): source = "int APS5_VABI sceExample();\nint APS5_VABI sceOther() { return 0; }" self.assertIsNone(function_body(source.splitlines(), 0)) def test_unterminated_function_has_no_body(self): source = "int APS5_VABI sceExample() {\n// pending implementation\nreturn 0;" self.assertIsNone(function_body(source.splitlines(), 0)) class RawLiteralCommentTests(unittest.TestCase): path = "core/example.cpp" def findings(self, source, added): check = Check.__new__(Check) check.tree = set() check.status = {} check.findings = [] lines = source.split("\n") check.added = {self.path: [(number, lines[number - 1]) for number in added]} with patch.object(check, "show", return_value=source), patch.object(check, "duplicates"): check.code() return check.findings def test_isolated_checker_loads_its_trusted_sibling_from_another_directory(self): checker = Path(__file__).resolve().parents[1] / "check_conventions.py" with tempfile.TemporaryDirectory() as directory: Path(directory, "nid_names.py").write_text('raise RuntimeError("untrusted current directory")\n', encoding="utf-8") result = subprocess.run([sys.executable, "-I", str(checker), "--help"], cwd=directory, capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertIn("check a branch", result.stdout) def test_raw_url_does_not_create_a_comment(self): source = 'static const char* Metadata = R"json({"url":"https://example.test"})json";' self.assertEqual(self.findings(source, [1]), []) def test_blank_added_cpp_lines_preserve_positions(self): for source, added in (("\n", [1]), ("int value;\n\n", [2])): with self.subTest(source=source): self.assertEqual(self.findings(source, added), []) def test_unicode_separators_in_literals_keep_physical_lines(self): for separator in ("\x85", "\u2028", "\u2029"): with self.subTest(separator=separator): source = f'static const char* Metadata = R"text(https://example.test/{separator}more)text";\nint value; // pending\n' self.assertEqual(self.findings(source, [1, 2]), [("comment", self.path, 2, "")]) def test_added_line_inside_an_existing_multiline_raw_literal_is_not_a_comment(self): source = 'static const char* Metadata = R"text(\nhttps://example.test/metadata\n)text";\n' self.assertEqual(self.findings(source, [2]), []) def test_actual_comment_after_a_raw_literal_retains_its_line(self): for comment in ("// pending implementation", "/* pending implementation */"): with self.subTest(comment=comment): source = f'static const char* Metadata = R"text(\nhttps://example.test/metadata\n)text"; {comment}\n' self.assertEqual(self.findings(source, [2, 3]), [("comment", self.path, 3, "")]) def test_quotes_and_raw_looking_text_do_not_hide_actual_comments(self): for comment in ('// " R"text(https://example.test)text"', '/* " R"text(https://example.test)text" */'): with self.subTest(comment=comment): self.assertEqual(self.findings(comment, [1]), [("comment", self.path, 1, "")]) class SilentStubTests(unittest.TestCase): def test_commented_stub_is_reported_when_debt_change_allows_comments(self): 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", "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") git("add", ".") git("commit", "-qm", "docs: record technical debt") source = root / "core/libs/prx/libSceExample/Export.cpp" source.parent.mkdir(parents=True) source.write_text("int APS5_VABI sceExample() {\n// pending implementation\nreturn 0;\n}\n", encoding="utf-8") debt.write_text("# Project technical debt\n\nAn unrelated build limitation.\n", encoding="utf-8") git("add", ".") git("commit", "-qm", "feat: add example export") result = subprocess.run( [sys.executable, str(checker), "--base", "HEAD~1"], cwd=root, capture_output=True, text=True, env={key: value for key, value in os.environ.items() if key not in ("GITHUB_EVENT_PATH", "GITHUB_STEP_SUMMARY", "GITHUB_ACTIONS")}, ) self.assertEqual(result.returncode, 1, result.stdout + result.stderr) self.assertIn("[silent-stub]", result.stdout) self.assertNotIn("[comment]", result.stdout) class QuotedPathTests(unittest.TestCase): def test_non_ascii_path_does_not_crash_the_checker(self): 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", "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") git("config", "core.quotePath", "true") readme = root / "README.md" readme.write_text("# Example\n", encoding="utf-8") git("add", ".") git("commit", "-qm", "docs: add readme") note = root / "docs/dev/caf\u00e9.md" note.parent.mkdir(parents=True) note.write_text("# Cafe\n\nA short note.\n", encoding="utf-8") source = root / "core/x.cpp" source.parent.mkdir(parents=True) source.write_text("int x; // note\n", encoding="utf-8") git("add", ".") git("commit", "-qm", "feat: add cafe note") result = subprocess.run( [sys.executable, "-I", str(checker), "--base", "HEAD~1"], cwd=root, capture_output=True, text=True, encoding="utf-8", env={key: value for key, value in os.environ.items() if key not in ("GITHUB_EVENT_PATH", "GITHUB_STEP_SUMMARY", "GITHUB_ACTIONS")}, ) self.assertNotIn("Traceback", result.stderr) self.assertIn("[comment]", result.stdout) self.assertIn("core/x.cpp", result.stdout) class SilentStubSectionTests(unittest.TestCase): path = "core/libs/prx/libSceExample/Export.cpp" entry = f"- [Existing limitation](../../../{path}) - console layout is unknown.\n" library_debt = "docs/dev/debt/libSceExample.md" def check_stub(self, debt_text, debt_path=library_debt): 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") (root / debt_path).parent.mkdir(parents=True, exist_ok=True) (root / debt_path).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, debt_path=library_debt): result = self.check_stub(debt, debt_path) self.assertEqual(result.returncode, 1, result.stdout + result.stderr) self.assertIn("[silent-stub]", result.stdout) self.assertNotIn("[doc-link]", result.stdout) self.assertNotIn("[debt-file]", result.stdout) def require_accepted(self, debt, debt_path=library_debt): result = self.check_stub(debt, debt_path) 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"# libSceExample 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"# libSceExample 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"# libSceExample 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"# libSceExample 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"# libSceOther technical debt\n\n### Silent stubs\n\n{entry}", "docs/dev/debt/libSceOther.md") 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"# libSceExample technical debt\n\n### Silent stubs\n\n{entry}") def test_subsection_in_silent_stubs_keeps_exemption(self): self.require_accepted(f"# libSceExample technical debt\n\n### Silent stubs\n\n#### Dialogs\n\n{self.entry}") def test_entry_in_the_index_still_exempts_export(self): entry = self.entry.replace("../../../", "../../") result = self.check_stub(f"# Project technical debt\n\n### Silent stubs\n\n{entry}", "docs/dev/TechnicalDebt.md") self.assertNotIn("[silent-stub]", result.stdout) def test_library_entry_in_the_index_is_rejected(self): entry = self.entry.replace("../../../", "../../") result = self.check_stub(f"# Project technical debt\n\n### Silent stubs\n\n{entry}", "docs/dev/TechnicalDebt.md") self.assertEqual(result.returncode, 1, result.stdout + result.stderr) self.assertIn("docs/dev/TechnicalDebt.md:5: error: [debt-file]", result.stdout) self.assertIn("docs/dev/debt/libSceExample.md", result.stdout) def test_library_entry_in_another_library_file_is_rejected(self): result = self.check_stub(f"# libSceOther technical debt\n\n### Silent stubs\n\n{self.entry}", "docs/dev/debt/libSceOther.md") self.assertEqual(result.returncode, 1, result.stdout + result.stderr) self.assertIn("docs/dev/debt/libSceOther.md:5: error: [debt-file]", result.stdout) def test_component_entry_goes_in_the_component_file(self): result = self.check_stub(f"# shader technical debt\n\n### Functional\n\n- [Example](../../../{self.path}) - gap.\n".replace("libs/prx/libSceExample", "shader"), "docs/dev/debt/shader.md") self.assertNotIn("[debt-file]", result.stdout + result.stderr) def test_entry_goes_with_its_first_core_link(self): entry = f"- [Recompiler](../../../core/shader/Example.cpp) - needs [a driver feature](../../../{self.path}).\n" result = self.check_stub(f"# libSceExample technical debt\n\n### Functional\n\n{entry}") self.assertIn("docs/dev/debt/libSceExample.md:5: error: [debt-file]", result.stdout) self.assertIn("docs/dev/debt/shader.md", result.stdout) def test_entry_without_a_core_link_stays_in_the_index(self): result = self.check_stub("# Project technical debt\n\n### Build\n\n- A toolchain limitation.\n", "docs/dev/TechnicalDebt.md") self.assertNotIn("[debt-file]", result.stdout + result.stderr) class TestFixtureExportTests(unittest.TestCase): def check(self, path, 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, ) def write(relative, content): target = root / relative target.parent.mkdir(parents=True, exist_ok=True) target.write_text(content, encoding="utf-8") git("init", "-q") write("core/libs/prx/libkernel/src/Time.cpp", "std::uint64_t APS5_VABI sceKernelGetProcessTime() { return now(); }\n") write("core/libs/prx/libSceNgs2/tests/Render.cpp", "static int APS5_VABI Process(int value) { return value; }\n") git("add", ".") git("commit", "-qm", "feat: add libraries") write(path, text) git("add", ".") git("commit", "-qm", "feat: add source") 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, "-I", str(checker), "--base", "HEAD~1"], cwd=root, capture_output=True, text=True, env=env, timeout=20, ) def test_mock_in_tests_directory_is_not_an_export(self): result = self.check("core/libs/prx/libSceAudioOut/tests/Shutdown.cpp", 'extern "C" std::uint64_t APS5_VABI sceKernelGetProcessTime() { return 0; }\n') self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertIn("All conventions checks passed", result.stdout) def test_export_named_like_a_test_fixture_is_not_a_duplicate(self): result = self.check("core/libs/prx/libSceAjm/src/Port.cpp", "int APS5_VABI Process(int value) { return value + 1; }\n") self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertIn("All conventions checks passed", result.stdout) def test_export_in_another_library_is_still_a_duplicate(self): result = self.check("core/libs/prx/libSceAudioOut/src/Clock.cpp", "std::uint64_t APS5_VABI sceKernelGetProcessTime() { return now(); }\n") self.assertEqual(result.returncode, 1, result.stdout + result.stderr) self.assertIn("core/libs/prx/libSceAudioOut/src/Clock.cpp:1: error: [duplicate-export]", result.stdout) self.assertIn("sceKernelGetProcessTime is also in libkernel", result.stdout) class PullRequestTemplateTests(unittest.TestCase): def body_findings(self, body): check = Check.__new__(Check) check.findings = [] check.pull_request("fix: x", body) return check.findings def findings(self, line): return self.body_findings(f"### What\nx\n### Tested\nLinux\n### Checklist\n{line}\n- [x] Depends on: none\n- [x] AI-assisted: no\n") def test_unchecked_items_with_any_bullet_are_reported(self): for bullet in ("-", "*", "+"): with self.subTest(bullet=bullet): self.assertEqual(self.findings(f"{bullet} [ ] all"), [("pr-template", "", 0, "unchecked: all")]) def test_checked_item_is_accepted(self): self.assertEqual(self.findings("- [x] all"), []) def test_subsections_stay_inside_their_section(self): body = "### What\nx\n### Tested\n#### Linux\nctest\n#### Windows\nctest\n### Checklist\n- [x] Depends on: none\n- [x] AI-assisted: no\n" self.assertEqual(self.body_findings(body), []) def test_unchecked_item_after_a_checklist_subsection_is_reported(self): body = "### What\nx\n### Tested\nLinux\n### Checklist\n- [x] Depends on: none\n- [x] AI-assisted: no\n#### Later\n- [ ] all\n" self.assertEqual(self.body_findings(body), [("pr-template", "", 0, "unchecked: all")]) class SpacedPathTests(unittest.TestCase): def test_comment_in_path_with_space_is_reported(self): 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", "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") (root / "readme.txt").write_text("x\n", encoding="utf-8") git("add", ".") git("commit", "-qm", "docs: add readme") source = root / "core/a b.cpp" source.parent.mkdir(parents=True) source.write_text("int x; // note\n", encoding="utf-8") git("add", ".") git("commit", "-qm", "feat: add spaced file") result = subprocess.run( [sys.executable, "-I", str(checker), "--base", "HEAD~1"], cwd=root, capture_output=True, text=True, env={key: value for key, value in os.environ.items() if key not in ("GITHUB_EVENT_PATH", "GITHUB_STEP_SUMMARY", "GITHUB_ACTIONS")}, ) self.assertEqual(result.returncode, 1, result.stdout + result.stderr) self.assertIn("core/a b.cpp:1: error: [comment]", result.stdout) class SystemDependencyTests(unittest.TestCase): def test_flags_system_lookups(self): for line in ("find_package( SDL2 REQUIRED)", "FIND_PACKAGE(SDL2)", "find_path(X x.h)", "find_program(P prog)"): with self.subTest(line=line): self.assertIsNotNone(SYSTEM_DEPENDENCY.search(line)) def test_allows_exempt_lines(self): for line in ("find_package(Python3 COMPONENTS Interpreter)", "find_package( Threads REQUIRED )", "FIND_PACKAGE(Git)", "add_library(x)"): with self.subTest(line=line): self.assertIsNone(SYSTEM_DEPENDENCY.search(line)) class RepositoryChecksTests(unittest.TestCase): def setUp(self): self.directory = tempfile.TemporaryDirectory() self.root = Path(self.directory.name) self.original = Path.cwd() os.chdir(self.root) self.git("init", "-q") (self.root / "README.md").write_text("Project\n", encoding="utf-8") self.commit() def tearDown(self): os.chdir(self.original) self.directory.cleanup() def git(self, *args): return subprocess.run(["git", "-c", "user.name=Conventions Test", "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], capture_output=True, text=True, check=True).stdout.strip() def commit(self): self.git("add", ".") self.git("commit", "-qm", "test: add convention fixture") def add_thirdparty_submodule(self, gitmodules=None): head = self.git("rev-parse", "HEAD") if gitmodules is not None: (self.root / ".gitmodules").write_text(gitmodules, encoding="utf-8") self.git("add", ".gitmodules") self.git("update-index", "--add", "--cacheinfo", "160000", head, "3rdparty/vendor") self.git("commit", "-qm", "build: add submodule") def test_quoted_utf8_paths_are_checked(self): name = "docs/dev/測試guide.md" if os.name == "nt" else "docs/dev/測試\tguide.md" path = self.root / name path.parent.mkdir(parents=True) path.write_text("[missing](absent.md)\n", encoding="utf-8") self.commit() check = Check("HEAD~1", "HEAD") check.repository() check.docs() self.assertEqual(check.findings, [("doc-link", name, 1, "absent.md")]) def test_mixed_patch_types_preserve_file_order(self): name = "a b.cpp" if os.name == "nt" else "a\nb.cpp" (self.root / "README.md").unlink() (self.root / "a.bin").write_bytes(b"binary\x00data") (self.root / name).write_text("// debt\n", encoding="utf-8") self.commit() check = Check("HEAD~1", "HEAD") check.code() check.repository() self.assertIn(("comment", name, 1, ""), check.findings) self.assertIn(("binary", "a.bin", 0, ""), check.findings) self.assertNotIn("README.md", check.added) def test_increment_line_is_not_a_patch_file_header(self): path = self.root / "core/example.cpp" path.parent.mkdir() path.write_text("void example() {\n++ counter;\n// debt\n}\n", encoding="utf-8") self.commit() check = Check("HEAD~1", "HEAD") check.code() self.assertIn(("comment", "core/example.cpp", 3, ""), check.findings) def test_unicode_raw_literals_keep_git_patch_and_grep_records_intact(self): literals = 'R"text(foo\u2028+tail\u2029+tail)text"' fixtures = ( ("core/example.cpp", f"static const char* Metadata = {literals};\n"), ("core/libs/prx/libSceExample/Export.cpp", f"const char* APS5_VABI sceGetLabel() {{ return {literals}; }}\n"), ) for name, definition in fixtures: with self.subTest(path=name): path = self.root / name path.parent.mkdir(parents=True, exist_ok=True) path.write_text(definition + "int value; // pending\n", encoding="utf-8") self.commit() check = Check("HEAD~1", "HEAD") self.assertEqual([number for number, _ in check.added[name]], [1, 2]) check.code() self.assertEqual(check.findings, [("comment", name, 2, "")]) def test_checks_from_a_subdirectory_see_the_whole_repository(self): definition = "int APS5_VABI sceFooBar(int a, int b) { return a + b; }\n" first = self.root / "core/libs/prx/libSceA/Export.cpp" first.parent.mkdir(parents=True) first.write_text(definition, encoding="utf-8") (self.root / "tools").mkdir() (self.root / "tools/tool.py").write_text("print()\n", encoding="utf-8") self.commit() second = self.root / "core/libs/prx/libSceB/Export.cpp" second.parent.mkdir(parents=True) second.write_text(definition, encoding="utf-8") (self.root / "docs").mkdir() (self.root / "docs/guide.md").write_text("[export](../core/libs/prx/libSceA/Export.cpp)\n", encoding="utf-8") self.commit() os.chdir(self.root / "tools") check = Check("HEAD~1", "HEAD") check.code() check.docs() self.assertEqual(check.findings, [("duplicate-export", "core/libs/prx/libSceB/Export.cpp", 1, "sceFooBar is also in libSceA")]) def test_submodule_outside_thirdparty_reports_without_crashing(self): head = self.git("rev-parse", "HEAD") self.git("update-index", "--add", "--cacheinfo", "160000", head, "vendor") self.git("commit", "-qm", "build: add invalid submodule") check = Check("HEAD~1", "HEAD") check.repository() self.assertIn(("system-dependency", "vendor", 0, "submodule outside 3rdparty/"), check.findings) def test_thirdparty_submodule_remains_allowed(self): self.add_thirdparty_submodule('[submodule "vendor"]\n\tpath = 3rdparty/vendor\n' "\turl = https://example.com/vendor.git\n") check = Check("HEAD~1", "HEAD") check.repository() self.assertEqual(check.findings, []) def test_thirdparty_submodule_without_gitmodules_entry_is_reported(self): self.add_thirdparty_submodule() check = Check("HEAD~1", "HEAD") check.repository() self.assertEqual(check.findings, [("system-dependency", "3rdparty/vendor", 0, "submodule without a url in .gitmodules")]) def test_dropping_a_submodule_url_is_reported(self): self.add_thirdparty_submodule('[submodule "vendor"]\n\tpath = 3rdparty/vendor\n' "\turl = https://example.com/vendor.git\n") (self.root / ".gitmodules").write_text('[submodule "vendor"]\n\tpath = 3rdparty/vendor\n', encoding="utf-8") self.git("add", ".gitmodules") self.git("commit", "-qm", "build: drop submodule url") check = Check("HEAD~1", "HEAD") check.repository() self.assertEqual(check.findings, [("system-dependency", "3rdparty/vendor", 0, "submodule without a url in .gitmodules")]) def test_relative_link_url_decoding(self): self.assertEqual(links("docs/dev/index.md", "[guide](./space%20name.md#details)"), [("./space%20name.md", "docs/dev/space name.md")]) self.assertEqual(links("docs/dev/index.md", "[reference](HTTPS://example.com/x)"), []) self.assertEqual(links("docs/dev/index.md", "[reference](//example.com/x)"), []) def test_percent_encoded_fragment_is_part_of_filename(self): self.assertEqual(links("docs/dev/index.md", "[guide](./part%23one.md#details)"), [("./part%23one.md", "docs/dev/part#one.md")]) def test_annotation_filename_properties_are_escaped(self): check = type("Findings", (), {"findings": [("doc-link", "docs/name,with:colon%\n.md", 2, "missing") ]})() original = os.environ.get("GITHUB_ACTIONS") os.environ["GITHUB_ACTIONS"] = "true" try: with contextlib.redirect_stdout(io.StringIO()) as captured: output(check, "owner/repo") finally: if original is None: os.environ.pop("GITHUB_ACTIONS", None) else: os.environ["GITHUB_ACTIONS"] = original annotation = captured.getvalue().splitlines()[0] self.assertIn("file=docs/name%2Cwith%3Acolon%25%0A.md,line=2", annotation) if __name__ == "__main__": unittest.main()