""" 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