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