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