cmvr_edge_ai/tests/integration/test_application_cli.py

178 lines
6.2 KiB
Python
Raw Permalink Normal View History

2026-07-20 16:59:37 +08:00
from __future__ import annotations
import asyncio
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from cmvr_edge_ai.application import (
ApplicationState,
EdgeAIApplication,
create_default_registry,
validate_application,
)
from cmvr_edge_ai.cli import main
from cmvr_edge_ai.config import load_config
PROJECT_ROOT = Path(__file__).resolve().parents[2]
2026-07-21 12:07:12 +08:00
MINIMAL_CONFIG = PROJECT_ROOT / "tests" / "fixtures" / "minimal_pipeline.yaml"
ACTIVE_DETECTION_CONFIG = PROJECT_ROOT / "configs" / "active_detection.yaml"
2026-07-20 16:59:37 +08:00
2026-07-21 12:07:12 +08:00
def test_application_runs_the_finite_minimal_pipeline() -> None:
2026-07-20 16:59:37 +08:00
async def exercise(): # type: ignore[no-untyped-def]
loop = asyncio.get_running_loop()
host_executor = ThreadPoolExecutor(max_workers=1)
loop.set_default_executor(host_executor)
application = EdgeAIApplication(
2026-07-21 12:07:12 +08:00
load_config(MINIMAL_CONFIG),
2026-07-20 16:59:37 +08:00
create_default_registry(discover_entry_points=False),
)
try:
await application.start()
running_health = await application.health()
await application.wait()
await application.stop()
# The application owns only its bounded executor. Stopping it must
# not poison an embedding event loop's pre-existing executor.
host_executor_result = await asyncio.to_thread(lambda: "still-usable")
return application, running_health, host_executor_result
finally:
# Python 3.12 Runner shuts its default executor down from another
# helper thread. Explicit cleanup keeps this regression deterministic
# in restricted containers where that helper cannot wake the loop.
host_executor.shutdown(wait=True, cancel_futures=True)
loop._default_executor = None # type: ignore[attr-defined]
application, running_health, host_executor_result = asyncio.run(exercise())
assert running_health["state"] == "running"
2026-07-21 12:07:12 +08:00
assert set(running_health["pipelines"]) == {"minimal"}
assert application.pipelines == ("minimal",)
2026-07-20 16:59:37 +08:00
assert application.state is ApplicationState.STOPPED
assert host_executor_result == "still-usable"
2026-07-21 12:07:12 +08:00
def test_cli_validates_and_runs_minimal_config(
2026-07-20 16:59:37 +08:00
capsys, monkeypatch
) -> None: # type: ignore[no-untyped-def]
# ``configure_logging(force=True)`` intentionally owns process logging in
# production. Avoid leaking that global CLI side effect into later tests.
monkeypatch.setattr("cmvr_edge_ai.cli.configure_logging", lambda *_: None)
2026-07-21 12:07:12 +08:00
assert main(["validate", "--config", str(MINIMAL_CONFIG)]) == 0
2026-07-20 16:59:37 +08:00
validation_output = capsys.readouterr()
2026-07-21 12:07:12 +08:00
assert "configuration is valid; pipelines: minimal" in validation_output.out
2026-07-20 16:59:37 +08:00
assert validation_output.err == ""
2026-07-21 12:07:12 +08:00
assert (
main(
[
"run",
"--config",
str(MINIMAL_CONFIG),
"--log-level",
"WARNING",
]
)
== 0
)
2026-07-20 16:59:37 +08:00
run_output = capsys.readouterr()
assert run_output.err == ""
def test_cli_lists_versioned_builtin_plugins(capsys) -> None: # type: ignore[no-untyped-def]
assert main(["plugins"]) == 0
output = capsys.readouterr().out
assert "core.sequence_source@1\tsource" in output
assert "detection.model@1\toperator" in output
assert "media.video_decoder.pyav@1\toperator" in output
assert "safety.robot_command_gate@1\toperator" in output
def test_cli_lists_detection_model_metadata(capsys) -> None: # type: ignore[no-untyped-def]
assert main(["models"]) == 0
output = capsys.readouterr().out
assert (
"construction-ppe-yolov8@1\tConstruction PPE YOLOv8s\t" "ultralytics-yolo"
) in output
assert "No-Helmet" in output
assert "No-Vest" in output
assert (
"ppe-6classes-yolov8n@1\tPPE Detection YOLOv8n (6 Classes)\t"
"ultralytics-yolo"
) in output
assert "Gloves,Vest,goggles,helmet,mask,safety_shoe" in output
2026-07-21 16:23:33 +08:00
assert (
"people-talking-yolov8x@1\tPeople Talking YOLOv8x\t"
"ultralytics-yolo\tlabel,talking on phone"
) in output
assert (
"yolov8n-mobile-phone@1\tYOLOv8n Mobile Phone\t"
"ultralytics-yolo\tmobile_phone"
) in output
assert (
"construction-ppe-yolov8@2\tConstruction PPE YOLOv8s ONNX\t"
"onnxruntime-yolov8"
) in output
assert (
"ppe-6classes-yolov8n@2\tPPE Detection YOLOv8n (6 Classes) ONNX\t"
"onnxruntime-yolov8"
) in output
assert (
"people-talking-yolov8x@2\tPeople Talking YOLOv8x ONNX\t"
"onnxruntime-yolov8\tlabel,talking on phone"
) in output
assert (
"yolov8n-mobile-phone@2\tYOLOv8n Mobile Phone ONNX\t"
"onnxruntime-yolov8\tmobile_phone"
) in output
2026-07-20 16:59:37 +08:00
def test_active_detection_config_compiles_without_loading_optional_runtimes() -> None:
config = load_config(ACTIVE_DETECTION_CONFIG)
2026-07-21 12:07:12 +08:00
registry = create_default_registry(discover_entry_points=False)
2026-07-20 16:59:37 +08:00
compiled = validate_application(
config,
2026-07-21 12:07:12 +08:00
registry,
("detection",),
2026-07-20 16:59:37 +08:00
)
assert set(config.pipelines) == {"detection"}
2026-07-20 16:59:37 +08:00
assert len(compiled) == 1
2026-07-21 12:07:12 +08:00
assert compiled[0].pipeline_id == "detection"
assert "ppe_alert_platform" in config.endpoints
assert (
config.pipelines["detection"]
.nodes["alert_platform"]
.params["endpoint"]
== "ppe_alert_platform"
)
2026-07-20 16:59:37 +08:00
assert (
2026-07-21 12:07:12 +08:00
config.pipelines["detection"]
.nodes["alert_platform"]
.params["failure_mode"]
2026-07-20 16:59:37 +08:00
== "log_and_drop"
)
assert (
config.pipelines["detection"]
.nodes["alert_platform"]
.params["grpc_endpoint"]
== "cmvr_es"
)
2026-07-21 16:23:33 +08:00
phone_detector = config.pipelines["detection"].nodes["phone_detector"]
assert phone_detector.params["model"] == "people-talking-yolov8x@2"
2026-07-21 16:23:33 +08:00
assert phone_detector.params["detect_labels"] == ["talking on phone"]
phone_gate = config.pipelines["detection"].nodes["phone_repeat_gate"]
assert phone_gate.params["rules"][0]["labels"] == ["talking on phone"]
2026-07-20 16:59:37 +08:00
assert set(compiled[0].plugin_specs) == {
"camera",
"decoder",
"detector",
"repeat_gate",
2026-07-21 16:23:33 +08:00
"phone_detector",
"phone_repeat_gate",
2026-07-21 12:07:12 +08:00
"alert_platform",
2026-07-20 16:59:37 +08:00
}