152 lines
5.4 KiB
Python
152 lines
5.4 KiB
Python
"""Tests for utils/paths.py — chapter/case-study path resolution.
|
|
|
|
Pins the redirection semantics of ML4T_OUTPUT_DIR (test isolation env var)
|
|
and the input validation on the chapter/strategy registries. The redirection
|
|
behavior is load-bearing: every case-study pipeline notebook depends on it
|
|
to avoid overwriting production artifacts during tests.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from utils.paths import (
|
|
CHAPTERS,
|
|
REPO_ROOT,
|
|
STRATEGY_IDS,
|
|
get_case_study_dir,
|
|
get_chapter_dir,
|
|
get_output_dir,
|
|
)
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Registry invariants
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
def test_chapters_registry_covers_1_through_27() -> None:
|
|
"""The book ships 27 chapters; the registry must enumerate all of them."""
|
|
assert set(CHAPTERS.keys()) == set(range(1, 28))
|
|
|
|
|
|
def test_chapter_directory_names_are_prefixed_by_number() -> None:
|
|
"""Notebook discovery in tests/pm_helpers relies on the NN_ prefix pattern."""
|
|
for n, dirname in CHAPTERS.items():
|
|
assert dirname.startswith(f"{n:02d}_"), f"chapter {n} dir {dirname!r} missing prefix"
|
|
|
|
|
|
def test_strategy_ids_match_case_studies_dir() -> None:
|
|
"""STRATEGY_IDS should mirror case_studies/<id>/ on disk (structure invariant)."""
|
|
cs_root = REPO_ROOT / "case_studies"
|
|
on_disk = {
|
|
p.name
|
|
for p in cs_root.iterdir()
|
|
if p.is_dir() and not p.name.startswith(("_", ".")) and p.name not in {"utils", "config"}
|
|
}
|
|
# STRATEGY_IDS is the enforced registry; on_disk may contain extras (ignored_subdirs)
|
|
# but every declared id must exist on disk.
|
|
missing = STRATEGY_IDS - on_disk
|
|
assert not missing, f"STRATEGY_IDS declare non-existent case studies: {missing}"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# get_chapter_dir
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
def test_get_chapter_dir_returns_absolute_path() -> None:
|
|
path = get_chapter_dir(7)
|
|
assert path.is_absolute()
|
|
assert path.name == "07_defining_the_learning_task"
|
|
|
|
|
|
def test_get_chapter_dir_rejects_out_of_range() -> None:
|
|
with pytest.raises(ValueError, match="Invalid chapter"):
|
|
get_chapter_dir(99)
|
|
|
|
|
|
def test_get_chapter_dir_rejects_zero() -> None:
|
|
with pytest.raises(ValueError, match="Invalid chapter"):
|
|
get_chapter_dir(0)
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# get_output_dir — test-mode redirection
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
def test_get_output_dir_production_path(tmp_path, monkeypatch) -> None:
|
|
"""No env var → writes under the chapter dir."""
|
|
monkeypatch.delenv("ML4T_OUTPUT_DIR", raising=False)
|
|
monkeypatch.delenv("ML4T_CHAPTER_OUTPUT_DIR", raising=False)
|
|
|
|
# Use create=False to avoid making a directory in the real repo.
|
|
path = get_output_dir(7, "etfs", create=False)
|
|
assert path == get_chapter_dir(7) / "output" / "etfs"
|
|
|
|
|
|
def test_get_output_dir_redirects_under_ml4t_output_dir(tmp_path, monkeypatch) -> None:
|
|
monkeypatch.setenv("ML4T_OUTPUT_DIR", str(tmp_path))
|
|
monkeypatch.delenv("ML4T_CHAPTER_OUTPUT_DIR", raising=False)
|
|
|
|
path = get_output_dir(7, "etfs")
|
|
assert path == tmp_path / "ch07_etfs"
|
|
assert path.exists()
|
|
|
|
|
|
def test_get_output_dir_chapter_specific_env_wins_over_global(tmp_path, monkeypatch) -> None:
|
|
"""ML4T_CHAPTER_OUTPUT_DIR should take precedence over ML4T_OUTPUT_DIR."""
|
|
ch_dir = tmp_path / "chapter-only"
|
|
global_dir = tmp_path / "global"
|
|
monkeypatch.setenv("ML4T_CHAPTER_OUTPUT_DIR", str(ch_dir))
|
|
monkeypatch.setenv("ML4T_OUTPUT_DIR", str(global_dir))
|
|
|
|
path = get_output_dir(11, "etfs")
|
|
assert path.parent == ch_dir
|
|
assert not str(path).startswith(str(global_dir))
|
|
|
|
|
|
def test_get_output_dir_zero_pads_chapter_number(tmp_path, monkeypatch) -> None:
|
|
monkeypatch.setenv("ML4T_OUTPUT_DIR", str(tmp_path))
|
|
monkeypatch.delenv("ML4T_CHAPTER_OUTPUT_DIR", raising=False)
|
|
|
|
assert get_output_dir(3, "x").name == "ch03_x"
|
|
assert get_output_dir(11, "x").name == "ch11_x"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# get_case_study_dir — test-mode redirection
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
def test_get_case_study_dir_production_path(monkeypatch) -> None:
|
|
"""No env var → writes under case_studies/{id}/."""
|
|
monkeypatch.delenv("ML4T_OUTPUT_DIR", raising=False)
|
|
|
|
path = get_case_study_dir("etfs", create=False)
|
|
assert path == REPO_ROOT / "case_studies" / "etfs"
|
|
|
|
|
|
def test_get_case_study_dir_redirects_under_ml4t_output_dir(tmp_path, monkeypatch) -> None:
|
|
monkeypatch.setenv("ML4T_OUTPUT_DIR", str(tmp_path))
|
|
|
|
path = get_case_study_dir("etfs")
|
|
assert path == tmp_path / "etfs"
|
|
assert path.exists()
|
|
|
|
|
|
def test_get_case_study_dir_create_false_does_not_mkdir(tmp_path, monkeypatch) -> None:
|
|
monkeypatch.setenv("ML4T_OUTPUT_DIR", str(tmp_path))
|
|
|
|
path = get_case_study_dir("etfs", create=False)
|
|
assert not path.exists()
|
|
|
|
|
|
def test_get_case_study_dir_create_true_is_idempotent(tmp_path, monkeypatch) -> None:
|
|
monkeypatch.setenv("ML4T_OUTPUT_DIR", str(tmp_path))
|
|
|
|
first = get_case_study_dir("etfs", create=True)
|
|
second = get_case_study_dir("etfs", create=True)
|
|
assert first == second
|
|
assert first.exists()
|