Check if a single named test is fully cached (build + result). Performs the full pipeline: 1. Load cached test names from introspect cache 2. Normalize test name and find a match 3. Locate the test artifact (DLL/SO) 4. Verify ninja build can be skipped (no source changes
(test_name_raw: str, build_dir: Path)
| 355 | |
| 356 | |
| 357 | def check_single_test_cached(test_name_raw: str, build_dir: Path) -> bool: |
| 358 | """Check if a single named test is fully cached (build + result). |
| 359 | |
| 360 | Performs the full pipeline: |
| 361 | 1. Load cached test names from introspect cache |
| 362 | 2. Normalize test name and find a match |
| 363 | 3. Locate the test artifact (DLL/SO) |
| 364 | 4. Verify ninja build can be skipped (no source changes) |
| 365 | 5. Verify test result is cached and passing |
| 366 | |
| 367 | This is the shared logic used by both argv_ultra_early_exit() (pre-parse_args) |
| 368 | and the post-parse_args early exit in main(). |
| 369 | |
| 370 | Args: |
| 371 | test_name_raw: Raw test name string (e.g., "xypath", "tests/fl/alloca"). |
| 372 | build_dir: Meson build directory (e.g., .build/meson-quick/). |
| 373 | |
| 374 | Returns: |
| 375 | True if the test is fully cached and can be skipped, False otherwise. |
| 376 | """ |
| 377 | cached_names = load_test_names(build_dir) |
| 378 | if not cached_names: |
| 379 | return False |
| 380 | |
| 381 | # Normalize: strip "tests/" prefix and convert path separators to underscores |
| 382 | name_lower = test_name_raw.lower() |
| 383 | if name_lower.startswith("tests/") or name_lower.startswith("tests\\"): |
| 384 | name_lower = name_lower[len("tests/") :] |
| 385 | name_normalized = name_lower.replace("/", "_").replace("\\", "_") |
| 386 | stem = Path(test_name_raw).stem.lower() |
| 387 | |
| 388 | # Try multiple candidate names (exact, test_ prefixed, stem-only) |
| 389 | candidates = dict.fromkeys( |
| 390 | [name_normalized, f"test_{name_normalized}", stem, f"test_{stem}"] |
| 391 | ) |
| 392 | cache_set = set(cached_names) |
| 393 | matched: str | None = None |
| 394 | for cn in candidates: |
| 395 | if cn in cache_set: |
| 396 | matched = cn |
| 397 | break |
| 398 | if not matched: |
| 399 | # Suffix matching: "allocator" matches "fl_stl_allocator" etc. |
| 400 | # Only accept if exactly one cached name ends with _<query> to avoid ambiguity. |
| 401 | suffix = f"_{name_normalized}" |
| 402 | suffix_matches = [n for n in cached_names if n.endswith(suffix)] |
| 403 | if len(suffix_matches) == 1: |
| 404 | matched = suffix_matches[0] |
| 405 | if not matched: |
| 406 | return False |
| 407 | |
| 408 | ext = ".dll" if os.name == "nt" else ".so" |
| 409 | dll = build_dir / "tests" / f"{matched}{ext}" |
| 410 | if not dll.exists(): |
| 411 | return False |
| 412 | |
| 413 | # Determine the test's source directory to narrow the tests/ scan. |
| 414 | # For "tests/fl/alloca" → narrow to tests/fl/ (+ tests/ root for shared headers). |