From e811b210823b15749afb746b590bb4845ec95d19 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Mon, 12 May 2025 17:29:00 +0000 Subject: [PATCH] Add power supply simulator tests Comprehensive test coverage for PowerSupplySim including VOLT, CURR, OUTP, and MEAS commands. Tests both standalone operation and physics engine integration. --- tests/unit/test_power_supply.py | 352 ++++++++++++++++++++++++++++++++ 1 file changed, 352 insertions(+) create mode 100644 tests/unit/test_power_supply.py diff --git a/tests/unit/test_power_supply.py b/tests/unit/test_power_supply.py new file mode 100644 index 0000000..d394cfa --- /dev/null +++ b/tests/unit/test_power_supply.py @@ -0,0 +1,352 @@ +"""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)