返回 CodeWhale
test_check_command_crate_boundaries.py
根目录 / scripts / test_check_command_crate_boundaries.py
1 #!/usr/bin/env python3
2 """Hermetic tests for the FEAT-014 command-contract boundary gate."""
3
4 from __future__ import annotations
5
6 import importlib.util
7 import sys
8 import tempfile
9 import unittest
10 from pathlib import Path
11
12 ROOT = Path(__file__).resolve().parents[1]
13 SCRIPT = ROOT / "scripts" / "check-command-crate-boundaries.py"
14 SPEC = importlib.util.spec_from_file_location("command_boundary", SCRIPT)
15 assert SPEC and SPEC.loader
16 mod = importlib.util.module_from_spec(SPEC)
17 sys.modules[SPEC.name] = mod
18 SPEC.loader.exec_module(mod)
19
20
21 def valid_graph() -> dict[str, set[str]]:
22 return {
23 "codewhale-command-contract": {"codewhale-core"},
24 "codewhale-core": set(),
25 "codewhale-secrets": {"codewhale-paths"},
26 "codewhale-paths": set(),
27 "codewhale-tui": set(),
28 }
29
30
31 class DependencyTests(unittest.TestCase):
32 def test_leaf_graph_passes(self) -> None:
33 self.assertEqual(mod.check_dependency_graph(valid_graph()), [])
34
35 def test_direct_tui_edge_fails(self) -> None:
36 graph = valid_graph()
37 graph["codewhale-command-contract"].add("codewhale-tui")
38 self.assertEqual(len(mod.check_dependency_graph(graph)), 1)
39
40 def test_transitive_tui_edge_fails(self) -> None:
41 graph = valid_graph()
42 graph["codewhale-core"].add("codewhale-tui")
43 self.assertEqual(len(mod.check_dependency_graph(graph)), 1)
44
45 def test_missing_contract_fails(self) -> None:
46 graph = valid_graph()
47 del graph["codewhale-command-contract"]
48 violations = mod.check_dependency_graph(graph)
49 self.assertEqual(len(violations), 1)
50 self.assertIn("missing", str(violations[0]))
51
52 def test_missing_sanitizer_fails(self) -> None:
53 graph = valid_graph()
54 del graph["codewhale-secrets"]
55 violations = mod.check_dependency_graph(graph)
56 self.assertEqual(len(violations), 1)
57 self.assertIn("codewhale-secrets", str(violations[0]))
58
59 def test_sanitizer_reaching_tui_fails(self) -> None:
60 # The shared sanitizer is consumed by portable command helpers, so a TUI
61 # edge would pull the whole TUI into the extracted command crate.
62 graph = valid_graph()
63 graph["codewhale-paths"].add("codewhale-tui")
64 violations = mod.check_dependency_graph(graph)
65 self.assertEqual(len(violations), 1)
66 self.assertIn("codewhale-secrets", str(violations[0]))
67
68 def test_both_packages_reaching_tui_fails_twice(self) -> None:
69 graph = valid_graph()
70 graph["codewhale-core"].add("codewhale-tui")
71 graph["codewhale-paths"].add("codewhale-tui")
72 self.assertEqual(len(mod.check_dependency_graph(graph)), 2)
73
74 def test_dev_dependency_is_not_a_normal_edge(self) -> None:
75 metadata = {"packages": [
76 {"name": "codewhale-command-contract", "dependencies": [
77 {"name": "codewhale-tui", "kind": "dev"},
78 {"name": "codewhale-core", "kind": None},
79 ]},
80 {"name": "codewhale-core", "dependencies": []},
81 {"name": "codewhale-secrets", "dependencies": []},
82 {"name": "codewhale-tui", "dependencies": []},
83 ]}
84 graph = mod.dependency_graph(metadata)
85 self.assertEqual(graph["codewhale-command-contract"], {"codewhale-core"})
86 self.assertEqual(mod.check_dependency_graph(graph), [])
87
88
89 class SourceTests(unittest.TestCase):
90 def test_clean_shapes_pass(self) -> None:
91 source = "pub struct CommandContexts<'a> {}\npub trait CommandModelContext {}\n"
92 self.assertEqual(mod.check_contract_source_text(source, "clean.rs"), [])
93
94 def test_forbidden_edges_fail(self) -> None:
95 cases = [
96 "use codewhale_tui::tui::app::App;",
97 "use ratatui::widgets::Paragraph;",
98 "use crate::tui::App;",
99 "pub struct CommandContext {}",
100 "let handler: Box<dyn Fn()> = value;",
101 ]
102 for source in cases:
103 with self.subTest(source=source):
104 self.assertTrue(mod.check_contract_source_text(source, "sample.rs"))
105
106 def test_comments_and_plural_envelope_pass(self) -> None:
107 source = (
108 "// Never import codewhale_tui or define CommandContext here.\n"
109 "pub struct CommandContexts<'a> { marker: &'a str }\n"
110 )
111 self.assertEqual(mod.check_contract_source_text(source, "safe.rs"), [])
112
113
114 class TreeModeTests(unittest.TestCase):
115 RULE = mod.BoundaryRule(
116 "codewhale-runtime",
117 "tree",
118 ("codewhale-tui", "ratatui", "crossterm"),
119 "the runtime must stay UI-free",
120 )
121
122 def test_parse_cargo_tree_keeps_names(self) -> None:
123 text = "codewhale-runtime v0.10.0 (/x)\nanyhow v1.0.100\nratatui v0.30.2 (*)\n"
124 self.assertEqual(mod.parse_cargo_tree(text), {"codewhale-runtime", "anyhow", "ratatui"})
125
126 def test_clean_tree_passes(self) -> None:
127 self.assertEqual(mod.check_tree_packages(self.RULE, {"anyhow", "serde"}), [])
128
129 def test_runtime_rule_is_tree_mode(self) -> None:
130 rule = next(r for r in mod.BOUNDARY_RULES if r.package == "codewhale-runtime")
131 self.assertEqual(rule.mode, "tree")
132 self.assertIn("ratatui", rule.forbidden_packages)
133 self.assertIn("crossterm", rule.forbidden_packages)
134
135 def test_runtime_source_scan(self) -> None:
136 self.assertEqual(
137 mod.check_runtime_source_text("// ratatui::Frame is not used here\nfn f() {}\n", "a.rs"),
138 [],
139 )
140 for source in (
141 "use crossterm::terminal;",
142 "let _ = ratatui::style::Color::Reset;",
143 'const X: &str = include_str!("../../tui/assets/x.json");',
144 ):
145 with self.subTest(source=source):
146 self.assertTrue(mod.check_runtime_source_text(source, "a.rs"))
147
148 def test_ui_library_in_tree_fails(self) -> None:
149 violations = mod.check_tree_packages(self.RULE, {"anyhow", "ratatui", "crossterm"})
150 self.assertEqual(len(violations), 2)
151 self.assertIn("ratatui", str(violations[1]) + str(violations[0]))
152
153
154 def write_tree(root: Path, files: dict[str, str]) -> None:
155 for rel, text in files.items():
156 path = root / rel
157 path.parent.mkdir(parents=True, exist_ok=True)
158 path.write_text(text, encoding="utf-8")
159
160
161 class RatchetTests(unittest.TestCase):
162 def setUp(self) -> None:
163 self.graph = mod.load_runtime_ratchet()
164
165 def report(self, tui: dict[str, str], runtime: dict[str, str] | None = None):
166 with tempfile.TemporaryDirectory() as tmp:
167 write_tree(Path(tmp, "tui"), tui)
168 write_tree(Path(tmp, "runtime"), runtime or {})
169 return self.graph.build_report(Path(tmp, "tui"), Path(tmp, "runtime"))
170
171 def test_counts_grouped_imports_and_masks_comments_and_strings(self) -> None:
172 report = self.report({
173 "lib.rs": "mod core; mod tui;\n",
174 "core.rs": (
175 "use crate::{tui::App, tui::views::{A, B}};\n"
176 "// crate::tui::ignored\n"
177 "const S: &str = \"crate::tui::ignored\";\n"
178 "fn f() { crate::tui::draw(); }\n"
179 "#[cfg(test)]\nmod tests { fn t() { crate::tui::fixture(); } }\n"
180 ),
181 "tui.rs": "",
182 })
183 self.assertEqual(report.counts["prod"], {"core|tui": 4})
184 self.assertEqual(report.counts["test"], {"core|tui": 1})
185
186 def test_ui_library_and_late_edges_are_counted(self) -> None:
187 report = self.report({
188 "lib.rs": "mod core; mod exec_agent;\n",
189 "core.rs": "fn f() { crossterm::terminal::enable_raw_mode(); }\n"
190 "#[cfg(test)]\nmod tests { fn t() { crate::exec_agent::run(); } }\n",
191 "exec_agent.rs": "",
192 })
193 self.assertEqual(report.counts["uilib"], {"core|crossterm": 1})
194 self.assertEqual(report.counts["late"], {"core|exec_agent": 1})
195
196 def test_runtime_crate_modules_join_the_closure(self) -> None:
197 report = self.report(
198 {"lib.rs": "mod core;\nuse codewhale_runtime::{elapsed};\n", "core.rs": ""},
199 {"lib.rs": "pub mod elapsed;\n", "elapsed.rs": "fn f() { ratatui::x(); }\n"},
200 )
201 self.assertIn("elapsed", report.closure)
202 self.assertEqual(report.counts["uilib"], {"elapsed|ratatui": 1})
203
204 def test_rise_and_unrecorded_drop_both_fail(self) -> None:
205 report = self.report({
206 "lib.rs": "mod core; mod tui;\n",
207 "core.rs": "fn f() { crate::tui::a(); crate::tui::b(); }\n",
208 "tui.rs": "",
209 })
210 rises, drops = self.graph.compare({"counts": {"prod": {"core|tui": 1}}}, report)
211 self.assertTrue(rises and rises[0].startswith("prod core|tui: 1 -> 2"))
212 self.assertEqual(drops, [])
213 rises, drops = self.graph.compare({"counts": {"prod": {"core|tui": 3}}}, report)
214 self.assertEqual((rises, drops), ([], ["prod core|tui: 3 -> 2"]))
215
216 def test_super_chains_that_reach_the_root_count(self) -> None:
217 report = self.report({
218 "lib.rs": "mod core; mod tui;\n",
219 "core/mod.rs": (
220 "mod inner;\n"
221 "fn f() { super::tui::a(); }\n"
222 "pub(in super::super) fn g() {}\n"
223 "#[cfg(test)]\nmod tests { use super::super::{tui::B, x}; use super::*; }\n"
224 ),
225 # Depth 2: two `super`s reach the root, one stays inside `core`.
226 "core/inner.rs": "use super::super::tui as ui;\nfn f() { super::tui::local(); }\n",
227 "tui.rs": "",
228 })
229 self.assertEqual(report.counts["prod"], {"core|tui": 2})
230 self.assertEqual(report.counts["test"], {"core|tui": 1})
231
232 def test_hand_raised_baseline_fails_against_the_base(self) -> None:
233 previous = {"counts": {"prod": {"tools|tui": 8}}}
234 self.assertEqual(self.graph.baseline_raises(previous, previous), [])
235 self.assertEqual(
236 self.graph.baseline_raises(previous, {"counts": {"prod": {"tools|tui": 7}}}), []
237 )
238 self.assertEqual(
239 self.graph.baseline_raises(previous, {"counts": {"prod": {"tools|tui": 9}}}),
240 ["prod tools|tui: 8 -> 9"],
241 )
242 self.assertEqual(
243 self.graph.baseline_raises(previous, {"counts": {"test": {"core|tui": 1}}}),
244 ["test core|tui: 0 -> 1 (new pair)"],
245 )
246
247 def test_same_scope_split_preserves_original_test_and_production_counts(self) -> None:
248 original = self.report({
249 "lib.rs": 'mod client; mod tui;',
250 "client.rs": 'fn run() { crate::tui::real(); } #[cfg(test)] mod suite { fn probe() { crate::tui::one(); crate::tui::two(); } }',
251 "tui.rs": '',
252 })
253 split = self.report({
254 "lib.rs": 'mod client; mod tui;',
255 "client.rs": 'fn run() { crate::tui::real(); } #[cfg(test)] mod suite { include!("client/first.rs"); }',
256 "client/first.rs": 'fn probe() { crate::tui::one(); crate::tui::two(); }',
257 "tui.rs": '',
258 })
259 self.assertEqual(split.counts, original.counts)
260 self.assertEqual(split.closure, original.closure)
261 self.assertEqual(split.counts["prod"], {"client|tui": 1})
262 self.assertEqual(split.counts["test"], {"client|tui": 2})
263
264 def test_literal_path_and_recursive_raw_includes_reuse_test_scope(self) -> None:
265 report = self.report({
266 "lib.rs": 'mod session_manager; mod tui;',
267 "session_manager.rs": '#[cfg(test)] #[path="pieces/suite.rs"] mod verification;',
268 "pieces/suite.rs": 'include!(r"first.rs");',
269 "pieces/first.rs": 'include!("second.rs");',
270 "pieces/second.rs": 'fn probe() { crate::tui::one(); }',
271 "tui.rs": '',
272 })
273 # The graph knows these are test-only even though none has a test name.
274 crate = self.graph.load_crate("fixture", Path("/not-a-crate"))
275 self.assertEqual(report.counts["prod"], {})
276 # Independent top-level pieces are outside the runtime closure; inspect
277 # the actual graph for scope below rather than manufacturing a seed.
278 with tempfile.TemporaryDirectory() as tmp:
279 source = Path(tmp) / "src"
280 write_tree(source, {
281 "lib.rs": 'mod client;',
282 "client.rs": '#[cfg(test)] #[path="pieces/suite.rs"] mod verification;',
283 "pieces/suite.rs": 'include!(r"first.rs");',
284 "pieces/first.rs": 'include!("second.rs");',
285 "pieces/second.rs": 'fn probe() {}',
286 })
287 crate = self.graph.load_crate("fixture", source)
288 exact, prefixes = self.graph.test_file_set(crate)
289 self.assertEqual(exact, {"pieces/suite.rs", "pieces/first.rs", "pieces/second.rs"})
290 self.assertEqual(prefixes, set())
291
292 def test_shared_production_includes_override_test_filename_and_descendants(self) -> None:
293 report = self.report({
294 "lib.rs": 'mod client; mod tui;',
295 "client.rs": 'include!("client/shared_tests.rs"); #[cfg(test)] mod suite { include!("client/shared_tests.rs"); }',
296 "client/shared_tests.rs": 'fn real() { crate::tui::one(); } include!("child_test.rs");',
297 "client/child_test.rs": 'fn real_child() { crate::tui::two(); }',
298 "tui.rs": '',
299 })
300 self.assertEqual(report.counts["prod"], {"client|tui": 2})
301 self.assertEqual(report.counts["test"], {})
302
303 def test_cfg_test_module_does_not_exempt_a_guessed_directory_prefix(self) -> None:
304 report = self.report({
305 "lib.rs": 'mod client; mod tui;',
306 "client.rs": '#[cfg(test)] mod suite; #[path="client/suite/shipping.rs"] mod shipping;',
307 "client/suite.rs": 'fn test_only() {}',
308 "client/suite/shipping.rs": 'fn real() { crate::tui::one(); }',
309 "tui.rs": '',
310 })
311 self.assertEqual(report.counts["prod"], {"client|tui": 1})
312 self.assertEqual(report.counts["test"], {})
313
314 def test_comment_dynamic_and_mixed_cfg_cannot_exempt_production_fragments(self) -> None:
315 report = self.report({
316 "lib.rs": 'mod client; mod tui;',
317 "client.rs": '// #[cfg(test)] mod fake { include!("client/part.rs"); }\n'
318 '#[cfg(test)] mod suite { include!(concat!("client/", "part.rs")); }'
319 '#[cfg(any(test, feature="shipping"))] mod shipped { include!("client/part.rs"); }',
320 "client/part.rs": 'fn real() { crate::tui::one(); }',
321 "tui.rs": '',
322 })
323 self.assertEqual(report.counts["prod"], {"client|tui": 1})
324 self.assertEqual(report.counts["test"], {})
325
326 def test_real_custom_cargo_root_outside_src_keeps_test_named_child_production(self) -> None:
327 with tempfile.TemporaryDirectory() as tmp:
328 crate_root = Path(tmp) / "demo"
329 source = crate_root / "src"
330 write_tree(crate_root, {
331 "Cargo.toml": '[package]\nname="demo"\nversion="0.1.0"\n[lib]\npath="front/entry.rs"\n',
332 "src/lib.rs": '#[cfg(test)] mod suite { include!("../front/entry.rs"); }',
333 "front/entry.rs": 'include!("../src/entry_tests.rs");',
334 "src/entry_tests.rs": 'fn real() { crate::tui::one(); }',
335 })
336 crate = self.graph.load_crate("fixture", source)
337 exact, prefixes = self.graph.test_file_set(crate)
338 self.assertFalse(self.graph.file_is_test("entry_tests.rs", exact, prefixes, crate.production_files))
339 self.assertIn("entry_tests.rs", crate.production_files)
340
341 def test_scan_roots_do_not_consult_a_neighboring_crates_test_edges(self) -> None:
342 with tempfile.TemporaryDirectory() as tmp:
343 left = Path(tmp) / "left"
344 right = Path(tmp) / "right"
345 write_tree(left, {"lib.rs": 'mod client;', "client.rs": 'fn real() {}'})
346 write_tree(right, {"lib.rs": '#[cfg(test)] mod suite { include!("../left/client.rs"); }'})
347 crate = self.graph.load_crate("fixture", left)
348 exact, _prefixes = self.graph.test_file_set(crate)
349 self.assertNotIn("client.rs", exact)
350
351 def test_checked_in_baseline_holds(self) -> None:
352 self.assertEqual(self.graph.check(), [])
353
354
355 if __name__ == "__main__":
356 unittest.main()
357
357 lines PYTHON