228 lines
7.4 KiB
Python
228 lines
7.4 KiB
Python
|
|
"""Tests for the layered configuration system."""
|
||
|
|
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
import yaml
|
||
|
|
|
||
|
|
from impakt.config import Config
|
||
|
|
from impakt.config.model import (
|
||
|
|
CriteriaConfig,
|
||
|
|
PlotConfig,
|
||
|
|
ProtocolConfig,
|
||
|
|
SessionConfig,
|
||
|
|
TransformConfig,
|
||
|
|
WebConfig,
|
||
|
|
_deep_merge,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class TestDeepMerge:
|
||
|
|
def test_simple_override(self):
|
||
|
|
base = {"a": 1, "b": 2}
|
||
|
|
override = {"b": 3, "c": 4}
|
||
|
|
result = _deep_merge(base, override)
|
||
|
|
assert result == {"a": 1, "b": 3, "c": 4}
|
||
|
|
|
||
|
|
def test_nested_merge(self):
|
||
|
|
base = {"plot": {"colors": ["red"], "width": 1}}
|
||
|
|
override = {"plot": {"width": 2}}
|
||
|
|
result = _deep_merge(base, override)
|
||
|
|
assert result["plot"]["colors"] == ["red"]
|
||
|
|
assert result["plot"]["width"] == 2
|
||
|
|
|
||
|
|
def test_list_replacement(self):
|
||
|
|
"""Lists are replaced entirely, not merged."""
|
||
|
|
base = {"items": [1, 2, 3]}
|
||
|
|
override = {"items": [4, 5]}
|
||
|
|
result = _deep_merge(base, override)
|
||
|
|
assert result["items"] == [4, 5]
|
||
|
|
|
||
|
|
def test_deep_nested(self):
|
||
|
|
base = {"a": {"b": {"c": 1, "d": 2}}}
|
||
|
|
override = {"a": {"b": {"c": 99}}}
|
||
|
|
result = _deep_merge(base, override)
|
||
|
|
assert result["a"]["b"]["c"] == 99
|
||
|
|
assert result["a"]["b"]["d"] == 2
|
||
|
|
|
||
|
|
|
||
|
|
class TestConfigLoad:
|
||
|
|
def test_from_defaults(self):
|
||
|
|
config = Config.from_defaults()
|
||
|
|
assert config.plot.line_width == 1.5
|
||
|
|
assert len(config.plot.colors) == 10
|
||
|
|
assert config.transforms.default_cfc is None
|
||
|
|
assert config.protocols.default == "euro_ncap"
|
||
|
|
|
||
|
|
def test_load_without_session(self):
|
||
|
|
config = Config.load()
|
||
|
|
assert isinstance(config.plot, PlotConfig)
|
||
|
|
assert isinstance(config.transforms, TransformConfig)
|
||
|
|
assert isinstance(config.criteria, CriteriaConfig)
|
||
|
|
assert isinstance(config.protocols, ProtocolConfig)
|
||
|
|
assert isinstance(config.session, SessionConfig)
|
||
|
|
assert isinstance(config.web, WebConfig)
|
||
|
|
|
||
|
|
def test_load_with_nonexistent_session(self, tmp_path):
|
||
|
|
config = Config.load(session_path=tmp_path / "nonexistent")
|
||
|
|
# Should still load package defaults
|
||
|
|
assert config.plot.line_width == 1.5
|
||
|
|
|
||
|
|
def test_session_override(self, tmp_path):
|
||
|
|
# Create a session config with overrides
|
||
|
|
session_dir = tmp_path / ".impakt"
|
||
|
|
session_dir.mkdir()
|
||
|
|
session_config = session_dir / "config.yaml"
|
||
|
|
session_config.write_text(
|
||
|
|
yaml.dump(
|
||
|
|
{
|
||
|
|
"plot": {"line_width": 3.0},
|
||
|
|
"transforms": {"default_cfc": 600},
|
||
|
|
}
|
||
|
|
),
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
|
||
|
|
config = Config.load(session_path=tmp_path)
|
||
|
|
assert config.plot.line_width == 3.0
|
||
|
|
assert config.transforms.default_cfc == 600
|
||
|
|
# Non-overridden values still come from defaults
|
||
|
|
assert len(config.plot.colors) == 10
|
||
|
|
|
||
|
|
def test_to_dict(self):
|
||
|
|
config = Config.from_defaults()
|
||
|
|
d = config.to_dict()
|
||
|
|
assert "plot" in d
|
||
|
|
assert "transforms" in d
|
||
|
|
assert "criteria" in d
|
||
|
|
assert "protocols" in d
|
||
|
|
assert d["plot"]["line_width"] == 1.5
|
||
|
|
|
||
|
|
def test_to_yaml(self):
|
||
|
|
config = Config.from_defaults()
|
||
|
|
yaml_str = config.to_yaml()
|
||
|
|
assert "line_width" in yaml_str
|
||
|
|
assert "default_cfc" in yaml_str
|
||
|
|
# Should be parseable
|
||
|
|
parsed = yaml.safe_load(yaml_str)
|
||
|
|
assert parsed["plot"]["line_width"] == 1.5
|
||
|
|
|
||
|
|
def test_repr(self):
|
||
|
|
config = Config.from_defaults()
|
||
|
|
r = repr(config)
|
||
|
|
assert "Config(" in r
|
||
|
|
assert "cfc=" in r
|
||
|
|
|
||
|
|
|
||
|
|
class TestConfigSave:
|
||
|
|
def test_save_session(self, tmp_path):
|
||
|
|
config = Config.from_defaults()
|
||
|
|
config.plot.line_width = 4.0
|
||
|
|
config.transforms.default_cfc = 1000
|
||
|
|
|
||
|
|
path = config.save_session(tmp_path)
|
||
|
|
assert path.exists()
|
||
|
|
assert (tmp_path / ".impakt" / "config.yaml").exists()
|
||
|
|
|
||
|
|
# Reload and verify
|
||
|
|
reloaded = Config.load(session_path=tmp_path)
|
||
|
|
assert reloaded.plot.line_width == 4.0
|
||
|
|
assert reloaded.transforms.default_cfc == 1000
|
||
|
|
|
||
|
|
def test_save_copies_protocols(self, tmp_path):
|
||
|
|
config = Config.from_defaults()
|
||
|
|
config.save_session(tmp_path)
|
||
|
|
|
||
|
|
proto_dir = tmp_path / ".impakt" / "protocols"
|
||
|
|
assert proto_dir.exists()
|
||
|
|
# Should have at least euro_ncap and iihs
|
||
|
|
yaml_files = list(proto_dir.glob("*.yaml"))
|
||
|
|
assert len(yaml_files) >= 2
|
||
|
|
|
||
|
|
def test_save_user(self, tmp_path, monkeypatch):
|
||
|
|
# Redirect user dir to tmp
|
||
|
|
import impakt.config.model as cm
|
||
|
|
|
||
|
|
monkeypatch.setattr(cm, "_USER_DIR", tmp_path)
|
||
|
|
monkeypatch.setattr(cm, "_USER_CONFIG", tmp_path / "config.yaml")
|
||
|
|
|
||
|
|
config = Config.from_defaults()
|
||
|
|
config.plot.line_width = 5.0
|
||
|
|
path = config.save_user()
|
||
|
|
assert path.exists()
|
||
|
|
|
||
|
|
# Read it back
|
||
|
|
text = path.read_text()
|
||
|
|
assert "5.0" in text
|
||
|
|
|
||
|
|
def test_init_user_dir(self, tmp_path, monkeypatch):
|
||
|
|
import impakt.config.model as cm
|
||
|
|
|
||
|
|
monkeypatch.setattr(cm, "_USER_DIR", tmp_path / "test_impakt")
|
||
|
|
monkeypatch.setattr(cm, "_USER_CONFIG", tmp_path / "test_impakt" / "config.yaml")
|
||
|
|
|
||
|
|
user_dir = Config.init_user_dir()
|
||
|
|
assert user_dir.exists()
|
||
|
|
assert (user_dir / "config.yaml").exists()
|
||
|
|
assert (user_dir / "templates").exists()
|
||
|
|
assert (user_dir / "corridors").exists()
|
||
|
|
assert (user_dir / "protocols").exists()
|
||
|
|
|
||
|
|
|
||
|
|
class TestConfigRoundTrip:
|
||
|
|
"""Verify that save -> load preserves all values."""
|
||
|
|
|
||
|
|
def test_full_round_trip(self, tmp_path):
|
||
|
|
# Create config with non-default values
|
||
|
|
config = Config.from_defaults()
|
||
|
|
config.plot.line_width = 2.5
|
||
|
|
config.plot.focus_color = "#ff0000"
|
||
|
|
config.transforms.default_cfc = 180
|
||
|
|
config.transforms.default_y_align = True
|
||
|
|
config.criteria.chest_deflection_max_peak_mm = 200.0
|
||
|
|
config.protocols.default = "iihs"
|
||
|
|
config.web.cursor_poll_interval_ms = 50
|
||
|
|
|
||
|
|
# Save
|
||
|
|
config.save_session(tmp_path)
|
||
|
|
|
||
|
|
# Reload
|
||
|
|
reloaded = Config.load(session_path=tmp_path)
|
||
|
|
assert reloaded.plot.line_width == 2.5
|
||
|
|
assert reloaded.plot.focus_color == "#ff0000"
|
||
|
|
assert reloaded.transforms.default_cfc == 180
|
||
|
|
assert reloaded.transforms.default_y_align is True
|
||
|
|
assert reloaded.criteria.chest_deflection_max_peak_mm == 200.0
|
||
|
|
assert reloaded.protocols.default == "iihs"
|
||
|
|
assert reloaded.web.cursor_poll_interval_ms == 50
|
||
|
|
|
||
|
|
|
||
|
|
class TestConfigInSession:
|
||
|
|
"""Verify Config integrates with Session."""
|
||
|
|
|
||
|
|
def test_session_has_config(self):
|
||
|
|
from impakt import Session
|
||
|
|
|
||
|
|
s = Session.open("tests/fixtures/sample_mme")
|
||
|
|
assert s.config is not None
|
||
|
|
assert s.config.plot.line_width == 1.5
|
||
|
|
|
||
|
|
def test_session_save_config(self, tmp_path):
|
||
|
|
import shutil
|
||
|
|
|
||
|
|
# Copy fixture to tmp so we can write .impakt/
|
||
|
|
test_dir = tmp_path / "test_session"
|
||
|
|
shutil.copytree("tests/fixtures/sample_mme", test_dir)
|
||
|
|
|
||
|
|
from impakt import Session
|
||
|
|
|
||
|
|
s = Session.open(test_dir)
|
||
|
|
s.config.transforms.default_cfc = 600
|
||
|
|
result = s.save_config()
|
||
|
|
assert result is not None
|
||
|
|
assert (test_dir / ".impakt" / "config.yaml").exists()
|
||
|
|
|
||
|
|
# Reopen and verify
|
||
|
|
s2 = Session.open(test_dir)
|
||
|
|
assert s2.config.transforms.default_cfc == 600
|