Newer
Older
TIASJudgeModel / tests / test_schema.py
"""設定スキーマのテスト(SPEC_01 基本方針 3・testing ルール必須対象 5)."""

from pathlib import Path

import pytest
from pydantic import ValidationError

from tias_judge_model.schema import DATA_ROOT_ENV_VAR, ExperimentConfig, load_config

REPO_ROOT = Path(__file__).resolve().parents[1]

VALID_YAML = """\
data:
  root: some/data
  manifest: manifest.csv
split:
  seed: 7
  val_ratio: 0.1
  test_ratio: 0.2
  n_folds: 5
"""


def _write(tmp_path: Path, text: str) -> Path:
    path = tmp_path / "config.yaml"
    path.write_text(text, encoding="utf-8")
    return path


def test_load_valid_config(tmp_path: Path) -> None:
    config = load_config(_write(tmp_path, VALID_YAML), environ={})
    assert isinstance(config, ExperimentConfig)
    assert config.data.root == Path("some/data")
    assert config.data.manifest_path == Path("some/data") / "manifest.csv"
    assert config.data.images_dir == Path("some/data") / "images"
    assert config.split.seed == 7
    assert config.split.val_ratio == pytest.approx(0.1)
    assert config.split.test_ratio == pytest.approx(0.2)
    assert config.split.n_folds == 5


def test_split_section_is_optional(tmp_path: Path) -> None:
    config = load_config(_write(tmp_path, "data:\n  root: d\n"), environ={})
    assert config.split.n_folds == 5  # SPEC_02: 結論を出すときは 5-fold
    assert config.split.val_ratio + config.split.test_ratio < 1.0


@pytest.mark.parametrize("name", ["synthetic.yaml", "newton.yaml"])
def test_repository_configs_are_valid(name: str) -> None:
    config = load_config(REPO_ROOT / "configs" / name, environ={})
    assert config.split.n_folds == 5


def test_env_var_overrides_yaml_data_root(tmp_path: Path) -> None:
    env_root = tmp_path / "from_env"
    config = load_config(_write(tmp_path, VALID_YAML), environ={DATA_ROOT_ENV_VAR: str(env_root)})
    assert DATA_ROOT_ENV_VAR == "TIAS_DATA_ROOT"
    assert config.data.root == env_root
    assert config.data.manifest_path == env_root / "manifest.csv"


def test_env_var_supplies_root_missing_from_yaml(tmp_path: Path) -> None:
    config = load_config(
        _write(tmp_path, "split:\n  seed: 1\n"), environ={DATA_ROOT_ENV_VAR: "env/root"}
    )
    assert config.data.root == Path("env/root")


def test_yaml_root_used_when_env_var_absent(tmp_path: Path) -> None:
    config = load_config(_write(tmp_path, VALID_YAML), environ={"OTHER": "x"})
    assert config.data.root == Path("some/data")


def test_os_environ_is_used_by_default(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv(DATA_ROOT_ENV_VAR, "from/os/environ")
    config = load_config(_write(tmp_path, VALID_YAML))
    assert config.data.root == Path("from/os/environ")


@pytest.mark.parametrize(
    "split_yaml",
    [
        "  val_ratio: 0.5\n  test_ratio: 0.5\n",  # 合計 1
        "  val_ratio: 0.6\n  test_ratio: 0.5\n",  # 合計 1 超
        "  val_ratio: -0.1\n",
        "  test_ratio: 1.0\n",
        "  n_folds: 1\n",
        "  n_folds: 0\n",
        "  seed: not_a_number\n",
        "  n_foldz: 5\n",  # 未知のキー(綴り間違い)
    ],
)
def test_invalid_split_is_rejected(tmp_path: Path, split_yaml: str) -> None:
    text = "data:\n  root: d\nsplit:\n" + split_yaml
    with pytest.raises(ValidationError):
        load_config(_write(tmp_path, text), environ={})


@pytest.mark.parametrize(
    "text",
    [
        "split:\n  seed: 1\n",  # data が無い
        "data:\n  manifest: m.csv\n",  # data.root が無い
        "data:\n  root: d\n  rooot: e\n",  # data の未知のキー
        "data:\n  root: d\nmodel: convnext\n",  # トップレベルの未知のキー
    ],
)
def test_missing_required_or_unknown_key_is_rejected(tmp_path: Path, text: str) -> None:
    with pytest.raises(ValidationError):
        load_config(_write(tmp_path, text), environ={})


def test_empty_yaml_is_rejected(tmp_path: Path) -> None:
    with pytest.raises(ValidationError):
        load_config(_write(tmp_path, ""), environ={})


def test_non_mapping_yaml_is_rejected(tmp_path: Path) -> None:
    with pytest.raises(ValueError):
        load_config(_write(tmp_path, "- a\n- b\n"), environ={})