Tests for ThermalChamberSim SCPI command responses: - Basic IEEE 488.2 commands (*IDN?, *RST, *OPC?) - TEMP:SETPOINT set/query - TEMP:ACTUAL? query - TEMP:STAB? stability query - Physics engine integration tests
216 lines
7.5 KiB
Python
216 lines
7.5 KiB
Python
"""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"
|