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