| 1 | #!/usr/bin/env python3 |
| 2 | """Hermetic tests for the blocking-calls budget gate (#6149).""" |
| 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-blocking-calls-budget.py" |
| 14 | SPEC = importlib.util.spec_from_file_location("blocking_calls", 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 counts(source: str) -> dict[str, int]: |
| 22 | with tempfile.TemporaryDirectory() as tmp: |
| 23 | victim = Path(tmp) / "victim.rs" |
| 24 | victim.write_text(source, encoding="utf-8") |
| 25 | return mod.file_counts(victim) |
| 26 | |
| 27 | |
| 28 | class BlockingCallScopeTests(unittest.TestCase): |
| 29 | def test_sleep_in_plain_fn_counts(self) -> None: |
| 30 | self.assertEqual( |
| 31 | counts("fn wait() {\n std::thread::sleep(std::time::Duration::from_millis(1));\n}\n"), |
| 32 | {"thread_sleep": 1}, |
| 33 | ) |
| 34 | |
| 35 | def test_sleep_in_async_fn_counts(self) -> None: |
| 36 | self.assertEqual( |
| 37 | counts("async fn run() {\n std::thread::sleep(std::time::Duration::from_millis(1));\n}\n"), |
| 38 | {"thread_sleep": 1}, |
| 39 | ) |
| 40 | |
| 41 | def test_sleep_in_spawn_blocking_is_exempt(self) -> None: |
| 42 | self.assertEqual( |
| 43 | counts( |
| 44 | "async fn run() {\n" |
| 45 | " tokio::task::spawn_blocking(move || {\n" |
| 46 | " std::thread::sleep(std::time::Duration::from_millis(1));\n" |
| 47 | " });\n" |
| 48 | "}\n" |
| 49 | ), |
| 50 | {}, |
| 51 | ) |
| 52 | |
| 53 | def test_sleep_in_dedicated_thread_is_exempt(self) -> None: |
| 54 | self.assertEqual( |
| 55 | counts( |
| 56 | "fn pump() {\n" |
| 57 | " std::thread::Builder::new().spawn(move || {\n" |
| 58 | " std::thread::sleep(std::time::Duration::from_millis(5));\n" |
| 59 | " });\n" |
| 60 | "}\n" |
| 61 | ), |
| 62 | {}, |
| 63 | ) |
| 64 | |
| 65 | def test_sleep_in_tests_mod_is_exempt(self) -> None: |
| 66 | self.assertEqual( |
| 67 | counts( |
| 68 | "fn prod() {}\n" |
| 69 | "#[cfg(test)]\n" |
| 70 | "mod tests {\n" |
| 71 | " fn probe() { std::thread::sleep(std::time::Duration::from_millis(1)); }\n" |
| 72 | "}\n" |
| 73 | ), |
| 74 | {}, |
| 75 | ) |
| 76 | |
| 77 | def test_sleep_in_cfg_test_fn_is_exempt(self) -> None: |
| 78 | self.assertEqual( |
| 79 | counts( |
| 80 | "#[cfg(test)]\n" |
| 81 | "fn helper() { std::thread::sleep(std::time::Duration::from_millis(1)); }\n" |
| 82 | ), |
| 83 | {}, |
| 84 | ) |
| 85 | |
| 86 | def test_std_fs_call_counts(self) -> None: |
| 87 | self.assertEqual( |
| 88 | counts("async fn go() {\n let _ = std::fs::read_to_string(p).unwrap();\n}\n"), |
| 89 | {"std_fs": 1}, |
| 90 | ) |
| 91 | |
| 92 | def test_std_fs_imports_and_signature_types_do_not_count(self) -> None: |
| 93 | self.assertEqual( |
| 94 | counts( |
| 95 | "use std::fs::File;\n" |
| 96 | "use std::fs::OpenOptions;\n" |
| 97 | "use std::fs::DirBuilder;\n" |
| 98 | "type Handle = std::fs::File;\n" |
| 99 | "fn types(_: &std::fs::File, _: std::fs::OpenOptions, " |
| 100 | "_: std::fs::DirBuilder) -> Option<std::fs::File> { None }\n" |
| 101 | ), |
| 102 | {}, |
| 103 | ) |
| 104 | |
| 105 | def test_std_fs_qualified_member_operations_count(self) -> None: |
| 106 | for operation in ( |
| 107 | "std::fs::File::open(path)", |
| 108 | "std::fs::File::create(path)", |
| 109 | "std::fs::File::options()", |
| 110 | "std::fs::OpenOptions::new()", |
| 111 | "std::fs::DirBuilder::new()", |
| 112 | "std::fs::File :: open(path)", |
| 113 | ): |
| 114 | with self.subTest(operation=operation): |
| 115 | self.assertEqual( |
| 116 | counts(f"async fn run() {{ let _ = {operation}; }}\n"), |
| 117 | {"std_fs": 1}, |
| 118 | ) |
| 119 | |
| 120 | def test_std_fs_qualified_member_in_spawn_blocking_is_exempt(self) -> None: |
| 121 | self.assertEqual( |
| 122 | counts( |
| 123 | "async fn run() {\n" |
| 124 | " tokio::task::spawn_blocking(move || {\n" |
| 125 | " let _ = std::fs::File::open(path);\n" |
| 126 | " });\n" |
| 127 | "}\n" |
| 128 | ), |
| 129 | {}, |
| 130 | ) |
| 131 | |
| 132 | def test_comment_and_string_literals_do_not_count(self) -> None: |
| 133 | self.assertEqual( |
| 134 | counts( |
| 135 | "fn doc() {\n" |
| 136 | " // std::thread::sleep(std::time::Duration::from_millis(1));\n" |
| 137 | ' let s = "std::fs::read_to_string(p)";\n' |
| 138 | " let t = r#\"std::fs::write(a, b)\"#;\n" |
| 139 | "}\n" |
| 140 | ), |
| 141 | {}, |
| 142 | ) |
| 143 | |
| 144 | def test_tokio_equivalents_do_not_count(self) -> None: |
| 145 | self.assertEqual( |
| 146 | counts( |
| 147 | "async fn go() {\n" |
| 148 | " tokio::time::sleep(std::time::Duration::from_millis(1)).await;\n" |
| 149 | " let _ = tokio::fs::read_to_string(p).await;\n" |
| 150 | "}\n" |
| 151 | ), |
| 152 | {}, |
| 153 | ) |
| 154 | |
| 155 | |
| 156 | class CfgTestModuleExclusion(unittest.TestCase): |
| 157 | """A file that is wholly a `#[cfg(test)]` module is test code (#6149). |
| 158 | |
| 159 | The per-file scanner only sees test scope declared *inside* a file, so an |
| 160 | extracted test suite looked like brand-new unprotected call sites even |
| 161 | though nothing moved onto an async path. PR #6096's |
| 162 | `session_export_*_tests.rs` reddened `main` this way. |
| 163 | """ |
| 164 | |
| 165 | def _crates(self, tmp: Path, files: dict[str, str]) -> Path: |
| 166 | crates = tmp / "crates" / "demo" / "src" |
| 167 | crates.mkdir(parents=True) |
| 168 | for name, body in files.items(): |
| 169 | target = crates / name |
| 170 | target.parent.mkdir(parents=True, exist_ok=True) |
| 171 | target.write_text(body, encoding="utf-8") |
| 172 | return tmp / "crates" |
| 173 | |
| 174 | def test_whole_file_cfg_test_module_is_excluded(self) -> None: |
| 175 | with tempfile.TemporaryDirectory() as tmp: |
| 176 | root = Path(tmp) |
| 177 | crates = self._crates( |
| 178 | root, |
| 179 | { |
| 180 | "lib.rs": "#[cfg(test)]\nmod suite;\n", |
| 181 | "suite.rs": "fn helper() { let _ = std::fs::read_to_string(p); }\n", |
| 182 | }, |
| 183 | ) |
| 184 | original = mod.CRATES |
| 185 | try: |
| 186 | mod.CRATES = crates |
| 187 | excluded = mod.cfg_test_module_files() |
| 188 | finally: |
| 189 | mod.CRATES = original |
| 190 | self.assertIn((crates / "demo" / "src" / "suite.rs").resolve(), excluded) |
| 191 | |
| 192 | def test_plain_mod_declaration_is_not_excluded(self) -> None: |
| 193 | with tempfile.TemporaryDirectory() as tmp: |
| 194 | root = Path(tmp) |
| 195 | crates = self._crates( |
| 196 | root, |
| 197 | { |
| 198 | "lib.rs": "mod production;\n", |
| 199 | "production.rs": "fn helper() { let _ = std::fs::read_to_string(p); }\n", |
| 200 | }, |
| 201 | ) |
| 202 | original = mod.CRATES |
| 203 | try: |
| 204 | mod.CRATES = crates |
| 205 | excluded = mod.cfg_test_module_files() |
| 206 | finally: |
| 207 | mod.CRATES = original |
| 208 | self.assertNotIn( |
| 209 | (crates / "demo" / "src" / "production.rs").resolve(), excluded |
| 210 | ) |
| 211 | |
| 212 | def _excluded(self, root: Path, files: dict[str, str]) -> tuple[Path, set[Path]]: |
| 213 | crates = self._crates(root, files) |
| 214 | original = mod.CRATES |
| 215 | try: |
| 216 | mod.CRATES = crates |
| 217 | return crates / "demo" / "src", mod.cfg_test_module_files() |
| 218 | finally: |
| 219 | mod.CRATES = original |
| 220 | |
| 221 | def test_inline_cfg_test_item_include_is_excluded(self) -> None: |
| 222 | with tempfile.TemporaryDirectory() as tmp: |
| 223 | source, excluded = self._excluded(Path(tmp), { |
| 224 | "lib.rs": '#[cfg(test)] mod verification { include!("part.rs"); }', |
| 225 | "part.rs": 'fn helper() { std::fs::read("fixture"); }', |
| 226 | }) |
| 227 | self.assertIn((source / "part.rs").resolve(), excluded) |
| 228 | |
| 229 | def test_path_module_and_recursive_includes_inherit_test_only_scope(self) -> None: |
| 230 | with tempfile.TemporaryDirectory() as tmp: |
| 231 | source, excluded = self._excluded(Path(tmp), { |
| 232 | "lib.rs": '#[cfg(test)] #[path = "suite.rs"] mod verification;', |
| 233 | "suite.rs": 'include!("pieces/first.rs");', |
| 234 | "pieces/first.rs": 'include!(r"second.rs");', |
| 235 | "pieces/second.rs": 'fn helper() { std::fs::read("fixture"); }', |
| 236 | }) |
| 237 | for name in ("suite.rs", "pieces/first.rs", "pieces/second.rs"): |
| 238 | self.assertIn((source / name).resolve(), excluded) |
| 239 | |
| 240 | def test_test_looking_plain_module_and_filename_are_not_exempt(self) -> None: |
| 241 | with tempfile.TemporaryDirectory() as tmp: |
| 242 | source, excluded = self._excluded(Path(tmp), { |
| 243 | "lib.rs": 'mod tests { include!("test_cases.rs"); }', |
| 244 | "test_cases.rs": 'fn helper() { std::fs::read("fixture"); }', |
| 245 | }) |
| 246 | self.assertNotIn((source / "test_cases.rs").resolve(), excluded) |
| 247 | |
| 248 | def test_comments_and_string_include_or_cfg_lookalikes_are_not_exempt(self) -> None: |
| 249 | with tempfile.TemporaryDirectory() as tmp: |
| 250 | source, excluded = self._excluded(Path(tmp), { |
| 251 | "lib.rs": '''// #[cfg(test)] mod commented; |
| 252 | /* #[cfg(test)] mod blocked { include!("part.rs"); } */ |
| 253 | const A: &str = "#[cfg(test)] mod stringed; include!(\\\"part.rs\\\");"; |
| 254 | const B: &str = r#"#[cfg(test)] mod raw; include!("part.rs");"#; |
| 255 | const C: &str = r"backslash \\"; |
| 256 | include!("production.rs"); |
| 257 | ''', |
| 258 | "commented.rs": '', "stringed.rs": '', "raw.rs": '', |
| 259 | "part.rs": '', "production.rs": '', |
| 260 | }) |
| 261 | self.assertFalse(excluded) |
| 262 | self.assertNotIn((source / "production.rs").resolve(), excluded) |
| 263 | |
| 264 | def test_production_include_blocks_shared_fragment_and_descendants(self) -> None: |
| 265 | with tempfile.TemporaryDirectory() as tmp: |
| 266 | source, excluded = self._excluded(Path(tmp), { |
| 267 | "lib.rs": 'include!("shared.rs"); #[cfg(test)] mod suite { include!("shared.rs"); }', |
| 268 | "shared.rs": 'include!("child.rs");', |
| 269 | "child.rs": 'fn helper() { std::fs::read("fixture"); }', |
| 270 | }) |
| 271 | for name in ("shared.rs", "child.rs"): |
| 272 | self.assertNotIn((source / name).resolve(), excluded) |
| 273 | |
| 274 | def test_production_path_module_blocks_test_alias(self) -> None: |
| 275 | with tempfile.TemporaryDirectory() as tmp: |
| 276 | source, excluded = self._excluded(Path(tmp), { |
| 277 | "lib.rs": '#[path="shared.rs"] mod production; #[cfg(test)] #[path="shared.rs"] mod suite;', |
| 278 | "shared.rs": 'include!("child.rs");', |
| 279 | "child.rs": 'fn helper() { std::fs::read("fixture"); }', |
| 280 | }) |
| 281 | self.assertNotIn((source / "shared.rs").resolve(), excluded) |
| 282 | self.assertNotIn((source / "child.rs").resolve(), excluded) |
| 283 | |
| 284 | def test_cfg_test_function_include_is_not_a_module_exemption(self) -> None: |
| 285 | with tempfile.TemporaryDirectory() as tmp: |
| 286 | source, excluded = self._excluded(Path(tmp), { |
| 287 | "lib.rs": '#[cfg(test)] fn probe() { include!("part.rs"); }', |
| 288 | "part.rs": 'fn helper() { std::fs::read("fixture"); }', |
| 289 | }) |
| 290 | self.assertNotIn((source / "part.rs").resolve(), excluded) |
| 291 | |
| 292 | def test_dynamic_include_and_mixed_cfg_are_not_inferred_as_test_only(self) -> None: |
| 293 | with tempfile.TemporaryDirectory() as tmp: |
| 294 | source, excluded = self._excluded(Path(tmp), { |
| 295 | "lib.rs": '#[cfg(test)] mod suite { include!(concat!("part", ".rs")); }\n#[cfg(any(test, feature="shipping"))] mod shipped { include!("shipping.rs"); }', |
| 296 | "part.rs": '', "shipping.rs": '', |
| 297 | }) |
| 298 | self.assertFalse(excluded) |
| 299 | self.assertNotIn((source / "part.rs").resolve(), excluded) |
| 300 | |
| 301 | def test_default_crate_roots_and_production_children_stay_counted_when_included_by_tests(self) -> None: |
| 302 | for entry in ("lib.rs", "main.rs"): |
| 303 | with self.subTest(entry=entry), tempfile.TemporaryDirectory() as tmp: |
| 304 | source, excluded = self._excluded(Path(tmp), { |
| 305 | "lib.rs": '#[cfg(test)] mod suite { include!("' + entry + '"); }', |
| 306 | entry: 'mod production; #[cfg(test)] mod suite { include!("' + entry + '"); }', |
| 307 | "production.rs": 'fn run() { std::fs::read("real"); }', |
| 308 | }) |
| 309 | self.assertNotIn((source / entry).resolve(), excluded) |
| 310 | self.assertNotIn((source / "production.rs").resolve(), excluded) |
| 311 | |
| 312 | def test_cargo_custom_lib_root_keeps_unguarded_module_and_include_descendants_counted(self) -> None: |
| 313 | with tempfile.TemporaryDirectory() as tmp: |
| 314 | source, excluded = self._excluded(Path(tmp), { |
| 315 | "../Cargo.toml": '[package]\nname="demo"\nversion="0.1.0"\n[lib]\npath="src/custom.rs"\n', |
| 316 | "lib.rs": '#[cfg(test)] mod suite { include!("custom.rs"); }', |
| 317 | "custom.rs": 'mod production; include!("fragment.rs");', |
| 318 | "custom/production.rs": 'fn run() { std::fs::read("real"); }', |
| 319 | "fragment.rs": 'fn run() { std::thread::sleep(delay); }', |
| 320 | }) |
| 321 | for name in ("custom.rs", "custom/production.rs", "fragment.rs"): |
| 322 | self.assertNotIn((source / name).resolve(), excluded) |
| 323 | |
| 324 | def test_cargo_custom_binary_root_outside_src_stays_counted_when_test_reachable(self) -> None: |
| 325 | with tempfile.TemporaryDirectory() as tmp: |
| 326 | source, excluded = self._excluded(Path(tmp), { |
| 327 | "../Cargo.toml": '[package]\nname="demo"\nversion="0.1.0"\n[[bin]]\nname="launcher"\npath="front/launch.rs"\n', |
| 328 | "lib.rs": '#[cfg(test)] mod suite { include!("../front/launch.rs"); }', |
| 329 | "../front/launch.rs": 'include!("production.rs");', |
| 330 | "../front/production.rs": 'fn run() { std::fs::read("real"); }', |
| 331 | }) |
| 332 | for name in ("../front/launch.rs", "../front/production.rs"): |
| 333 | self.assertNotIn((source / name).resolve(), excluded) |
| 334 | |
| 335 | def test_automatic_binary_roots_remain_counted(self) -> None: |
| 336 | with tempfile.TemporaryDirectory() as tmp: |
| 337 | source, excluded = self._excluded(Path(tmp), { |
| 338 | "lib.rs": '#[cfg(test)] mod suite { include!("bin/one.rs"); include!("bin/two/main.rs"); }', |
| 339 | "bin/one.rs": 'include!("child.rs");', |
| 340 | "bin/two/main.rs": 'include!("../child.rs");', |
| 341 | "bin/child.rs": 'fn run() { std::fs::read("real"); }', |
| 342 | }) |
| 343 | for name in ("bin/one.rs", "bin/two/main.rs", "bin/child.rs"): |
| 344 | self.assertNotIn((source / name).resolve(), excluded) |
| 345 | |
| 346 | def test_recursive_test_include_cycle_terminates(self) -> None: |
| 347 | with tempfile.TemporaryDirectory() as tmp: |
| 348 | source, excluded = self._excluded(Path(tmp), { |
| 349 | "lib.rs": '#[cfg(test)] mod suite { include!("first.rs"); }', |
| 350 | "first.rs": 'include!("second.rs");', |
| 351 | "second.rs": 'include!("first.rs");', |
| 352 | }) |
| 353 | self.assertEqual(excluded, {(source / name).resolve() for name in ("first.rs", "second.rs")}) |
| 354 | |
| 355 | |
| 356 | class ExplicitModuleGraphScopeTests(unittest.TestCase): |
| 357 | _crates = CfgTestModuleExclusion._crates |
| 358 | |
| 359 | def test_cargo_integration_target_and_declared_helpers_are_test_only(self) -> None: |
| 360 | with tempfile.TemporaryDirectory() as tmp: |
| 361 | crates = self._crates(Path(tmp), { |
| 362 | "../Cargo.toml": '[package]\nname="demo"\nversion="0.1.0"\n', |
| 363 | "lib.rs": 'fn shipping() {}', |
| 364 | "../tests/scenario.rs": 'mod support; fn probe() { std::fs::read("fixture"); }', |
| 365 | "../tests/support/mod.rs": 'fn helper() { std::fs::read("fixture"); }', |
| 366 | }) |
| 367 | package = crates / "demo" |
| 368 | test_only, _ = mod.module_file_scopes(package) |
| 369 | self.assertIn((package / "tests/scenario.rs").resolve(), test_only) |
| 370 | self.assertIn((package / "tests/support/mod.rs").resolve(), test_only) |
| 371 | self.assertNotIn((package / "src/lib.rs").resolve(), test_only) |
| 372 | |
| 373 | def test_disabled_automatic_tests_do_not_exempt_an_unclaimed_path(self) -> None: |
| 374 | with tempfile.TemporaryDirectory() as tmp: |
| 375 | crates = self._crates(Path(tmp), { |
| 376 | "../Cargo.toml": '[package]\nname="demo"\nversion="0.1.0"\nautotests=false\n', |
| 377 | "lib.rs": 'fn shipping() {}', |
| 378 | "../tests/scenario.rs": 'fn run() { std::fs::read("real"); }', |
| 379 | }) |
| 380 | package = crates / "demo" |
| 381 | self.assertNotIn((package / "tests/scenario.rs").resolve(), mod.cfg_test_module_files(package)) |
| 382 | |
| 383 | def test_explicit_test_target_never_exempts_a_shipping_binary(self) -> None: |
| 384 | with tempfile.TemporaryDirectory() as tmp: |
| 385 | crates = self._crates(Path(tmp), { |
| 386 | "../Cargo.toml": '[package]\nname="demo"\nversion="0.1.0"\n[[bin]]\nname="launch"\npath="front/launch.rs"\n[[test]]\nname="probe"\npath="front/launch.rs"\n', |
| 387 | "lib.rs": 'fn shipping() {}', |
| 388 | "../front/launch.rs": 'include!("child.rs");', |
| 389 | "../front/child.rs": 'fn run() { std::fs::read("real"); }', |
| 390 | }) |
| 391 | package = crates / "demo" |
| 392 | test_only, production = mod.module_file_scopes(package) |
| 393 | for name in ("front/launch.rs", "front/child.rs"): |
| 394 | self.assertNotIn((package / name).resolve(), test_only) |
| 395 | self.assertIn((package / name).resolve(), production) |
| 396 | |
| 397 | def test_explicit_test_path_overrides_automatic_name_without_exempting_both(self) -> None: |
| 398 | with tempfile.TemporaryDirectory() as tmp: |
| 399 | crates = self._crates(Path(tmp), { |
| 400 | "../Cargo.toml": '[package]\nname="demo"\nversion="0.1.0"\n[[test]]\nname="scenario"\npath="probes/actual.rs"\n', |
| 401 | "lib.rs": 'fn shipping() {}', |
| 402 | "../tests/scenario.rs": 'fn unclaimed() {}', |
| 403 | "../probes/actual.rs": 'fn actual() {}', |
| 404 | }) |
| 405 | package = crates / "demo" |
| 406 | test_only, _ = mod.module_file_scopes(package) |
| 407 | self.assertIn((package / "probes/actual.rs").resolve(), test_only) |
| 408 | self.assertNotIn((package / "tests/scenario.rs").resolve(), test_only) |
| 409 | |
| 410 | def test_explicit_source_root_does_not_mutate_or_consult_global_crates(self) -> None: |
| 411 | with tempfile.TemporaryDirectory() as tmp: |
| 412 | source = self._crates(Path(tmp), { |
| 413 | "lib.rs": '#[cfg(test)] mod suite { include!("piece.rs"); }', |
| 414 | "piece.rs": 'fn probe() {}', |
| 415 | }) / "demo" / "src" |
| 416 | original = mod.CRATES |
| 417 | self.assertEqual(mod.cfg_test_module_files(source), {(source / "piece.rs").resolve()}) |
| 418 | self.assertEqual(mod.CRATES, original) |
| 419 | |
| 420 | def test_production_conflicts_include_descendants_and_override_test_names(self) -> None: |
| 421 | with tempfile.TemporaryDirectory() as tmp: |
| 422 | source = self._crates(Path(tmp), { |
| 423 | "lib.rs": 'include!("shared_tests.rs"); #[cfg(test)] mod suite { include!("shared_tests.rs"); }', |
| 424 | "shared_tests.rs": 'include!("child_test.rs");', |
| 425 | "child_test.rs": 'fn run() {}', |
| 426 | }) / "demo" / "src" |
| 427 | test_only, production = mod.module_file_scopes(source) |
| 428 | self.assertEqual(test_only, set()) |
| 429 | for name in ("lib.rs", "shared_tests.rs", "child_test.rs"): |
| 430 | self.assertIn((source / name).resolve(), production) |
| 431 | |
| 432 | def test_custom_root_outside_source_preserves_production_descendants(self) -> None: |
| 433 | with tempfile.TemporaryDirectory() as tmp: |
| 434 | crates = self._crates(Path(tmp), { |
| 435 | "../Cargo.toml": '[package]\nname="demo"\nversion="0.1.0"\n[lib]\npath="entry.rs"\n', |
| 436 | "lib.rs": '#[cfg(test)] mod suite { include!("../entry.rs"); }', |
| 437 | "../entry.rs": 'include!("src/child_tests.rs");', |
| 438 | "child_tests.rs": 'fn run() {}', |
| 439 | }) |
| 440 | source = crates / "demo" / "src" |
| 441 | test_only, production = mod.module_file_scopes(source.parent) |
| 442 | self.assertNotIn((source / "child_tests.rs").resolve(), test_only) |
| 443 | self.assertIn((source / "child_tests.rs").resolve(), production) |
| 444 | |
| 445 | |
| 446 | if __name__ == "__main__": |
| 447 | unittest.main() |
| 448 |