ics-simlab-config-gen-claude/tests/test_e2e_bottle_control_plan.py

221 lines
8.1 KiB
Python

"""
Integration tests for E2E Bottle Line ControlPlan scenario.
These tests verify:
1. The example control plan compiles to valid Python
2. Generated HIL files have correct structure
3. Validation mode passes
4. Generated code can be parsed by Python's ast module
No Docker or external services required.
"""
import ast
import tempfile
from pathlib import Path
import pytest
from models.control_plan import ControlPlan
from tools.compile_control_plan import compile_control_plan, validate_control_plan
# Path to the example control plan
EXAMPLE_CONTROL_PLAN = Path(__file__).parent.parent / "examples" / "control_plans" / "bottle_line_v0.1.json"
class TestBottleLineControlPlan:
"""Test the bottle_line_v0.1.json control plan."""
def test_example_exists(self):
"""Verify the example control plan file exists."""
assert EXAMPLE_CONTROL_PLAN.exists(), f"Example not found: {EXAMPLE_CONTROL_PLAN}"
def test_loads_as_valid_control_plan(self):
"""Verify the example loads as a valid ControlPlan."""
import json
data = json.loads(EXAMPLE_CONTROL_PLAN.read_text(encoding="utf-8"))
plan = ControlPlan.model_validate(data)
assert plan.version == "v0.1"
assert len(plan.hils) == 2
assert plan.hils[0].name == "water_hil"
assert plan.hils[1].name == "filler_hil"
def test_validation_passes(self):
"""Verify validation passes with no errors."""
import json
data = json.loads(EXAMPLE_CONTROL_PLAN.read_text(encoding="utf-8"))
plan = ControlPlan.model_validate(data)
errors = validate_control_plan(plan)
assert errors == [], f"Validation errors: {errors}"
def test_compiles_to_valid_python(self):
"""Verify compilation produces syntactically valid Python."""
import json
data = json.loads(EXAMPLE_CONTROL_PLAN.read_text(encoding="utf-8"))
plan = ControlPlan.model_validate(data)
result = compile_control_plan(plan)
# Should have 2 HIL files
assert len(result) == 2
assert "water_hil" in result
assert "filler_hil" in result
# Each should be valid Python
for hil_name, code in result.items():
try:
ast.parse(code)
except SyntaxError as e:
pytest.fail(f"Syntax error in {hil_name}: {e}")
def test_generated_code_has_logic_function(self):
"""Verify generated code contains logic(physical_values) function."""
import json
data = json.loads(EXAMPLE_CONTROL_PLAN.read_text(encoding="utf-8"))
plan = ControlPlan.model_validate(data)
result = compile_control_plan(plan)
for hil_name, code in result.items():
assert "def logic(physical_values):" in code, \
f"{hil_name} missing logic(physical_values)"
def test_generated_code_has_while_true(self):
"""Verify generated code contains while True loop."""
import json
data = json.loads(EXAMPLE_CONTROL_PLAN.read_text(encoding="utf-8"))
plan = ControlPlan.model_validate(data)
result = compile_control_plan(plan)
for hil_name, code in result.items():
assert "while True:" in code, \
f"{hil_name} missing while True loop"
def test_generated_code_has_time_sleep(self):
"""Verify generated code contains time.sleep calls."""
import json
data = json.loads(EXAMPLE_CONTROL_PLAN.read_text(encoding="utf-8"))
plan = ControlPlan.model_validate(data)
result = compile_control_plan(plan)
for hil_name, code in result.items():
assert "time.sleep(" in code, \
f"{hil_name} missing time.sleep"
def test_warmup_delay_included(self):
"""Verify warmup delay is included in generated code."""
import json
data = json.loads(EXAMPLE_CONTROL_PLAN.read_text(encoding="utf-8"))
plan = ControlPlan.model_validate(data)
result = compile_control_plan(plan)
# Both HILs have warmup_s: 3.0
for hil_name, code in result.items():
assert "time.sleep(3.0)" in code, \
f"{hil_name} missing warmup delay"
def test_writes_to_temp_directory(self):
"""Verify compiled files can be written to disk."""
import json
data = json.loads(EXAMPLE_CONTROL_PLAN.read_text(encoding="utf-8"))
plan = ControlPlan.model_validate(data)
result = compile_control_plan(plan)
with tempfile.TemporaryDirectory() as tmpdir:
tmppath = Path(tmpdir)
for hil_name, code in result.items():
out_file = tmppath / f"{hil_name}.py"
out_file.write_text(code, encoding="utf-8")
assert out_file.exists()
# Read back and verify
content = out_file.read_text(encoding="utf-8")
assert "def logic(physical_values):" in content
def test_water_hil_initializes_tank_level(self):
"""Verify water_hil initializes water_tank_level."""
import json
data = json.loads(EXAMPLE_CONTROL_PLAN.read_text(encoding="utf-8"))
plan = ControlPlan.model_validate(data)
result = compile_control_plan(plan)
water_code = result["water_hil"]
assert "pv['water_tank_level'] = 500" in water_code, \
"water_hil should initialize water_tank_level to 500"
def test_filler_hil_initializes_bottle_fill_level(self):
"""Verify filler_hil initializes bottle_fill_level."""
import json
data = json.loads(EXAMPLE_CONTROL_PLAN.read_text(encoding="utf-8"))
plan = ControlPlan.model_validate(data)
result = compile_control_plan(plan)
filler_code = result["filler_hil"]
assert "pv['bottle_fill_level'] = 0" in filler_code, \
"filler_hil should initialize bottle_fill_level to 0"
def test_clamp_function_included(self):
"""Verify clamp helper function is included."""
import json
data = json.loads(EXAMPLE_CONTROL_PLAN.read_text(encoding="utf-8"))
plan = ControlPlan.model_validate(data)
result = compile_control_plan(plan)
for hil_name, code in result.items():
assert "def clamp(x, lo, hi):" in code, \
f"{hil_name} missing clamp function"
class TestPromptFileExists:
"""Test that the e2e prompt file exists."""
def test_e2e_bottle_prompt_exists(self):
"""Verify prompts/e2e_bottle.txt exists."""
prompt_file = Path(__file__).parent.parent / "prompts" / "e2e_bottle.txt"
assert prompt_file.exists(), f"Prompt not found: {prompt_file}"
def test_e2e_bottle_prompt_has_content(self):
"""Verify prompt file has meaningful content."""
prompt_file = Path(__file__).parent.parent / "prompts" / "e2e_bottle.txt"
content = prompt_file.read_text(encoding="utf-8")
# Should mention the two HILs
assert "water_hil" in content
assert "filler_hil" in content
# Should mention the two PLCs
assert "plc1" in content.lower() or "PLC1" in content
assert "plc2" in content.lower() or "PLC2" in content
class TestE2EScriptExists:
"""Test that the E2E script exists and is executable."""
def test_e2e_script_exists(self):
"""Verify scripts/e2e_bottle_control_plan.sh exists."""
script_file = Path(__file__).parent.parent / "scripts" / "e2e_bottle_control_plan.sh"
assert script_file.exists(), f"Script not found: {script_file}"
def test_e2e_script_is_executable(self):
"""Verify script has executable permission."""
import os
script_file = Path(__file__).parent.parent / "scripts" / "e2e_bottle_control_plan.sh"
assert os.access(script_file, os.X_OK), f"Script not executable: {script_file}"
def test_e2e_script_has_shebang(self):
"""Verify script starts with proper shebang."""
script_file = Path(__file__).parent.parent / "scripts" / "e2e_bottle_control_plan.sh"
content = script_file.read_text(encoding="utf-8")
assert content.startswith("#!/bin/bash"), "Script missing bash shebang"