221 lines
8.1 KiB
Python
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"
|