# file: tests/test_rust_export_completeness.py # version: 2 """Regression tests for the optional Rust crate-root façade audit.""" from __future__ import annotations import pathlib import subprocess import sys import tempfile import unittest sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "scripts")) from audit_rust_export_completeness import audit_workspace # noqa: E402 class RustExportCompletenessTests(unittest.TestCase): def setUp(self) -> None: self.temporary = tempfile.TemporaryDirectory() self.addCleanup(self.temporary.cleanup) self.root = pathlib.Path(self.temporary.name) def crate(self, relative: str, root_source: str, files: dict[str, str]) -> pathlib.Path: crate = self.root / relative (crate / "src").mkdir(parents=True) (crate / "Cargo.toml").write_text( '[package]\nname = "fixture"\nversion = "0.1.0"\nedition = "2024"\n', encoding="utf-8", ) (crate / "src/lib.rs").write_text(root_source, encoding="utf-8") for name, text in files.items(): path = crate / name path.parent.mkdir(parents=True, exist_ok=True) path.write_text(text, encoding="utf-8") return crate def candidates(self) -> list[object]: _, candidates = audit_workspace(self.root) return candidates def test_nested_crate_layout_is_discovered(self) -> None: self.crate("crates/common/example-lib", "mod api;\npub use self::api::Thing;\n", {"src/api.rs": "pub struct Thing;\n"}) crates, candidates = audit_workspace(self.root) self.assertEqual(len(crates), 1) self.assertEqual(candidates, []) def test_missing_public_reexport_is_rejected(self) -> None: self.crate("crates/example-lib", "mod api;\n", {"src/api.rs": "pub struct Thing;\n"}) candidates = self.candidates() self.assertEqual([candidate.code for candidate in candidates], ["RUST-API-201"]) def test_external_crate_public_item_requires_reexport(self) -> None: self.crate( "crates/example-lib", "mod api;\nmod consumer;\n", { "src/api.rs": "pub(crate) const VALUE: u8 = 1;\n", "src/consumer.rs": "pub(crate) fn value() -> u8 { return crate::api::VALUE; }\n", }, ) self.assertTrue(any(candidate.code == "RUST-API-201" for candidate in self.candidates())) def test_unused_crate_public_item_does_not_force_reexport(self) -> None: self.crate("crates/example-lib", "mod api;\n", {"src/api.rs": "pub(crate) const VALUE: u8 = 1;\n"}) self.assertEqual(self.candidates(), []) def test_exported_item_requires_crate_root_path(self) -> None: self.crate( "crates/example-lib", "mod api;\nmod consumer;\npub use self::api::Thing;\n", { "src/api.rs": "pub struct Thing;\n", "src/consumer.rs": "pub(crate) fn make() -> crate::api::Thing { return crate::api::Thing; }\n", }, ) candidates = self.candidates() self.assertTrue(any(candidate.code == "RUST-IMPORT-201" for candidate in candidates)) def test_grouped_and_aliased_reexports_are_understood(self) -> None: self.crate( "crates/example-lib", "mod api;\npub use self::api::{First, Second as PublicSecond};\n", {"src/api.rs": "pub struct First;\npub struct Second;\n"}, ) self.assertEqual(self.candidates(), []) def test_mod_rs_maps_to_parent_module(self) -> None: self.crate( "crates/example-lib", "mod api;\npub use self::api::Thing;\n", {"src/api/mod.rs": "pub struct Thing;\n"}, ) self.assertEqual(self.candidates(), []) def test_comments_and_literals_do_not_create_long_path_candidates(self) -> None: self.crate( "crates/example-lib", "mod api;\nmod consumer;\npub use self::api::Thing;\n", { "src/api.rs": "pub struct Thing;\n", "src/consumer.rs": 'pub(crate) fn text() -> &\'static str { return "crate::api::Thing"; } // crate::api::Thing\n', }, ) self.assertEqual(self.candidates(), []) def test_bare_reference_in_declaration_module_is_rejected(self) -> None: self.crate( "crates/example-lib", "mod api;\npub use self::api::Thing;\n", { "src/api.rs": ( "pub struct Thing;\n" "impl Thing {\n" " pub fn make() -> Thing { return Thing; }\n" "}\n" ), }, ) candidates = [candidate for candidate in self.candidates() if candidate.code == "RUST-IMPORT-201"] self.assertEqual(len(candidates), 2) self.assertTrue(all("crate::Thing" in candidate.message for candidate in candidates)) def test_crate_root_reference_in_declaration_module_is_clean(self) -> None: self.crate( "crates/example-lib", "mod api;\npub use self::api::Thing;\n", { "src/api.rs": ( "pub struct Thing;\n" "impl crate::Thing {\n" " pub fn make() -> crate::Thing { return crate::Thing; }\n" "}\n" ), }, ) self.assertEqual(self.candidates(), []) def test_import_then_bare_reference_is_rejected(self) -> None: self.crate( "crates/example-lib", "mod api;\nmod consumer;\npub use self::api::Thing;\n", { "src/api.rs": "pub struct Thing;\n", "src/consumer.rs": "use crate::Thing;\npub(crate) fn make() -> Thing { return Thing; }\n", }, ) candidates = [candidate for candidate in self.candidates() if candidate.code == "RUST-IMPORT-201"] self.assertEqual(len(candidates), 1) def test_self_super_and_internal_module_paths_are_rejected(self) -> None: self.crate( "crates/example-lib", "mod api;\nmod consumer;\npub use self::api::Thing;\n", { "src/api.rs": "pub struct Thing;\n", "src/consumer.rs": ( "pub(crate) fn a() -> crate::api::Thing { return crate::api::Thing; }\n" "pub(crate) fn b() -> api::Thing { todo!() }\n" ), }, ) candidates = [candidate for candidate in self.candidates() if candidate.code == "RUST-IMPORT-201"] self.assertEqual(len(candidates), 2) def test_attached_unit_test_visible_item_uses_crate_root(self) -> None: self.crate( "crates/example-lib", "mod api;\npub use self::api::Thing;\n", { "src/api.rs": ( "pub struct Thing;\n" "#[cfg(test)]\n" "#[path = \"../unit_tests/api_tests.rs\"]\n" "mod tests;\n" ), "unit_tests/api_tests.rs": "fn make() -> super::Thing { return super::Thing; }\n", }, ) candidates = [candidate for candidate in self.candidates() if candidate.code == "RUST-IMPORT-201"] self.assertEqual(len(candidates), 1) def test_private_parent_item_in_attached_test_is_not_owned_by_this_audit(self) -> None: self.crate( "crates/example-lib", "mod api;\n", { "src/api.rs": ( "fn helper() -> u8 { return 1; }\n" "#[cfg(test)]\n" "#[path = \"../unit_tests/api_tests.rs\"]\n" "mod tests;\n" ), "unit_tests/api_tests.rs": "fn value() -> u8 { return super::helper(); }\n", }, ) self.assertEqual(self.candidates(), []) def test_external_qualified_same_named_item_is_not_misclassified(self) -> None: self.crate( "crates/example-lib", "mod api;\nmod consumer;\npub use self::api::Result;\n", { "src/api.rs": "pub struct Result;\n", "src/consumer.rs": "pub(crate) fn value() -> external_crate::Result { todo!() }\n", }, ) self.assertEqual(self.candidates(), []) def test_method_with_same_name_as_public_function_is_not_misclassified(self) -> None: self.crate( "crates/example-lib", "mod api;\nmod consumer;\npub use self::api::run;\n", { "src/api.rs": "pub fn run() -> u8 { return 1; }\n", "src/consumer.rs": "pub(crate) fn value(service: &Service) -> u8 { return service.run(); }\nstruct Service;\nimpl Service { fn run(&self) -> u8 { return 2; } }\n", }, ) self.assertEqual(self.candidates(), []) def test_lib_and_main_targets_are_not_conflated(self) -> None: crate = self.crate( "crates/example-app", "mod api;\npub use self::api::Thing;\n", {"src/api.rs": "pub struct Thing;\n"}, ) (crate / "src/main.rs").write_text( "fn main() { let _ = fixture::Thing; }\n", encoding="utf-8", ) self.assertEqual(self.candidates(), []) def test_cli_status_for_clean_violation_report_only_and_missing_crates(self) -> None: script = pathlib.Path(__file__).resolve().parents[1] / "scripts" / "audit_rust_export_completeness.py" def run(root: pathlib.Path, *extra: str) -> subprocess.CompletedProcess[str]: return subprocess.run( [sys.executable, "-B", str(script), "--root", str(root), *extra], capture_output=True, text=True, check=False, ) empty = self.root / "empty" empty.mkdir() self.assertEqual(run(empty).returncode, 2) self.crate("crates/example-lib", "mod api;\npub use self::api::Thing;\n", {"src/api.rs": "pub struct Thing;\n"}) self.assertEqual(run(self.root).returncode, 0) (self.root / "crates/example-lib/src/lib.rs").write_text("mod api;\n", encoding="utf-8") self.assertEqual(run(self.root).returncode, 1) self.assertEqual(run(self.root, "--report-only").returncode, 0) if __name__ == "__main__": unittest.main()