"""Unit tests for thermal chamber simulator.""" import pytest from py_dvt_ate.simulation.physics.engine import PhysicsEngine from py_dvt_ate.simulation.virtual.chamber import ThermalChamberSim class TestThermalChamberSimBasic: """Tests for ThermalChamberSim without physics engine.""" @pytest.fixture def chamber(self) -> ThermalChamberSim: """Create chamber instance without physics engine.""" return ThermalChamberSim() def test_creation(self, chamber: ThermalChamberSim) -> None: """Test chamber can be created.""" assert chamber is not None assert chamber.model == "TC-SIM-001" assert chamber.manufacturer == "PyDVTATE" def test_idn_query(self, chamber: ThermalChamberSim) -> None: """Test *IDN? returns identification string.""" response = chamber.process("*IDN?") assert "PyDVTATE" in response assert "TC-SIM-001" in response def test_rst_command(self, chamber: ThermalChamberSim) -> None: """Test *RST resets to defaults.""" # Set non-default value chamber.process("TEMP:SETPOINT 85.0") assert chamber.process("TEMP:SETPOINT?") == "85.00" # Reset response = chamber.process("*RST") assert response == "" assert chamber.process("TEMP:SETPOINT?") == "25.00" def test_opc_query(self, chamber: ThermalChamberSim) -> None: """Test *OPC? returns 1.""" response = chamber.process("*OPC?") assert response == "1" def test_unknown_command(self, chamber: ThermalChamberSim) -> None: """Test unknown command returns error.""" response = chamber.process("INVALID:CMD") assert response.startswith("ERROR:") assert "Unknown command" in response class TestThermalChamberSetpoint: """Tests for TEMP:SETPOINT command.""" @pytest.fixture def chamber(self) -> ThermalChamberSim: """Create chamber instance without physics engine.""" return ThermalChamberSim() def test_setpoint_query_default(self, chamber: ThermalChamberSim) -> None: """Test TEMP:SETPOINT? returns default value.""" response = chamber.process("TEMP:SETPOINT?") assert response == "25.00" def test_setpoint_set(self, chamber: ThermalChamberSim) -> None: """Test TEMP:SETPOINT sets value.""" response = chamber.process("TEMP:SETPOINT 85.0") assert response == "" assert chamber.process("TEMP:SETPOINT?") == "85.00" def test_setpoint_set_negative(self, chamber: ThermalChamberSim) -> None: """Test TEMP:SETPOINT accepts negative values.""" chamber.process("TEMP:SETPOINT -40.0") assert chamber.process("TEMP:SETPOINT?") == "-40.00" def test_setpoint_set_invalid_value(self, chamber: ThermalChamberSim) -> None: """Test TEMP:SETPOINT with invalid value returns error.""" response = chamber.process("TEMP:SETPOINT abc") assert response.startswith("ERROR:") assert "Invalid temperature" in response def test_setpoint_set_no_argument(self, chamber: ThermalChamberSim) -> None: """Test TEMP:SETPOINT without argument returns error.""" response = chamber.process("TEMP:SETPOINT") assert response.startswith("ERROR:") assert "requires a value" in response class TestThermalChamberActual: """Tests for TEMP:ACTUAL? query.""" @pytest.fixture def chamber(self) -> ThermalChamberSim: """Create chamber instance without physics engine.""" return ThermalChamberSim() def test_actual_query_without_engine(self, chamber: ThermalChamberSim) -> None: """Test TEMP:ACTUAL? returns setpoint when no physics engine.""" chamber.process("TEMP:SETPOINT 50.0") response = chamber.process("TEMP:ACTUAL?") # Without physics engine, returns setpoint assert response == "50.00" def test_actual_as_command_fails(self, chamber: ThermalChamberSim) -> None: """Test TEMP:ACTUAL (without ?) returns error.""" response = chamber.process("TEMP:ACTUAL 25.0") assert response.startswith("ERROR:") assert "query only" in response class TestThermalChamberStability: """Tests for TEMP:STAB? query.""" @pytest.fixture def chamber(self) -> ThermalChamberSim: """Create chamber instance without physics engine.""" return ThermalChamberSim() def test_stab_query_without_engine(self, chamber: ThermalChamberSim) -> None: """Test TEMP:STAB? returns 1 when no physics engine.""" response = chamber.process("TEMP:STAB?") # Without physics engine, assume stable assert response == "1" def test_stab_as_command_fails(self, chamber: ThermalChamberSim) -> None: """Test TEMP:STAB (without ?) returns error.""" response = chamber.process("TEMP:STAB 1") assert response.startswith("ERROR:") assert "query only" in response class TestThermalChamberWithPhysicsEngine: """Tests for ThermalChamberSim with physics engine integration.""" @pytest.fixture def engine(self) -> PhysicsEngine: """Create physics engine instance.""" return PhysicsEngine(update_rate_hz=100.0) @pytest.fixture def chamber(self, engine: PhysicsEngine) -> ThermalChamberSim: """Create chamber instance with physics engine.""" return ThermalChamberSim(physics_engine=engine) def test_setpoint_updates_engine( self, chamber: ThermalChamberSim, engine: PhysicsEngine ) -> None: """Test TEMP:SETPOINT updates physics engine.""" chamber.process("TEMP:SETPOINT 85.0") # Step the engine and check thermal state thermal = engine.get_thermal_state() # Initial chamber temp is 25, will start moving towards 85 assert thermal.chamber_temperature == pytest.approx(25.0, abs=0.1) # After many steps, should approach setpoint for _ in range(10000): # 100 seconds at 100Hz engine.step() thermal = engine.get_thermal_state() # Should be closer to setpoint (but not quite there due to time constant) assert thermal.chamber_temperature > 80.0 def test_actual_query_returns_engine_temperature( self, chamber: ThermalChamberSim, engine: PhysicsEngine ) -> None: """Test TEMP:ACTUAL? returns physics engine temperature.""" response = chamber.process("TEMP:ACTUAL?") # Should match initial chamber temperature assert response == "25.00" def test_stability_when_at_setpoint( self, chamber: ThermalChamberSim, engine: PhysicsEngine ) -> None: """Test TEMP:STAB? returns 1 when at setpoint.""" # Default setpoint is 25, engine starts at 25 response = chamber.process("TEMP:STAB?") assert response == "1" def test_stability_when_settling( self, chamber: ThermalChamberSim, engine: PhysicsEngine ) -> None: """Test TEMP:STAB? returns 0 when settling.""" # Set new setpoint far from current temperature chamber.process("TEMP:SETPOINT 85.0") # Step once to ensure engine updates engine.step() # Should not be stable yet response = chamber.process("TEMP:STAB?") assert response == "0" def test_reset_updates_engine( self, chamber: ThermalChamberSim, engine: PhysicsEngine ) -> None: """Test *RST resets both chamber and engine setpoint.""" chamber.process("TEMP:SETPOINT 85.0") chamber.process("*RST") # Check setpoint is back to default assert chamber.process("TEMP:SETPOINT?") == "25.00"