"""設定スキーマのテスト(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={})