Files
2026-07-13 13:26:28 +08:00

157 lines
4.9 KiB
Python

"""Test case study pipeline notebooks via Papermill parameter injection.
Each case study notebook runs independently against pre-generated intermediates
(labels, features, predictions, registries) stored in the test-data repo.
A failure in one notebook does NOT cascade to skip later notebooks.
Stages are auto-discovered: any [0-9][0-9]_*.py file in a case study
directory is treated as a pipeline stage.
Usage:
# All case studies
pytest tests/test_case_studies.py -v
# Specific case study
pytest tests/test_case_studies.py -v -k "etfs"
# Specific stage
pytest tests/test_case_studies.py -v -k "03_features"
"""
import re
from pathlib import Path
import pytest
from tests.pm_helpers import current_test_tier, get_overrides, get_tier, run_notebook
REPO_ROOT = Path(__file__).parent.parent
# All case studies
CASE_STUDIES = [
"etfs",
"crypto_perps_funding",
"nasdaq100_microstructure",
"sp500_equity_option_analytics",
"us_firm_characteristics",
"fx_pairs",
"cme_futures",
"sp500_options",
"us_equities_panel",
]
# Pattern for numbered pipeline stages — allows optional single-letter suffix
# (e.g., 10a_pca, 11b_ipca) for per-estimator notebook splits.
_STAGE_RE = re.compile(r"^\d{2}[a-z]?_")
def _collect_case_study_tests():
"""Collect all case study pipeline notebooks as (case_study, stage, path) tuples.
Auto-discovers files matching ^\\d{2}[a-z]?_ in each case study directory,
sorted numerically. Skips helper files (starting with _).
"""
tests = []
for cs in CASE_STUDIES:
cs_dir = REPO_ROOT / "case_studies" / cs
if not cs_dir.exists():
continue
for notebook in sorted(cs_dir.glob("[0-9][0-9]*.py")):
if notebook.name.startswith("_"):
continue
if not _STAGE_RE.match(notebook.name):
continue
stage = notebook.stem # e.g., "06_linear" or "11a_pca"
tests.append((cs, stage, notebook))
return tests
CASE_STUDY_TESTS = _collect_case_study_tests()
print(f"Found {len(CASE_STUDY_TESTS)} case study pipeline notebooks to test")
@pytest.mark.parametrize(
"case_study,stage,notebook_path",
CASE_STUDY_TESTS,
ids=lambda *args: None, # Custom IDs below
)
def test_case_study_pipeline(
case_study, stage, notebook_path, populated_data_dir, seeded_output_dir
):
"""Execute a case study pipeline stage via Papermill.
Each notebook runs independently — intermediates (labels, features,
predictions, registries) are pre-generated in the test-data repo.
"""
# Check case-study-level skip (e.g., "case_studies/nasdaq100_microstructure")
cs_key = f"case_studies/{case_study}"
cs_overrides = get_overrides(cs_key)
if cs_overrides.get("skip"):
pytest.skip(f"Skipped: {cs_overrides.get('skip_reason', 'case study skipped')}")
rel_path = notebook_path.relative_to(REPO_ROOT).with_suffix("")
overrides = get_overrides(str(rel_path))
# Tier routing: skip when NB tier doesn't match the current run tier.
nb_tier = get_tier(overrides)
run_tier = current_test_tier()
if nb_tier != run_tier:
pytest.skip(f"Tier {nb_tier} — current run tier is {run_tier}")
# Skip if overrides say so
if overrides.get("skip"):
reason = overrides.get("skip_reason", "marked skip in overrides")
pytest.skip(f"Skipped: {reason}")
# Check required imports (e.g., gensim, signatory, duckdb)
requires = overrides.get("requires_import")
if requires:
pkg = requires if isinstance(requires, str) else requires[0]
try:
__import__(pkg)
except ImportError:
pytest.skip(f"Requires {pkg} (not installed in this Docker image)")
# Check GPU requirement
if overrides.get("gpu"):
try:
import torch
if not torch.cuda.is_available():
pytest.skip("GPU required but not available")
except ImportError:
pytest.skip("GPU required but torch not installed")
timeout = overrides.get("timeout", 300)
parameters = overrides.get("parameters", {})
result = run_notebook(
py_path=notebook_path,
parameters=parameters,
timeout=timeout,
output_dir=seeded_output_dir,
data_dir=populated_data_dir,
)
if result["status"] == "error":
pytest.fail(
f"\n{'=' * 70}\n"
f"Pipeline failed: {case_study}::{stage}\n"
f"{'=' * 70}\n"
f"Error: {result['error']}\n"
f"{'=' * 70}\n"
)
# Custom test IDs
def pytest_collection_modifyitems(items):
"""Set readable test IDs for case study tests."""
for item in items:
if "test_case_study_pipeline" in item.name and hasattr(item, "callspec"):
cs = item.callspec.params.get("case_study", "")
stage = item.callspec.params.get("stage", "")
item._nodeid = f"{item.parent.nodeid}::{cs}::{stage}"