260 lines
10 KiB
Python
260 lines
10 KiB
Python
# 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()
|