Add configuration tests
This commit is contained in:
266
tests/unit/test_config.py
Normal file
266
tests/unit/test_config.py
Normal 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
|
||||||
Reference in New Issue
Block a user