Files
py-dvt-ate/tests/unit/test_multimeter.py
Kai Chappell 0179cc384a Add multimeter simulator tests
Comprehensive test coverage for MultimeterSim including MEAS:VOLT:DC,
MEAS:CURR:DC, CONF, and READ commands. Tests both standalone operation
and physics engine integration including temperature-dependent measurements.
2025-12-02 13:46:03 +00:00

307 lines
9.9 KiB
Python

"""Unit tests for multimeter simulator."""
import pytest
from py_dvt_ate.simulation.physics.engine import PhysicsEngine
from py_dvt_ate.simulation.virtual.multimeter import MultimeterSim
class TestMultimeterSimBasic:
"""Tests for MultimeterSim without physics engine."""
@pytest.fixture
def dmm(self) -> MultimeterSim:
"""Create multimeter instance without physics engine."""
return MultimeterSim()
def test_creation(self, dmm: MultimeterSim) -> None:
"""Test multimeter can be created."""
assert dmm is not None
assert dmm.model == "DMM-SIM-001"
assert dmm.manufacturer == "PyDVTATE"
def test_idn_query(self, dmm: MultimeterSim) -> None:
"""Test *IDN? returns identification string."""
response = dmm.process("*IDN?")
assert "PyDVTATE" in response
assert "DMM-SIM-001" in response
def test_rst_command(self, dmm: MultimeterSim) -> None:
"""Test *RST resets to defaults."""
# Set non-default config
dmm.process("CONF:CURR:DC")
assert dmm.process("CONF?") == '"CURR:DC"'
# Reset
response = dmm.process("*RST")
assert response == ""
# Check defaults restored
assert dmm.process("CONF?") == '"VOLT:DC"'
def test_opc_query(self, dmm: MultimeterSim) -> None:
"""Test *OPC? returns 1."""
response = dmm.process("*OPC?")
assert response == "1"
def test_unknown_command(self, dmm: MultimeterSim) -> None:
"""Test unknown command returns error."""
response = dmm.process("INVALID:CMD")
assert response.startswith("ERROR:")
assert "Unknown command" in response
class TestMultimeterMeasVoltDC:
"""Tests for MEAS:VOLT:DC command."""
@pytest.fixture
def dmm(self) -> MultimeterSim:
"""Create multimeter instance without physics engine."""
return MultimeterSim()
def test_meas_volt_dc_query_no_engine(self, dmm: MultimeterSim) -> None:
"""Test MEAS:VOLT:DC? returns 0 without physics engine."""
response = dmm.process("MEAS:VOLT:DC?")
assert response == "0.000000"
def test_meas_volt_dc_sets_config(self, dmm: MultimeterSim) -> None:
"""Test MEAS:VOLT:DC? sets configuration to VOLT:DC."""
dmm.process("CONF:CURR:DC")
dmm.process("MEAS:VOLT:DC?")
assert dmm.process("CONF?") == '"VOLT:DC"'
def test_meas_volt_dc_as_command_fails(self, dmm: MultimeterSim) -> None:
"""Test MEAS:VOLT:DC (without ?) returns error."""
response = dmm.process("MEAS:VOLT:DC 1.0")
assert response.startswith("ERROR:")
assert "query only" in response
class TestMultimeterMeasCurrDC:
"""Tests for MEAS:CURR:DC command."""
@pytest.fixture
def dmm(self) -> MultimeterSim:
"""Create multimeter instance without physics engine."""
return MultimeterSim()
def test_meas_curr_dc_query_no_engine(self, dmm: MultimeterSim) -> None:
"""Test MEAS:CURR:DC? returns 0 without physics engine."""
response = dmm.process("MEAS:CURR:DC?")
assert response == "0.000000"
def test_meas_curr_dc_sets_config(self, dmm: MultimeterSim) -> None:
"""Test MEAS:CURR:DC? sets configuration to CURR:DC."""
dmm.process("MEAS:CURR:DC?")
assert dmm.process("CONF?") == '"CURR:DC"'
def test_meas_curr_dc_as_command_fails(self, dmm: MultimeterSim) -> None:
"""Test MEAS:CURR:DC (without ?) returns error."""
response = dmm.process("MEAS:CURR:DC 0.1")
assert response.startswith("ERROR:")
assert "query only" in response
class TestMultimeterConf:
"""Tests for CONF commands."""
@pytest.fixture
def dmm(self) -> MultimeterSim:
"""Create multimeter instance without physics engine."""
return MultimeterSim()
def test_conf_query_default(self, dmm: MultimeterSim) -> None:
"""Test CONF? returns default configuration."""
response = dmm.process("CONF?")
assert response == '"VOLT:DC"'
def test_conf_volt_dc(self, dmm: MultimeterSim) -> None:
"""Test CONF:VOLT:DC sets voltage measurement mode."""
dmm.process("CONF:CURR:DC")
response = dmm.process("CONF:VOLT:DC")
assert response == ""
assert dmm.process("CONF?") == '"VOLT:DC"'
def test_conf_volt_dc_as_query_fails(self, dmm: MultimeterSim) -> None:
"""Test CONF:VOLT:DC? returns error."""
response = dmm.process("CONF:VOLT:DC?")
assert response.startswith("ERROR:")
assert "command only" in response
def test_conf_curr_dc(self, dmm: MultimeterSim) -> None:
"""Test CONF:CURR:DC sets current measurement mode."""
response = dmm.process("CONF:CURR:DC")
assert response == ""
assert dmm.process("CONF?") == '"CURR:DC"'
def test_conf_curr_dc_as_query_fails(self, dmm: MultimeterSim) -> None:
"""Test CONF:CURR:DC? returns error."""
response = dmm.process("CONF:CURR:DC?")
assert response.startswith("ERROR:")
assert "command only" in response
def test_conf_as_command_fails(self, dmm: MultimeterSim) -> None:
"""Test CONF without subcommand returns error."""
response = dmm.process("CONF")
assert response.startswith("ERROR:")
class TestMultimeterRead:
"""Tests for READ command."""
@pytest.fixture
def dmm(self) -> MultimeterSim:
"""Create multimeter instance without physics engine."""
return MultimeterSim()
def test_read_query_volt_mode(self, dmm: MultimeterSim) -> None:
"""Test READ? returns voltage when configured for voltage."""
dmm.process("CONF:VOLT:DC")
response = dmm.process("READ?")
assert response == "0.000000"
def test_read_query_curr_mode(self, dmm: MultimeterSim) -> None:
"""Test READ? returns current when configured for current."""
dmm.process("CONF:CURR:DC")
response = dmm.process("READ?")
assert response == "0.000000"
def test_read_as_command_fails(self, dmm: MultimeterSim) -> None:
"""Test READ (without ?) returns error."""
response = dmm.process("READ")
assert response.startswith("ERROR:")
assert "query only" in response
class TestMultimeterWithPhysicsEngine:
"""Tests for MultimeterSim with physics engine integration."""
@pytest.fixture
def engine(self) -> PhysicsEngine:
"""Create physics engine instance."""
return PhysicsEngine(update_rate_hz=100.0)
@pytest.fixture
def dmm(self, engine: PhysicsEngine) -> MultimeterSim:
"""Create multimeter instance with physics engine."""
return MultimeterSim(physics_engine=engine)
def test_meas_volt_dc_returns_engine_voltage(
self, dmm: MultimeterSim, engine: PhysicsEngine
) -> None:
"""Test MEAS:VOLT:DC? returns physics engine output voltage."""
engine.set_input_voltage(5.0)
engine.set_output_enabled(True)
engine.step()
response = dmm.process("MEAS:VOLT:DC?")
# LDO model outputs ~3.3V nominal
voltage = float(response)
assert voltage > 3.0
assert voltage < 4.0
def test_meas_volt_dc_returns_zero_when_disabled(
self, dmm: MultimeterSim, engine: PhysicsEngine
) -> None:
"""Test MEAS:VOLT:DC? returns 0 when DUT output disabled."""
engine.set_input_voltage(5.0)
engine.set_output_enabled(False)
engine.step()
response = dmm.process("MEAS:VOLT:DC?")
assert response == "0.000000"
def test_meas_curr_dc_returns_engine_current(
self, dmm: MultimeterSim, engine: PhysicsEngine
) -> None:
"""Test MEAS:CURR:DC? returns physics engine load current."""
engine.set_input_voltage(5.0)
engine.set_load_current(0.1)
engine.set_output_enabled(True)
engine.step()
response = dmm.process("MEAS:CURR:DC?")
assert float(response) == pytest.approx(0.1, abs=0.001)
def test_meas_curr_dc_returns_zero_when_disabled(
self, dmm: MultimeterSim, engine: PhysicsEngine
) -> None:
"""Test MEAS:CURR:DC? returns 0 when DUT output disabled."""
engine.set_input_voltage(5.0)
engine.set_load_current(0.1)
engine.set_output_enabled(False)
engine.step()
response = dmm.process("MEAS:CURR:DC?")
assert response == "0.000000"
def test_read_uses_configured_function(
self, dmm: MultimeterSim, engine: PhysicsEngine
) -> None:
"""Test READ? respects configured measurement function."""
engine.set_input_voltage(5.0)
engine.set_load_current(0.1)
engine.set_output_enabled(True)
engine.step()
# Configure for current
dmm.process("CONF:CURR:DC")
response = dmm.process("READ?")
# Should return current, not voltage
assert float(response) == pytest.approx(0.1, abs=0.001)
def test_reset_restores_voltage_mode(
self, dmm: MultimeterSim, engine: PhysicsEngine
) -> None:
"""Test *RST restores default voltage measurement mode."""
dmm.process("CONF:CURR:DC")
dmm.process("*RST")
assert dmm.process("CONF?") == '"VOLT:DC"'
def test_voltage_changes_with_temperature(
self, dmm: MultimeterSim, engine: PhysicsEngine
) -> None:
"""Test measured voltage changes with DUT temperature."""
engine.set_input_voltage(5.0)
engine.set_output_enabled(True)
engine.step()
# Measure at initial temperature
response1 = dmm.process("MEAS:VOLT:DC?")
v1 = float(response1)
# Change chamber temperature and let settle
engine.set_chamber_setpoint(85.0)
for _ in range(5000): # Let temperature settle somewhat
engine.step()
# Measure at elevated temperature
response2 = dmm.process("MEAS:VOLT:DC?")
v2 = float(response2)
# Output voltage should have changed (LDO has tempco)
assert v1 != v2