From 0615eb7e07c14d8abded8adbc672bf52f35a1484 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Thu, 14 Aug 2025 15:59:04 +0000 Subject: [PATCH] Add configuration tests --- tests/unit/test_config.py | 266 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 tests/unit/test_config.py diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py new file mode 100644 index 0000000..0a4fffe --- /dev/null +++ b/tests/unit/test_config.py @@ -0,0 +1,266 @@ +"""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