"""Tests for configuration loading and validation.""" from pathlib import Path from typing import Any import pytest import yaml from pydantic import ValidationError from py_dvt_ate.app.config import ( APIConfig, AppConfig, ChamberConfig, DashboardConfig, DataConfig, DUTConfig, DUTParameters, InstrumentsConfig, LoggingConfig, PhysicsConfig, PyVISAConfig, SimulatorConfig, ThermalConfig, load_config, ) def test_default_config_values() -> None: """Test that default configuration values are correct.""" config = AppConfig() assert config.instruments.backend == "simulator" assert config.instruments.simulator.host == "localhost" assert config.instruments.simulator.thermal_chamber_port == 5001 assert config.physics.update_rate_hz == 100.0 assert config.physics.thermal.chamber_time_constant_s == 30.0 assert config.physics.thermal.theta_jc == 15.0 assert config.dut.model == "ldo" assert config.dut.parameters.nominal_output_voltage == 3.3 assert config.dut.parameters.tempco_ppm_per_c == 50.0 assert config.data.database_path == "./data/py_dvt_ate.db" assert config.logging.level == "INFO" assert config.dashboard.enabled is True assert config.api.enabled is False def test_load_config_with_defaults_only() -> None: """Test loading config without a file uses defaults.""" config = load_config(None) assert config.instruments.backend == "simulator" assert config.physics.update_rate_hz == 100.0 def test_load_config_from_file(tmp_path: Path) -> None: """Test loading configuration from YAML file.""" config_file = tmp_path / "test_config.yaml" config_data = { "instruments": {"backend": "pyvisa"}, "physics": {"update_rate_hz": 50.0}, "dut": {"model": "custom_ldo"}, } with config_file.open("w") as f: yaml.dump(config_data, f) config = load_config(config_file) assert config.instruments.backend == "pyvisa" assert config.physics.update_rate_hz == 50.0 assert config.dut.model == "custom_ldo" # Defaults still apply assert config.instruments.simulator.host == "localhost" def test_load_config_partial_override(tmp_path: Path) -> None: """Test that partial config overrides work correctly.""" config_file = tmp_path / "partial.yaml" config_data = { "physics": { "thermal": { "theta_jc": 20.0, # Other thermal params should use defaults } } } with config_file.open("w") as f: yaml.dump(config_data, f) config = load_config(config_file) # Overridden value assert config.physics.thermal.theta_jc == 20.0 # Default values assert config.physics.thermal.theta_ca == 5.0 assert config.physics.thermal.chamber_time_constant_s == 30.0 def test_load_config_missing_file() -> None: """Test that loading from missing file raises FileNotFoundError.""" with pytest.raises(FileNotFoundError, match="Configuration file not found"): load_config("nonexistent.yaml") def test_load_config_invalid_yaml(tmp_path: Path) -> None: """Test that malformed YAML raises an error.""" config_file = tmp_path / "invalid.yaml" config_file.write_text("invalid: yaml: content: [\n") with pytest.raises(yaml.YAMLError): load_config(config_file) def test_load_config_validation_error(tmp_path: Path) -> None: """Test that invalid configuration raises ValidationError.""" config_file = tmp_path / "invalid_config.yaml" config_data = { "instruments": {"backend": "invalid_backend"}, # Not in Literal["simulator", "pyvisa"] } with config_file.open("w") as f: yaml.dump(config_data, f) with pytest.raises(ValidationError): load_config(config_file) def test_env_override_simple(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """Test environment variable override for simple values.""" config_file = tmp_path / "config.yaml" config_data: dict[str, Any] = {} with config_file.open("w") as f: yaml.dump(config_data, f) monkeypatch.setenv("PYDVTATE__INSTRUMENTS__BACKEND", "pyvisa") monkeypatch.setenv("PYDVTATE__PHYSICS__UPDATE_RATE_HZ", "200.0") config = load_config(config_file) assert config.instruments.backend == "pyvisa" assert config.physics.update_rate_hz == 200.0 def test_env_override_nested(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """Test environment variable override for nested values.""" config_file = tmp_path / "config.yaml" config_data: dict[str, Any] = {} with config_file.open("w") as f: yaml.dump(config_data, f) monkeypatch.setenv("PYDVTATE__INSTRUMENTS__SIMULATOR__HOST", "192.168.1.100") monkeypatch.setenv("PYDVTATE__INSTRUMENTS__SIMULATOR__THERMAL_CHAMBER_PORT", "6001") monkeypatch.setenv("PYDVTATE__PHYSICS__THERMAL__THETA_JC", "25.0") config = load_config(config_file) assert config.instruments.simulator.host == "192.168.1.100" assert config.instruments.simulator.thermal_chamber_port == 6001 assert config.physics.thermal.theta_jc == 25.0 def test_env_override_types(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """Test that environment variables are parsed to correct types.""" config_file = tmp_path / "config.yaml" config_data: dict[str, Any] = {} with config_file.open("w") as f: yaml.dump(config_data, f) monkeypatch.setenv("PYDVTATE__DASHBOARD__ENABLED", "false") # bool monkeypatch.setenv("PYDVTATE__DASHBOARD__PORT", "9000") # int monkeypatch.setenv("PYDVTATE__PHYSICS__UPDATE_RATE_HZ", "75.5") # float config = load_config(config_file) assert config.dashboard.enabled is False assert config.dashboard.port == 9000 assert config.physics.update_rate_hz == 75.5 def test_env_override_precedence(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """Test that environment variables override file values.""" config_file = tmp_path / "config.yaml" config_data = {"physics": {"update_rate_hz": 50.0}} with config_file.open("w") as f: yaml.dump(config_data, f) monkeypatch.setenv("PYDVTATE__PHYSICS__UPDATE_RATE_HZ", "150.0") config = load_config(config_file) # Environment variable should win assert config.physics.update_rate_hz == 150.0 def test_env_variables_ignored_without_prefix( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Test that environment variables without prefix are ignored.""" config_file = tmp_path / "config.yaml" config_data: dict[str, Any] = {} with config_file.open("w") as f: yaml.dump(config_data, f) # These should be ignored monkeypatch.setenv("BACKEND", "pyvisa") monkeypatch.setenv("UPDATE_RATE_HZ", "200.0") config = load_config(config_file) # Should use defaults assert config.instruments.backend == "simulator" assert config.physics.update_rate_hz == 100.0 def test_simulator_config_defaults() -> None: """Test SimulatorConfig default values.""" config = SimulatorConfig() assert config.host == "localhost" assert config.thermal_chamber_port == 5001 assert config.power_supply_port == 5002 assert config.multimeter_port == 5003 def test_pyvisa_config_defaults() -> None: """Test PyVISAConfig default values.""" config = PyVISAConfig() assert config.thermal_chamber is None assert config.power_supply is None assert config.multimeter is None def test_complete_config_structure() -> None: """Test that all config sections can be instantiated.""" config = AppConfig( instruments=InstrumentsConfig( backend="pyvisa", simulator=SimulatorConfig(host="192.168.1.1"), pyvisa=PyVISAConfig(thermal_chamber="TCPIP::192.168.1.10::INSTR"), ), physics=PhysicsConfig( update_rate_hz=50.0, thermal=ThermalConfig(theta_jc=20.0), chamber=ChamberConfig(ramp_rate_c_per_min=5.0), ), dut=DUTConfig( model="custom", parameters=DUTParameters(nominal_output_voltage=5.0) ), data=DataConfig(database_path="/tmp/test.db"), logging=LoggingConfig(level="DEBUG"), dashboard=DashboardConfig(enabled=False), api=APIConfig(enabled=True, port=9000), ) assert config.instruments.backend == "pyvisa" assert config.physics.update_rate_hz == 50.0 assert config.dut.parameters.nominal_output_voltage == 5.0 assert config.api.port == 9000