"""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)