From 53bbfb823a8668a7d5ce4baac54ad8966772d3c3 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Sat, 26 Jul 2025 20:12:06 +0000 Subject: [PATCH] Add instrument interface tests --- tests/unit/test_instruments.py | 273 +++++++++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 tests/unit/test_instruments.py diff --git a/tests/unit/test_instruments.py b/tests/unit/test_instruments.py new file mode 100644 index 0000000..fede82c --- /dev/null +++ b/tests/unit/test_instruments.py @@ -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)