Add configuration tests

This commit is contained in:
2025-08-14 15:59:04 +00:00
parent fdf8c32395
commit bdc4fbff8f

266
tests/unit/test_config.py Normal file
View File

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