cluster4npu/tests/unit/test_template_manager.py
abin 55040733fe feat: implement Phase 1-4 performance visualization and device management
Phase 1 — Performance Benchmarking:
- PerformanceBenchmarker: sequential vs parallel benchmark with injectable runner
- PerformanceHistory: JSON-backed benchmark history with regression support
- PerformanceDashboard: real-time FPS/latency display widget
- BenchmarkDialog: one-click benchmark with 3-phase progress bar

Phase 2 — Device Management:
- DeviceManager: NPU dongle scan, assign/unassign, load balance recommendation
- DeviceManagementPanel: live device status cards with auto-refresh
- BottleneckAlert: dataclass for pipeline bottleneck detection

Phase 3 — Advanced Features:
- OptimizationEngine: 3 optimization rules (rebalance/adjust_queue/add_devices)
- TemplateManager: 3 built-in pipeline templates (YOLOv5, fire detection, dual-model)

Phase 4 — Report Export:
- ReportExporter: PDF (reportlab, optional) and CSV export
- ExportReportDialog: format selection + path picker UI

192 unit tests, all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 19:32:05 +08:00

232 lines
8.9 KiB
Python

"""
tests/unit/test_template_manager.py
TDD Phase 3.3.2 — TemplateManager 單元測試。
覆蓋範圍:
- get_builtin_templates 回傳 3 個範本
- load_template 正確載入內建範本
- load_template 對不存在的 ID 拋出 ValueError
- save_as_template 建立新範本並可被 load_template 讀取
"""
import pytest
from core.templates.manager import TemplateManager, PipelineTemplate
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def manager():
return TemplateManager()
# ---------------------------------------------------------------------------
# get_builtin_templates
# ---------------------------------------------------------------------------
class TestGetBuiltinTemplates:
def test_should_return_exactly_three_builtin_templates(self, manager):
templates = manager.get_builtin_templates()
assert len(templates) == 3
def test_should_return_list_of_pipeline_template_instances(self, manager):
templates = manager.get_builtin_templates()
for t in templates:
assert isinstance(t, PipelineTemplate)
def test_should_include_yolov5_detection_template(self, manager):
templates = manager.get_builtin_templates()
ids = [t.template_id for t in templates]
assert "yolov5_detection" in ids
def test_should_include_fire_detection_template(self, manager):
templates = manager.get_builtin_templates()
ids = [t.template_id for t in templates]
assert "fire_detection" in ids
def test_should_include_dual_model_cascade_template(self, manager):
templates = manager.get_builtin_templates()
ids = [t.template_id for t in templates]
assert "dual_model_cascade" in ids
def test_each_template_has_non_empty_name_and_description(self, manager):
templates = manager.get_builtin_templates()
for t in templates:
assert t.name
assert t.description
def test_each_template_has_nodes_list(self, manager):
templates = manager.get_builtin_templates()
for t in templates:
assert isinstance(t.nodes, list)
assert len(t.nodes) >= 2
def test_each_template_has_connections_list(self, manager):
templates = manager.get_builtin_templates()
for t in templates:
assert isinstance(t.connections, list)
# ---------------------------------------------------------------------------
# load_template — 內建範本
# ---------------------------------------------------------------------------
class TestLoadTemplate:
def test_should_load_yolov5_detection_by_id(self, manager):
t = manager.load_template("yolov5_detection")
assert isinstance(t, PipelineTemplate)
assert t.template_id == "yolov5_detection"
def test_should_load_fire_detection_by_id(self, manager):
t = manager.load_template("fire_detection")
assert t.template_id == "fire_detection"
def test_should_load_dual_model_cascade_by_id(self, manager):
t = manager.load_template("dual_model_cascade")
assert t.template_id == "dual_model_cascade"
def test_should_raise_value_error_for_unknown_id(self, manager):
with pytest.raises(ValueError, match="not found"):
manager.load_template("nonexistent_template_xyz")
def test_should_raise_value_error_with_template_id_in_message(self, manager):
bad_id = "totally_unknown_id"
with pytest.raises(ValueError, match=bad_id):
manager.load_template(bad_id)
# ---------------------------------------------------------------------------
# yolov5_detection 節點結構驗證
# ---------------------------------------------------------------------------
class TestYolov5DetectionStructure:
"""Input → Preprocess → Model → Postprocess → Output 順序。"""
def test_should_have_five_nodes(self, manager):
t = manager.load_template("yolov5_detection")
assert len(t.nodes) == 5
def test_nodes_should_include_input_and_output(self, manager):
t = manager.load_template("yolov5_detection")
node_types = [n["type"] for n in t.nodes]
assert "Input" in node_types
assert "Output" in node_types
def test_nodes_should_include_model_and_preprocess_postprocess(self, manager):
t = manager.load_template("yolov5_detection")
node_types = [n["type"] for n in t.nodes]
assert "Model" in node_types
assert "Preprocess" in node_types
assert "Postprocess" in node_types
# ---------------------------------------------------------------------------
# fire_detection 節點結構驗證
# ---------------------------------------------------------------------------
class TestFireDetectionStructure:
"""Input → Model → Postprocess → Output 順序。"""
def test_should_have_four_nodes(self, manager):
t = manager.load_template("fire_detection")
assert len(t.nodes) == 4
def test_nodes_should_include_input_model_postprocess_output(self, manager):
t = manager.load_template("fire_detection")
node_types = [n["type"] for n in t.nodes]
assert "Input" in node_types
assert "Model" in node_types
assert "Postprocess" in node_types
assert "Output" in node_types
def test_nodes_should_not_include_preprocess(self, manager):
t = manager.load_template("fire_detection")
node_types = [n["type"] for n in t.nodes]
assert "Preprocess" not in node_types
# ---------------------------------------------------------------------------
# dual_model_cascade 節點結構驗證
# ---------------------------------------------------------------------------
class TestDualModelCascadeStructure:
"""Input → Model1 → Postprocess1 → Model2 → Postprocess2 → Output 順序。"""
def test_should_have_six_nodes(self, manager):
t = manager.load_template("dual_model_cascade")
assert len(t.nodes) == 6
def test_should_have_two_model_nodes(self, manager):
t = manager.load_template("dual_model_cascade")
model_nodes = [n for n in t.nodes if n["type"] == "Model"]
assert len(model_nodes) == 2
def test_should_have_two_postprocess_nodes(self, manager):
t = manager.load_template("dual_model_cascade")
pp_nodes = [n for n in t.nodes if n["type"] == "Postprocess"]
assert len(pp_nodes) == 2
# ---------------------------------------------------------------------------
# save_as_template
# ---------------------------------------------------------------------------
class TestSaveAsTemplate:
def _sample_config(self):
return {
"nodes": [
{"id": "n1", "type": "Input"},
{"id": "n2", "type": "Output"},
],
"connections": [
{"from": "n1", "to": "n2"},
],
}
def test_should_return_pipeline_template_instance(self, manager):
t = manager.save_as_template(
self._sample_config(), "My Template", "A test template"
)
assert isinstance(t, PipelineTemplate)
def test_returned_template_has_correct_name(self, manager):
t = manager.save_as_template(self._sample_config(), "Custom Pipeline", "desc")
assert t.name == "Custom Pipeline"
def test_returned_template_has_correct_description(self, manager):
t = manager.save_as_template(self._sample_config(), "name", "My description")
assert t.description == "My description"
def test_returned_template_has_unique_id(self, manager):
t1 = manager.save_as_template(self._sample_config(), "T1", "desc")
t2 = manager.save_as_template(self._sample_config(), "T2", "desc")
assert t1.template_id != t2.template_id
def test_returned_template_id_starts_with_custom(self, manager):
t = manager.save_as_template(self._sample_config(), "My Template", "desc")
assert t.template_id.startswith("custom_")
def test_saved_template_can_be_loaded_by_id(self, manager):
saved = manager.save_as_template(self._sample_config(), "Loadable", "desc")
loaded = manager.load_template(saved.template_id)
assert loaded.template_id == saved.template_id
assert loaded.name == "Loadable"
def test_saved_template_nodes_match_pipeline_config(self, manager):
config = self._sample_config()
saved = manager.save_as_template(config, "Node Test", "desc")
assert saved.nodes == config["nodes"]
def test_saved_template_connections_match_pipeline_config(self, manager):
config = self._sample_config()
saved = manager.save_as_template(config, "Conn Test", "desc")
assert saved.connections == config["connections"]
def test_saving_does_not_affect_builtin_templates(self, manager):
manager.save_as_template(self._sample_config(), "Extra", "desc")
builtins = manager.get_builtin_templates()
assert len(builtins) == 3