Add instrument interface tests
This commit is contained in:
273
tests/unit/test_instruments.py
Normal file
273
tests/unit/test_instruments.py
Normal 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)
|
||||||
Reference in New Issue
Block a user