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.
This commit is contained in:
306
tests/unit/test_multimeter.py
Normal file
306
tests/unit/test_multimeter.py
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
"""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
|
||||||
Reference in New Issue
Block a user