"""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