From eac1be845f7a2f7de0ac0621a7ba03b444996bd6 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Thu, 10 Jul 2025 15:57:02 +0000 Subject: [PATCH] Add driver unit tests --- tests/unit/test_drivers.py | 346 +++++++++++++++++++++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 tests/unit/test_drivers.py diff --git a/tests/unit/test_drivers.py b/tests/unit/test_drivers.py new file mode 100644 index 0000000..9187d1d --- /dev/null +++ b/tests/unit/test_drivers.py @@ -0,0 +1,346 @@ +"""Unit tests for instrument drivers. + +Tests SCPI command formatting and driver functionality using mock transports. +""" + +from unittest.mock import MagicMock + +import pytest + +from py_dvt_ate.instruments.drivers.base import BaseDriver +from py_dvt_ate.instruments.drivers.chamber import ThermalChamberDriver +from py_dvt_ate.instruments.drivers.multimeter import MultimeterDriver +from py_dvt_ate.instruments.drivers.power_supply import PowerSupplyDriver + + +@pytest.fixture +def mock_transport(): + """Create a mock transport for testing.""" + transport = MagicMock() + transport.is_connected = True + return transport + + +class TestBaseDriver: + """Tests for BaseDriver base class.""" + + def test_connect(self, mock_transport): + """Test connection establishment.""" + driver = BaseDriver(mock_transport) + driver.connect() + mock_transport.connect.assert_called_once() + + def test_disconnect(self, mock_transport): + """Test disconnection.""" + driver = BaseDriver(mock_transport) + driver.disconnect() + mock_transport.disconnect.assert_called_once() + + def test_is_connected(self, mock_transport): + """Test connection status check.""" + driver = BaseDriver(mock_transport) + assert driver.is_connected is True + + def test_write(self, mock_transport): + """Test SCPI command write.""" + driver = BaseDriver(mock_transport) + driver.write("VOLT 3.3") + mock_transport.write.assert_called_once_with("VOLT 3.3") + + def test_query(self, mock_transport): + """Test SCPI query.""" + mock_transport.query.return_value = "3.300" + driver = BaseDriver(mock_transport) + result = driver.query("VOLT?") + assert result == "3.300" + mock_transport.query.assert_called_once_with("VOLT?", None) + + def test_query_float(self, mock_transport): + """Test SCPI query with float parsing.""" + mock_transport.query.return_value = "3.300" + driver = BaseDriver(mock_transport) + result = driver.query_float("VOLT?") + assert result == 3.3 + assert isinstance(result, float) + + def test_query_float_invalid(self, mock_transport): + """Test SCPI query with invalid float response.""" + mock_transport.query.return_value = "INVALID" + driver = BaseDriver(mock_transport) + with pytest.raises(ValueError, match="Cannot parse 'INVALID' as float"): + driver.query_float("VOLT?") + + def test_query_int(self, mock_transport): + """Test SCPI query with integer parsing.""" + mock_transport.query.return_value = "42" + driver = BaseDriver(mock_transport) + result = driver.query_int("COUNT?") + assert result == 42 + assert isinstance(result, int) + + def test_query_int_invalid(self, mock_transport): + """Test SCPI query with invalid integer response.""" + mock_transport.query.return_value = "3.14" + driver = BaseDriver(mock_transport) + with pytest.raises(ValueError, match="Cannot parse '3.14' as int"): + driver.query_int("COUNT?") + + def test_query_bool_true_variants(self, mock_transport): + """Test SCPI query with boolean parsing - true variants.""" + driver = BaseDriver(mock_transport) + + for value in ["1", "ON", "TRUE", "on", "true"]: + mock_transport.query.return_value = value + result = driver.query_bool("OUTP?") + assert result is True + + def test_query_bool_false_variants(self, mock_transport): + """Test SCPI query with boolean parsing - false variants.""" + driver = BaseDriver(mock_transport) + + for value in ["0", "OFF", "FALSE", "off", "false"]: + mock_transport.query.return_value = value + result = driver.query_bool("OUTP?") + assert result is False + + def test_query_bool_invalid(self, mock_transport): + """Test SCPI query with invalid boolean response.""" + mock_transport.query.return_value = "MAYBE" + driver = BaseDriver(mock_transport) + with pytest.raises(ValueError, match="Cannot parse 'MAYBE' as bool"): + driver.query_bool("OUTP?") + + def test_identify(self, mock_transport): + """Test instrument identification query.""" + mock_transport.query.return_value = "Manufacturer,Model,SN123,1.0.0" + driver = BaseDriver(mock_transport) + result = driver.identify() + assert result == "Manufacturer,Model,SN123,1.0.0" + mock_transport.query.assert_called_once_with("*IDN?", None) + + def test_reset(self, mock_transport): + """Test instrument reset command.""" + driver = BaseDriver(mock_transport) + driver.reset() + mock_transport.write.assert_called_once_with("*RST") + + def test_clear_status(self, mock_transport): + """Test clear status command.""" + driver = BaseDriver(mock_transport) + driver.clear_status() + mock_transport.write.assert_called_once_with("*CLS") + + def test_operation_complete(self, mock_transport): + """Test operation complete query.""" + mock_transport.query.return_value = "1" + driver = BaseDriver(mock_transport) + result = driver.operation_complete() + assert result is True + mock_transport.query.assert_called_once_with("*OPC?", None) + + +class TestThermalChamberDriver: + """Tests for ThermalChamberDriver.""" + + def test_set_temperature(self, mock_transport): + """Test temperature setpoint command.""" + driver = ThermalChamberDriver(mock_transport) + driver.set_temperature(85.0) + mock_transport.write.assert_called_once_with("TEMP:SETPOINT 85.00") + + def test_get_temperature(self, mock_transport): + """Test temperature measurement query.""" + mock_transport.query.return_value = "25.50" + driver = ThermalChamberDriver(mock_transport) + temp = driver.get_temperature() + assert temp == 25.5 + mock_transport.query.assert_called_once_with("TEMP:ACTUAL?", None) + + def test_get_setpoint(self, mock_transport): + """Test setpoint query.""" + mock_transport.query.return_value = "85.00" + driver = ThermalChamberDriver(mock_transport) + setpoint = driver.get_setpoint() + assert setpoint == 85.0 + mock_transport.query.assert_called_once_with("TEMP:SETPOINT?", None) + + def test_is_stable_true(self, mock_transport): + """Test stability check - stable.""" + mock_transport.query.return_value = "1" + driver = ThermalChamberDriver(mock_transport) + assert driver.is_stable() is True + + def test_is_stable_false(self, mock_transport): + """Test stability check - not stable.""" + mock_transport.query.return_value = "0" + driver = ThermalChamberDriver(mock_transport) + assert driver.is_stable() is False + + def test_wait_until_stable_immediate(self, mock_transport): + """Test wait for stability - already stable.""" + mock_transport.query.return_value = "1" + driver = ThermalChamberDriver(mock_transport) + result = driver.wait_until_stable(timeout=5.0, poll_interval=0.1) + assert result is True + + def test_wait_until_stable_timeout(self, mock_transport): + """Test wait for stability - timeout.""" + mock_transport.query.return_value = "0" # Never becomes stable + driver = ThermalChamberDriver(mock_transport) + result = driver.wait_until_stable(timeout=0.2, poll_interval=0.1) + assert result is False + + def test_wait_until_stable_invalid_timeout(self, mock_transport): + """Test wait with negative timeout.""" + driver = ThermalChamberDriver(mock_transport) + with pytest.raises(ValueError, match="Timeout must be non-negative"): + driver.wait_until_stable(timeout=-1.0) + + def test_wait_until_stable_invalid_interval(self, mock_transport): + """Test wait with non-positive poll interval.""" + driver = ThermalChamberDriver(mock_transport) + with pytest.raises(ValueError, match="Poll interval must be positive"): + driver.wait_until_stable(poll_interval=0.0) + + def test_set_ramp_rate(self, mock_transport): + """Test ramp rate command.""" + driver = ThermalChamberDriver(mock_transport) + driver.set_ramp_rate(5.0) + mock_transport.write.assert_called_once_with("TEMP:RAMP 5.00") + + def test_get_ramp_rate(self, mock_transport): + """Test ramp rate query.""" + mock_transport.query.return_value = "5.00" + driver = ThermalChamberDriver(mock_transport) + rate = driver.get_ramp_rate() + assert rate == 5.0 + + +class TestPowerSupplyDriver: + """Tests for PowerSupplyDriver.""" + + def test_set_voltage(self, mock_transport): + """Test voltage setpoint command.""" + driver = PowerSupplyDriver(mock_transport) + driver.set_voltage(1, 3.3) + mock_transport.write.assert_called_once_with("VOLT 3.300") + + def test_get_voltage(self, mock_transport): + """Test voltage setpoint query.""" + mock_transport.query.return_value = "3.300" + driver = PowerSupplyDriver(mock_transport) + voltage = driver.get_voltage(1) + assert voltage == 3.3 + + def test_set_current_limit(self, mock_transport): + """Test current limit command.""" + driver = PowerSupplyDriver(mock_transport) + driver.set_current_limit(1, 0.5) + mock_transport.write.assert_called_once_with("CURR 0.500") + + def test_get_current_limit(self, mock_transport): + """Test current limit query.""" + mock_transport.query.return_value = "0.500" + driver = PowerSupplyDriver(mock_transport) + current = driver.get_current_limit(1) + assert current == 0.5 + + def test_measure_voltage(self, mock_transport): + """Test voltage measurement.""" + mock_transport.query.return_value = "3.305" + driver = PowerSupplyDriver(mock_transport) + voltage = driver.measure_voltage(1) + assert voltage == 3.305 + mock_transport.query.assert_called_once_with("MEAS:VOLT?", None) + + def test_measure_current(self, mock_transport): + """Test current measurement.""" + mock_transport.query.return_value = "0.125" + driver = PowerSupplyDriver(mock_transport) + current = driver.measure_current(1) + assert current == 0.125 + mock_transport.query.assert_called_once_with("MEAS:CURR?", None) + + def test_enable_output_on(self, mock_transport): + """Test enable output command.""" + driver = PowerSupplyDriver(mock_transport) + driver.enable_output(1, True) + mock_transport.write.assert_called_once_with("OUTP ON") + + def test_enable_output_off(self, mock_transport): + """Test disable output command.""" + driver = PowerSupplyDriver(mock_transport) + driver.enable_output(1, False) + mock_transport.write.assert_called_once_with("OUTP OFF") + + def test_is_output_enabled_true(self, mock_transport): + """Test output enabled query - enabled.""" + mock_transport.query.return_value = "1" + driver = PowerSupplyDriver(mock_transport) + assert driver.is_output_enabled(1) is True + + def test_is_output_enabled_false(self, mock_transport): + """Test output enabled query - disabled.""" + mock_transport.query.return_value = "0" + driver = PowerSupplyDriver(mock_transport) + assert driver.is_output_enabled(1) is False + + +class TestMultimeterDriver: + """Tests for MultimeterDriver.""" + + def test_measure_dc_voltage(self, mock_transport): + """Test DC voltage measurement.""" + mock_transport.query.return_value = "3.300000" + driver = MultimeterDriver(mock_transport) + voltage = driver.measure_dc_voltage() + assert voltage == 3.3 + mock_transport.query.assert_called_once_with("MEAS:VOLT:DC?", None) + + def test_measure_dc_current(self, mock_transport): + """Test DC current measurement.""" + mock_transport.query.return_value = "0.125000" + driver = MultimeterDriver(mock_transport) + current = driver.measure_dc_current() + assert current == 0.125 + mock_transport.query.assert_called_once_with("MEAS:CURR:DC?", None) + + def test_measure_resistance_not_implemented(self, mock_transport): + """Test resistance measurement raises NotImplementedError.""" + driver = MultimeterDriver(mock_transport) + with pytest.raises(NotImplementedError, match="Resistance measurement"): + driver.measure_resistance() + + def test_set_integration_time_not_implemented(self, mock_transport): + """Test integration time setting raises NotImplementedError.""" + driver = MultimeterDriver(mock_transport) + with pytest.raises(NotImplementedError, match="Integration time"): + driver.set_integration_time(1.0) + + def test_configure_dc_voltage(self, mock_transport): + """Test configure for DC voltage.""" + driver = MultimeterDriver(mock_transport) + driver.configure_dc_voltage() + mock_transport.write.assert_called_once_with("CONF:VOLT:DC") + + def test_configure_dc_current(self, mock_transport): + """Test configure for DC current.""" + driver = MultimeterDriver(mock_transport) + driver.configure_dc_current() + mock_transport.write.assert_called_once_with("CONF:CURR:DC") + + def test_get_configuration(self, mock_transport): + """Test get current configuration.""" + mock_transport.query.return_value = '"VOLT:DC"' + driver = MultimeterDriver(mock_transport) + config = driver.get_configuration() + assert config == "VOLT:DC" + mock_transport.query.assert_called_once_with("CONF?", None) + + def test_read(self, mock_transport): + """Test read measurement with current configuration.""" + mock_transport.query.return_value = "3.300000" + driver = MultimeterDriver(mock_transport) + value = driver.read() + assert value == 3.3 + mock_transport.query.assert_called_once_with("READ?", None)