Add instrument interface tests

This commit is contained in:
2025-07-26 20:12:06 +00:00
parent 03e364e071
commit 990a910ecb

View File

@@ -0,0 +1,273 @@
"""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,
IPowerSupply,
IThermalChamber,
InstrumentConfig,
InstrumentFactory,
InstrumentSet,
)
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)