Files
py-dvt-ate/tests/unit/test_instruments.py
Kai Chappell 80baab6c94 Fix CI errors: linting, type checking, and tests
- Fix import sorting in test_instruments.py (ruff I001)
- Install pandas-stubs for mypy type checking
- Add garbage collection cleanup to repository test fixtures
- Prevent Windows file locking errors in tempfile cleanup

All CI checks now passing: lint, type check, and all 244 tests.
2025-09-29 18:02:39 +00:00

274 lines
9.8 KiB
Python

"""Unit tests for instrument interfaces and factory.
Tests the Hardware Abstraction Layer (HAL) interfaces and the factory
pattern for creating instrument sets.
"""
import pytest
from py_dvt_ate.instruments import (
IMultimeter,
InstrumentConfig,
InstrumentFactory,
InstrumentSet,
IPowerSupply,
IThermalChamber,
)
from py_dvt_ate.instruments.drivers import (
MultimeterDriver,
PowerSupplyDriver,
ThermalChamberDriver,
)
class TestInterfaceImplementation:
"""Test that drivers correctly implement the interface protocols."""
def test_thermal_chamber_implements_interface(self):
"""Verify ThermalChamberDriver implements IThermalChamber."""
# ABC inheritance ensures interface compliance at class definition time
assert issubclass(ThermalChamberDriver, IThermalChamber)
def test_power_supply_implements_interface(self):
"""Verify PowerSupplyDriver implements IPowerSupply."""
assert issubclass(PowerSupplyDriver, IPowerSupply)
def test_multimeter_implements_interface(self):
"""Verify MultimeterDriver implements IMultimeter."""
assert issubclass(MultimeterDriver, IMultimeter)
def test_thermal_chamber_has_all_methods(self):
"""Verify ThermalChamberDriver has all required methods."""
required_methods = [
"set_temperature",
"get_temperature",
"get_setpoint",
"is_stable",
"wait_until_stable",
"set_ramp_rate",
]
for method in required_methods:
assert hasattr(ThermalChamberDriver, method)
def test_power_supply_has_all_methods(self):
"""Verify PowerSupplyDriver has all required methods."""
required_methods = [
"set_voltage",
"get_voltage",
"set_current_limit",
"get_current_limit",
"measure_voltage",
"measure_current",
"enable_output",
"is_output_enabled",
]
for method in required_methods:
assert hasattr(PowerSupplyDriver, method)
def test_multimeter_has_all_methods(self):
"""Verify MultimeterDriver has all required methods."""
required_methods = [
"measure_dc_voltage",
"measure_dc_current",
"measure_resistance",
"set_integration_time",
]
for method in required_methods:
assert hasattr(MultimeterDriver, method)
class TestInstrumentSet:
"""Test the InstrumentSet dataclass."""
def test_instrument_set_creation(self):
"""Verify InstrumentSet can be created with mock instruments."""
from unittest.mock import Mock
# Create mock instruments that satisfy the interface
mock_chamber = Mock(spec=IThermalChamber)
mock_psu = Mock(spec=IPowerSupply)
mock_dmm = Mock(spec=IMultimeter)
instrument_set = InstrumentSet(
chamber=mock_chamber, psu=mock_psu, dmm=mock_dmm
)
assert instrument_set.chamber is mock_chamber
assert instrument_set.psu is mock_psu
assert instrument_set.dmm is mock_dmm
def test_instrument_set_type_annotations(self):
"""Verify InstrumentSet has correct type annotations."""
annotations = InstrumentSet.__annotations__
assert annotations["chamber"] == IThermalChamber
assert annotations["psu"] == IPowerSupply
assert annotations["dmm"] == IMultimeter
class TestInstrumentConfig:
"""Test the InstrumentConfig dataclass."""
def test_config_defaults_simulator(self):
"""Verify default configuration for simulator backend."""
config = InstrumentConfig(backend="simulator")
assert config.backend == "simulator"
assert config.simulator_host == "localhost"
assert config.chamber_port == 5001
assert config.psu_port == 5002
assert config.dmm_port == 5003
assert config.chamber_visa is None
assert config.psu_visa is None
assert config.dmm_visa is None
def test_config_custom_ports(self):
"""Verify configuration accepts custom port settings."""
config = InstrumentConfig(
backend="simulator",
simulator_host="192.168.1.100",
chamber_port=6001,
psu_port=6002,
dmm_port=6003,
)
assert config.simulator_host == "192.168.1.100"
assert config.chamber_port == 6001
assert config.psu_port == 6002
assert config.dmm_port == 6003
def test_config_pyvisa_backend(self):
"""Verify configuration for PyVISA backend."""
config = InstrumentConfig(
backend="pyvisa",
chamber_visa="TCPIP::192.168.1.10::INSTR",
psu_visa="TCPIP::192.168.1.11::INSTR",
dmm_visa="TCPIP::192.168.1.12::INSTR",
)
assert config.backend == "pyvisa"
assert config.chamber_visa == "TCPIP::192.168.1.10::INSTR"
assert config.psu_visa == "TCPIP::192.168.1.11::INSTR"
assert config.dmm_visa == "TCPIP::192.168.1.12::INSTR"
class TestInstrumentFactory:
"""Test the InstrumentFactory."""
def test_factory_rejects_unknown_backend(self):
"""Verify factory raises error for unknown backend."""
config = InstrumentConfig(backend="invalid") # type: ignore
with pytest.raises(ValueError, match="Unknown backend: invalid"):
InstrumentFactory.create(config)
def test_factory_pyvisa_not_implemented(self):
"""Verify PyVISA backend raises NotImplementedError."""
config = InstrumentConfig(backend="pyvisa")
with pytest.raises(NotImplementedError, match="PyVISA backend not yet"):
InstrumentFactory.create(config)
def test_factory_creates_instrument_set(self):
"""Verify factory creates InstrumentSet with correct structure."""
from unittest.mock import Mock, patch
config = InstrumentConfig(backend="simulator")
# Mock the transports and drivers to avoid actual connections
# Patch where they're imported FROM, not where they're used
with (
patch(
"py_dvt_ate.instruments.transport.tcp.TCPTransport"
) as mock_tcp_transport,
patch(
"py_dvt_ate.instruments.drivers.chamber.ThermalChamberDriver"
) as mock_chamber,
patch(
"py_dvt_ate.instruments.drivers.power_supply.PowerSupplyDriver"
) as mock_psu,
patch(
"py_dvt_ate.instruments.drivers.multimeter.MultimeterDriver"
) as mock_dmm,
):
# Create mock instrument instances
mock_chamber_instance = Mock(spec=IThermalChamber)
mock_psu_instance = Mock(spec=IPowerSupply)
mock_dmm_instance = Mock(spec=IMultimeter)
mock_chamber.return_value = mock_chamber_instance
mock_psu.return_value = mock_psu_instance
mock_dmm.return_value = mock_dmm_instance
instrument_set = InstrumentFactory.create(config)
# Verify InstrumentSet was created
assert isinstance(instrument_set, InstrumentSet)
# Verify transports were created with correct parameters
assert mock_tcp_transport.call_count == 3
mock_tcp_transport.assert_any_call("localhost", 5001) # chamber
mock_tcp_transport.assert_any_call("localhost", 5002) # psu
mock_tcp_transport.assert_any_call("localhost", 5003) # dmm
# Verify drivers were created
assert mock_chamber.call_count == 1
assert mock_psu.call_count == 1
assert mock_dmm.call_count == 1
# Verify InstrumentSet contains the mock instances
assert instrument_set.chamber is mock_chamber_instance
assert instrument_set.psu is mock_psu_instance
assert instrument_set.dmm is mock_dmm_instance
def test_factory_uses_custom_ports(self):
"""Verify factory uses custom port configuration."""
from unittest.mock import patch
config = InstrumentConfig(
backend="simulator",
simulator_host="testserver",
chamber_port=7001,
psu_port=7002,
dmm_port=7003,
)
with patch(
"py_dvt_ate.instruments.transport.tcp.TCPTransport"
) as mock_tcp_transport:
InstrumentFactory.create(config)
# Verify custom host and ports were used
mock_tcp_transport.assert_any_call("testserver", 7001)
mock_tcp_transport.assert_any_call("testserver", 7002)
mock_tcp_transport.assert_any_call("testserver", 7003)
def test_factory_returns_correct_types(self):
"""Verify factory returns instruments implementing correct interfaces."""
from unittest.mock import Mock, patch
config = InstrumentConfig(backend="simulator")
with (
patch("py_dvt_ate.instruments.transport.tcp.TCPTransport"),
patch(
"py_dvt_ate.instruments.drivers.chamber.ThermalChamberDriver"
) as mock_chamber,
patch(
"py_dvt_ate.instruments.drivers.power_supply.PowerSupplyDriver"
) as mock_psu,
patch(
"py_dvt_ate.instruments.drivers.multimeter.MultimeterDriver"
) as mock_dmm,
):
# Make the mocks subclasses of the interfaces
mock_chamber.return_value = Mock(spec=IThermalChamber)
mock_psu.return_value = Mock(spec=IPowerSupply)
mock_dmm.return_value = Mock(spec=IMultimeter)
instrument_set = InstrumentFactory.create(config)
# Verify returned instruments satisfy the interface specs
# (Mock with spec=Interface makes isinstance checks work)
assert isinstance(instrument_set, InstrumentSet)