"""Unit tests for power supply simulator.""" import pytest from py_dvt_ate.simulation.physics.engine import PhysicsEngine from py_dvt_ate.simulation.virtual.power_supply import PowerSupplySim class TestPowerSupplySimBasic: """Tests for PowerSupplySim without physics engine.""" @pytest.fixture def psu(self) -> PowerSupplySim: """Create power supply instance without physics engine.""" return PowerSupplySim() def test_creation(self, psu: PowerSupplySim) -> None: """Test power supply can be created.""" assert psu is not None assert psu.model == "PS-SIM-001" assert psu.manufacturer == "PyDVTATE" def test_idn_query(self, psu: PowerSupplySim) -> None: """Test *IDN? returns identification string.""" response = psu.process("*IDN?") assert "PyDVTATE" in response assert "PS-SIM-001" in response def test_rst_command(self, psu: PowerSupplySim) -> None: """Test *RST resets to defaults.""" # Set non-default values psu.process("VOLT 12.0") psu.process("CURR 2.0") psu.process("OUTP ON") # Reset response = psu.process("*RST") assert response == "" # Check defaults restored assert psu.process("VOLT?") == "0.000" assert psu.process("CURR?") == "1.000" assert psu.process("OUTP?") == "0" def test_opc_query(self, psu: PowerSupplySim) -> None: """Test *OPC? returns 1.""" response = psu.process("*OPC?") assert response == "1" def test_unknown_command(self, psu: PowerSupplySim) -> None: """Test unknown command returns error.""" response = psu.process("INVALID:CMD") assert response.startswith("ERROR:") assert "Unknown command" in response class TestPowerSupplyVoltage: """Tests for VOLT command.""" @pytest.fixture def psu(self) -> PowerSupplySim: """Create power supply instance without physics engine.""" return PowerSupplySim() def test_volt_query_default(self, psu: PowerSupplySim) -> None: """Test VOLT? returns default value.""" response = psu.process("VOLT?") assert response == "0.000" def test_volt_set(self, psu: PowerSupplySim) -> None: """Test VOLT sets value.""" response = psu.process("VOLT 12.5") assert response == "" assert psu.process("VOLT?") == "12.500" def test_volt_set_decimal(self, psu: PowerSupplySim) -> None: """Test VOLT accepts decimal values.""" psu.process("VOLT 3.3") assert psu.process("VOLT?") == "3.300" def test_volt_set_negative_fails(self, psu: PowerSupplySim) -> None: """Test VOLT rejects negative values.""" response = psu.process("VOLT -5.0") assert response.startswith("ERROR:") assert "negative" in response def test_volt_set_invalid_value(self, psu: PowerSupplySim) -> None: """Test VOLT with invalid value returns error.""" response = psu.process("VOLT abc") assert response.startswith("ERROR:") assert "Invalid voltage" in response def test_volt_set_no_argument(self, psu: PowerSupplySim) -> None: """Test VOLT without argument returns error.""" response = psu.process("VOLT") assert response.startswith("ERROR:") assert "requires a value" in response class TestPowerSupplyCurrent: """Tests for CURR command.""" @pytest.fixture def psu(self) -> PowerSupplySim: """Create power supply instance without physics engine.""" return PowerSupplySim() def test_curr_query_default(self, psu: PowerSupplySim) -> None: """Test CURR? returns default value.""" response = psu.process("CURR?") assert response == "1.000" def test_curr_set(self, psu: PowerSupplySim) -> None: """Test CURR sets value.""" response = psu.process("CURR 0.5") assert response == "" assert psu.process("CURR?") == "0.500" def test_curr_set_negative_fails(self, psu: PowerSupplySim) -> None: """Test CURR rejects negative values.""" response = psu.process("CURR -1.0") assert response.startswith("ERROR:") assert "negative" in response def test_curr_set_invalid_value(self, psu: PowerSupplySim) -> None: """Test CURR with invalid value returns error.""" response = psu.process("CURR xyz") assert response.startswith("ERROR:") assert "Invalid current" in response def test_curr_set_no_argument(self, psu: PowerSupplySim) -> None: """Test CURR without argument returns error.""" response = psu.process("CURR") assert response.startswith("ERROR:") assert "requires a value" in response class TestPowerSupplyOutput: """Tests for OUTP command.""" @pytest.fixture def psu(self) -> PowerSupplySim: """Create power supply instance without physics engine.""" return PowerSupplySim() def test_outp_query_default(self, psu: PowerSupplySim) -> None: """Test OUTP? returns default value (off).""" response = psu.process("OUTP?") assert response == "0" def test_outp_set_on(self, psu: PowerSupplySim) -> None: """Test OUTP ON enables output.""" response = psu.process("OUTP ON") assert response == "" assert psu.process("OUTP?") == "1" def test_outp_set_1(self, psu: PowerSupplySim) -> None: """Test OUTP 1 enables output.""" psu.process("OUTP 1") assert psu.process("OUTP?") == "1" def test_outp_set_off(self, psu: PowerSupplySim) -> None: """Test OUTP OFF disables output.""" psu.process("OUTP ON") psu.process("OUTP OFF") assert psu.process("OUTP?") == "0" def test_outp_set_0(self, psu: PowerSupplySim) -> None: """Test OUTP 0 disables output.""" psu.process("OUTP ON") psu.process("OUTP 0") assert psu.process("OUTP?") == "0" def test_outp_set_invalid(self, psu: PowerSupplySim) -> None: """Test OUTP with invalid value returns error.""" response = psu.process("OUTP MAYBE") assert response.startswith("ERROR:") assert "Invalid output state" in response def test_outp_set_no_argument(self, psu: PowerSupplySim) -> None: """Test OUTP without argument returns error.""" response = psu.process("OUTP") assert response.startswith("ERROR:") assert "requires a value" in response class TestPowerSupplyMeasurement: """Tests for MEAS commands.""" @pytest.fixture def psu(self) -> PowerSupplySim: """Create power supply instance without physics engine.""" return PowerSupplySim() def test_meas_volt_when_off(self, psu: PowerSupplySim) -> None: """Test MEAS:VOLT? returns 0 when output is off.""" psu.process("VOLT 12.0") response = psu.process("MEAS:VOLT?") assert response == "0.000" def test_meas_volt_when_on_no_engine(self, psu: PowerSupplySim) -> None: """Test MEAS:VOLT? returns setpoint when on without engine.""" psu.process("VOLT 12.0") psu.process("OUTP ON") response = psu.process("MEAS:VOLT?") assert response == "12.000" def test_meas_volt_as_command_fails(self, psu: PowerSupplySim) -> None: """Test MEAS:VOLT (without ?) returns error.""" response = psu.process("MEAS:VOLT 5.0") assert response.startswith("ERROR:") assert "query only" in response def test_meas_curr_when_off(self, psu: PowerSupplySim) -> None: """Test MEAS:CURR? returns 0 when output is off.""" response = psu.process("MEAS:CURR?") assert response == "0.000" def test_meas_curr_when_on_no_engine(self, psu: PowerSupplySim) -> None: """Test MEAS:CURR? returns 0 when on without engine.""" psu.process("OUTP ON") response = psu.process("MEAS:CURR?") assert response == "0.000" def test_meas_curr_as_command_fails(self, psu: PowerSupplySim) -> None: """Test MEAS:CURR (without ?) returns error.""" response = psu.process("MEAS:CURR 0.1") assert response.startswith("ERROR:") assert "query only" in response class TestPowerSupplyWithPhysicsEngine: """Tests for PowerSupplySim with physics engine integration.""" @pytest.fixture def engine(self) -> PhysicsEngine: """Create physics engine instance.""" return PhysicsEngine(update_rate_hz=100.0) @pytest.fixture def psu(self, engine: PhysicsEngine) -> PowerSupplySim: """Create power supply instance with physics engine.""" return PowerSupplySim(physics_engine=engine) def test_outp_on_enables_engine_output( self, psu: PowerSupplySim, engine: PhysicsEngine ) -> None: """Test OUTP ON enables physics engine output.""" psu.process("VOLT 5.0") psu.process("OUTP ON") assert engine.is_output_enabled is True def test_outp_off_disables_engine_output( self, psu: PowerSupplySim, engine: PhysicsEngine ) -> None: """Test OUTP OFF disables physics engine output.""" psu.process("OUTP ON") psu.process("OUTP OFF") assert engine.is_output_enabled is False def test_volt_updates_engine_when_on( self, psu: PowerSupplySim, engine: PhysicsEngine ) -> None: """Test VOLT updates engine input voltage when output is on.""" psu.process("OUTP ON") psu.process("VOLT 5.0") electrical = engine.get_electrical_state() assert electrical.input_voltage == pytest.approx(5.0) def test_volt_does_not_update_engine_when_off( self, psu: PowerSupplySim, engine: PhysicsEngine ) -> None: """Test VOLT does not update engine when output is off.""" psu.process("VOLT 5.0") electrical = engine.get_electrical_state() assert electrical.input_voltage == pytest.approx(0.0) def test_meas_volt_returns_engine_voltage( self, psu: PowerSupplySim, engine: PhysicsEngine ) -> None: """Test MEAS:VOLT? returns physics engine voltage.""" psu.process("VOLT 5.0") psu.process("OUTP ON") response = psu.process("MEAS:VOLT?") assert response == "5.000" def test_meas_curr_returns_engine_current( self, psu: PowerSupplySim, engine: PhysicsEngine ) -> None: """Test MEAS:CURR? returns total current from engine.""" psu.process("VOLT 5.0") psu.process("OUTP ON") engine.set_load_current(0.1) # Step engine to allow calculations engine.step() response = psu.process("MEAS:CURR?") # Should include load current + quiescent current assert float(response) > 0.0 def test_reset_disables_engine_output( self, psu: PowerSupplySim, engine: PhysicsEngine ) -> None: """Test *RST disables physics engine output.""" psu.process("VOLT 5.0") psu.process("OUTP ON") psu.process("*RST") assert engine.is_output_enabled is False def test_reset_sets_engine_voltage_zero( self, psu: PowerSupplySim, engine: PhysicsEngine ) -> None: """Test *RST sets physics engine voltage to zero.""" psu.process("VOLT 5.0") psu.process("OUTP ON") psu.process("*RST") electrical = engine.get_electrical_state() assert electrical.input_voltage == pytest.approx(0.0)