forked from masonhuang/cluster4npu
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>
232 lines
8.9 KiB
Python
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
|