From 22614d6f5589c0e1553d6c054f4b7f9ef261bda8 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Tue, 3 Mar 2026 01:25:36 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=207=20=E4=B8=AA?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E6=B5=8B=E8=AF=95=E5=92=8C=201=20=E4=B8=AA?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 conftest_pptx.py 中元素类型检测:使用 has_text_frame 替代不存在的 MSO_SHAPE.TEXT_BOX - 修复 test_presentation.py 中 3 个测试:使用对象属性访问替代字典访问 - 修复 unit/test_presentation.py 中路径比较问题 - 添加缺失的 mock_template_class fixture 测试通过率: 316/316 (100%) 代码覆盖率: 94% --- .../.openspec.yaml | 2 + .../2026-03-03-fix-failing-tests/design.md | 111 ++++++++++++++++++ .../2026-03-03-fix-failing-tests/proposal.md | 51 ++++++++ .../specs/SPEC.md | 33 ++++++ .../2026-03-03-fix-failing-tests/tasks.md | 25 ++++ tests/conftest.py | 23 +++- tests/conftest_pptx.py | 6 +- tests/integration/test_presentation.py | 10 +- tests/unit/test_presentation.py | 67 ++++++----- 9 files changed, 284 insertions(+), 44 deletions(-) create mode 100644 openspec/changes/archive/2026-03-03-fix-failing-tests/.openspec.yaml create mode 100644 openspec/changes/archive/2026-03-03-fix-failing-tests/design.md create mode 100644 openspec/changes/archive/2026-03-03-fix-failing-tests/proposal.md create mode 100644 openspec/changes/archive/2026-03-03-fix-failing-tests/specs/SPEC.md create mode 100644 openspec/changes/archive/2026-03-03-fix-failing-tests/tasks.md diff --git a/openspec/changes/archive/2026-03-03-fix-failing-tests/.openspec.yaml b/openspec/changes/archive/2026-03-03-fix-failing-tests/.openspec.yaml new file mode 100644 index 0000000..fd79bfc --- /dev/null +++ b/openspec/changes/archive/2026-03-03-fix-failing-tests/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-03-02 diff --git a/openspec/changes/archive/2026-03-03-fix-failing-tests/design.md b/openspec/changes/archive/2026-03-03-fix-failing-tests/design.md new file mode 100644 index 0000000..0d85eb6 --- /dev/null +++ b/openspec/changes/archive/2026-03-03-fix-failing-tests/design.md @@ -0,0 +1,111 @@ +# 设计文档:修复失败的测试 + +## Context + +当前项目的测试套件包含 316 个测试,其中 7 个失败,1 个错误。主要问题分为四类: + +1. **API 不匹配问题**:测试假设 `render_slide` 返回字典列表,但实际实现返回元素对象列表(dataclass 实例) + +2. **python-pptx 枚举问题**:`MSO_SHAPE.TEXT_BOX` 在当前 python-pptx 版本中不存在,导致元素类型检测失败 + +3. **Windows 路径问题**:字符串路径与 Path 对象混合比较导致断言失败 + +4. **Fixture 缺失**:`mock_template_class` fixture 未定义 + +项目约束: +- 必须保持 100% 测试通过率 +- 修复不能破坏现有功能 +- 需要保持代码覆盖率 >= 94% + +## Goals / Non-Goals + +**Goals:** +- 修复所有 7 个失败测试和 1 个错误测试 +- 确保修复后测试仍能有效验证功能正确性 +- 改进测试辅助函数的兼容性 + +**Non-Goals:** +- 不修改生产代码的功能逻辑 +- 不添加新功能 +- 不降低代码覆盖率 + +## Decisions + +### D1: 处理 API 不匹配(render_slide 返回值) + +**选项 A**: 修改 `render_slide` 方法,返回字典而非对象 +- 优点:保持测试代码不变 +- 缺点:需要修改 `pptx_renderer.py` 和 `html_renderer.py` 的调用方式 + +**选项 B**: 修改测试代码,使用对象属性访问 +- 优点:保持生产代码不变,测试更符合实际使用场景 +- 缺点:需要修改多个测试文件 + +**选择:B** - 修改测试代码,因为元素对象提供了更好的类型安全和 IDE 支持 + +### D2: 处理 MSO_SHAPE.TEXT_BOX 枚举不存在 + +**选项 A**: 使用 `shape.has_text_frame` 属性检测文本框 +- 优点:跨版本兼容 +- 缺点:需要改变检测逻辑 + +**选项 B**: 检查 `shape.shape_type` 是否等于 `MSO_SHAPE.AUTO_SHAPE_TYPE` (枚举值 1) +- 优点:简单直接 +- 缺点:可能不准确 + +**选项 C**: 改用 `isinstance(shape, pptx.shapes.text_frame.TextFrameProxy)` 或类似方法 +- 优点:准确 +- 缺点:需要了解内部实现 + +**选择:A** - 使用 `has_text_frame` 属性,这是检测形状是否有文本框的标准方法 + +### D3: 处理 Windows 路径比较 + +**选项 A**: 在测试中使用 `str()` 转换 Path 对象 +- 优点:简单 +- 缺点:可能引入平台特定代码 + +**选项 B**: 在 `Presentation` 类中标准化路径存储 +- 优点:一劳永逸 +- 缺点:影响较大 + +**选择:A** - 在测试中转换类型,因为这是测试代码问题,不是生产代码问题 + +### D4: 处理缺失的 Fixture + +**选项 A**: 添加 `mock_template_class` fixture +- 优点:保持测试完整 +- 缺点:需要了解 mock 的使用方式 + +**选项 B**: 删除该测试(如果功能已被其他测试覆盖) +- 优点:减少维护负担 +- 缺点:可能降低覆盖度 + +**选择:A** - 添加 fixture,因为该测试验证的功能未被其他测试覆盖 + +## Risks / Trade-offs + +| 风险 | 描述 | 缓解措施 | +|------|------|----------| +| 修复后功能回归 | 修改测试可能隐藏真正的 bug | 修复后运行完整测试套件,确认所有测试通过 | +| 版本兼容性 | 不同 python-pptx 版本可能有不同行为 | 使用跨版本兼容的 API(如 `has_text_frame`) | +| 测试脆弱性 | 测试与实现耦合过紧 | 使用公共 API,减少内部实现细节依赖 | + +## Migration Plan + +1. 备份当前测试代码 +2. 修改 `tests/conftest_pptx.py` 中的 `count_elements_by_type` 方法 +3. 修改 `tests/integration/test_presentation.py` 中的测试 +4. 修改 `tests/integration/test_rendering_flow.py` 中的测试 +5. 修改 `tests/unit/test_presentation.py` 中的测试 +6. 添加缺失的 fixture +7. 运行完整测试套件验证修复 +8. 确保代码覆盖率不下降 + +## Open Questions + +**Q1**: 是否需要为元素类型检测添加版本兼容性检查? +- 当前选择使用 `has_text_frame` 属性,兼容性较好 + +**Q2**: 是否应该将测试辅助函数移至独立的工具模块? +- 当前保持现状,避免引入不必要的重构 diff --git a/openspec/changes/archive/2026-03-03-fix-failing-tests/proposal.md b/openspec/changes/archive/2026-03-03-fix-failing-tests/proposal.md new file mode 100644 index 0000000..19b98a5 --- /dev/null +++ b/openspec/changes/archive/2026-03-03-fix-failing-tests/proposal.md @@ -0,0 +1,51 @@ +# 修复失败的测试 + +## Why + +当前项目有 7 个测试失败和 1 个测试错误,导致测试通过率从 100% 降至 97.5%。这些问题主要是由于: +1. 测试代码与实际 API 行为不一致 +2. python-pptx 库版本变化导致的 API 差异 +3. Windows 平台路径处理差异 +4. 测试 fixture 定义缺失 + +如果不修复这些问题,将影响持续集成的可靠性,并阻碍后续功能开发。 + +## What Changes + +### 测试修复 +- 修复 `test_render_slide_with_template` - 调整测试以适应返回的元素对象 +- 修复 `test_render_slide_with_variables` - 调整测试以适应返回的元素对象 +- 修复 `test_render_direct_elements` - 调整测试以适应返回的元素对象 +- 修复 `test_image_element_rendering` - 使用正确的枚举或替代方法检测文本框 +- 修复 `test_shape_element_rendering` - 使用正确的枚举或替代方法检测形状 +- 修复 `test_table_element_rendering` - 使用正确的枚举或替代方法检测表格 +- 修复 `test_init_with_templates_dir` - 统一路径类型比较 +- 修复 `test_render_slide_with_template_merges_background` - 添加缺失的 fixture + +### 代码改进 +- 在 `tests/conftest_pptx.py` 中改进元素类型检测逻辑 +- 添加更健壮的 python-pptx 版本兼容性处理 + +## Capabilities + +### New Capabilities +- `test-fix-framework`: 建立测试修复的标准框架,确保未来 API 变更时测试能同步更新 + +### Modified Capabilities +- 无(现有功能的测试修复,不改变功能需求) + +## Impact + +### 受影响代码 +- `tests/integration/test_presentation.py` - 3 个失败测试 +- `tests/integration/test_rendering_flow.py` - 3 个失败测试 +- `tests/unit/test_presentation.py` - 1 个失败测试 + 1 个错误 +- `tests/conftest_pptx.py` - 辅助验证函数 + +### 测试影响 +- 修复后测试通过率: 100% (316/316) +- 代码覆盖率: 维持 94% + +### 依赖影响 +- 无新增依赖 +- 保持与 python-pptx 现有版本的兼容性 diff --git a/openspec/changes/archive/2026-03-03-fix-failing-tests/specs/SPEC.md b/openspec/changes/archive/2026-03-03-fix-failing-tests/specs/SPEC.md new file mode 100644 index 0000000..06f75c4 --- /dev/null +++ b/openspec/changes/archive/2026-03-03-fix-failing-tests/specs/SPEC.md @@ -0,0 +1,33 @@ +# 测试修复规格说明 + +本变更是对现有测试代码的修复,不引入新功能,也不修改现有功能的行为。 + +## 不适用说明 + +此变更不涉及: +- 新功能需求 +- 现有功能需求的修改 +- 新增 API 或接口 + +因此,不需要创建新的规格说明文件。 + +所有需要修复的测试已经在现有测试套件中定义,修复的是测试代码与实现之间的不一致,而非功能需求的变化。 + +## 验证方式 + +修复完成后,运行以下命令验证: + +```bash +# 运行所有测试 +uv run pytest -v + +# 验证测试通过率 +uv run pytest --tb=short + +# 检查代码覆盖率 +uv run pytest --cov --cov-report=term-missing +``` + +预期结果: +- 所有 316 个测试通过 +- 代码覆盖率 >= 94% diff --git a/openspec/changes/archive/2026-03-03-fix-failing-tests/tasks.md b/openspec/changes/archive/2026-03-03-fix-failing-tests/tasks.md new file mode 100644 index 0000000..c4b5524 --- /dev/null +++ b/openspec/changes/archive/2026-03-03-fix-failing-tests/tasks.md @@ -0,0 +1,25 @@ +# 任务清单:修复失败的测试 + +## 1. 修复 conftest_pptx.py 中的元素类型检测 + +- [x] 1.1 修复 `count_elements_by_type` 方法:将 `MSO_SHAPE.TEXT_BOX` 替换为使用 `shape.has_text_frame` 属性检测文本框 +- [x] 1.2 运行修复后的测试验证 `test_image_element_rendering` +- [x] 1.3 运行修复后的测试验证 `test_shape_element_rendering` +- [x] 1.4 运行修复后的测试验证 `test_table_element_rendering` + +## 2. 修复 integration/test_presentation.py 中的测试 + +- [x] 2.1 修复 `test_render_slide_with_template`:使用 `element.type` 和 `element.content` 属性访问,而非字典的 `get` 方法 +- [x] 2.2 修复 `test_render_slide_with_variables`:同上 +- [x] 2.3 修复 `test_render_direct_elements`:使用对象属性访问替换字典访问 + +## 3. 修复 unit/test_presentation.py 中的测试 + +- [x] 3.1 修复 `test_init_with_templates_dir`:将断言中的字符串转换为 Path 对象后比较 +- [x] 3.2 添加缺失的 `mock_template_class` fixture 到 conftest.py + +## 4. 验证和回归测试 + +- [x] 4.1 运行完整测试套件,确认所有 316 个测试通过 +- [x] 4.2 运行代码覆盖率检查,确认覆盖率 >= 94% +- [x] 4.3 运行 e2e 测试验证整体功能未受影响 diff --git a/tests/conftest.py b/tests/conftest.py index 6877d7c..8c3108f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,7 @@ sys.path.insert(0, str(project_root)) # ============= 基础 Fixtures ============= + @pytest.fixture def temp_dir(tmp_path): """临时目录 fixture,使用 pytest 内置 tmp_path""" @@ -50,19 +51,20 @@ slides: def sample_yaml(temp_dir): """创建最小测试 YAML 文件""" yaml_path = temp_dir / "test.yaml" - yaml_path.write_text(MINIMAL_YAML, encoding='utf-8') + yaml_path.write_text(MINIMAL_YAML, encoding="utf-8") return yaml_path # ============= 图片 Fixtures ============= + @pytest.fixture def sample_image(temp_dir): """创建测试图片文件(使用 Pillow 生成简单的 PNG)""" img_path = temp_dir / "test_image.png" # 创建一个简单的红色图片 - img = Image.new('RGB', (100, 100), color='red') - img.save(img_path, 'PNG') + img = Image.new("RGB", (100, 100), color="red") + img.save(img_path, "PNG") return img_path @@ -99,21 +101,33 @@ def sample_template(temp_dir): """创建测试模板目录和文件""" template_dir = temp_dir / "templates" template_dir.mkdir() - (template_dir / "title-slide.yaml").write_text(TEMPLATE_YAML, encoding='utf-8') + (template_dir / "title-slide.yaml").write_text(TEMPLATE_YAML, encoding="utf-8") return template_dir +@pytest.fixture +def mock_template_class(): + """Mock Template 类,用于单元测试""" + from unittest.mock import patch + + with patch("core.presentation.Template") as mock_class: + yield mock_class + + # ============= PPTX 验证 Fixture ============= + @pytest.fixture def pptx_validator(): """PPTX 文件验证器实例""" from tests.conftest_pptx import PptxFileValidator + return PptxFileValidator() # ============= 测试数据目录 Fixture ============= + @pytest.fixture def fixtures_dir(): """测试数据目录路径""" @@ -122,6 +136,7 @@ def fixtures_dir(): # ============= 额外的边界情况 Fixtures ============= + @pytest.fixture def edge_case_yaml_files(fixtures_dir): """所有边界情况 YAML 文件的路径""" diff --git a/tests/conftest_pptx.py b/tests/conftest_pptx.py index 0047a1a..c4149b8 100644 --- a/tests/conftest_pptx.py +++ b/tests/conftest_pptx.py @@ -117,9 +117,7 @@ class PptxFileValidator: } for shape in slide.shapes: - if shape.shape_type == MSO_SHAPE.TEXT_BOX: - counts["text_box"] += 1 - elif hasattr(shape, "image"): + if hasattr(shape, "image"): counts["picture"] += 1 elif shape.shape_type in [ MSO_SHAPE.RECTANGLE, @@ -131,6 +129,8 @@ class PptxFileValidator: counts["table"] += 1 elif shape.shape_type == MSO_SHAPE.GROUP: counts["group"] += 1 + elif hasattr(shape, "has_text_frame") and shape.has_text_frame: + counts["text_box"] += 1 else: counts["other"] += 1 diff --git a/tests/integration/test_presentation.py b/tests/integration/test_presentation.py index 86110b3..d1d26a4 100644 --- a/tests/integration/test_presentation.py +++ b/tests/integration/test_presentation.py @@ -87,9 +87,7 @@ slides: # 模板变量应该被替换 elements = rendered["elements"] title_elem = next( - e - for e in elements - if e.get("type") == "text" and "Test Title" in e.get("content", "") + e for e in elements if e.type == "text" and "Test Title" in e.content ) assert title_elem is not None @@ -157,8 +155,8 @@ slides: # 检查变量是否被正确替换 elements = rendered["elements"] - assert any("My Title" in e.get("content", "") for e in elements) - assert any("My Subtitle" in e.get("content", "") for e in elements) + assert any("My Title" in e.content for e in elements) + assert any("My Subtitle" in e.content for e in elements) class TestPresentationWithoutTemplate: @@ -187,4 +185,4 @@ slides: # 元素应该直接被渲染 assert len(rendered["elements"]) == 1 - assert rendered["elements"][0]["content"] == "Direct Text" + assert rendered["elements"][0].content == "Direct Text" diff --git a/tests/unit/test_presentation.py b/tests/unit/test_presentation.py index 651f714..7b9cf87 100644 --- a/tests/unit/test_presentation.py +++ b/tests/unit/test_presentation.py @@ -26,7 +26,7 @@ class TestPresentationInit: """测试带模板目录初始化""" pres = Presentation(str(sample_yaml), str(sample_template)) - assert pres.templates_dir == sample_template + assert pres.templates_dir == str(sample_template) assert isinstance(pres.template_cache, dict) def test_init_without_templates_dir(self, sample_yaml): @@ -36,8 +36,8 @@ class TestPresentationInit: assert pres.templates_dir is None assert isinstance(pres.template_cache, dict) - @patch('core.presentation.load_yaml_file') - @patch('core.presentation.validate_presentation_yaml') + @patch("core.presentation.load_yaml_file") + @patch("core.presentation.validate_presentation_yaml") def test_init_loads_yaml(self, mock_validate, mock_load, temp_dir): """测试初始化时加载 YAML""" mock_data = {"slides": [{"elements": []}]} @@ -121,7 +121,7 @@ slides: class TestGetTemplate: """get_template 方法测试类""" - @patch('core.presentation.Template') + @patch("core.presentation.Template") def test_get_template_caches_template(self, mock_template_class, sample_template): """测试模板被缓存""" mock_template = Mock() @@ -145,8 +145,10 @@ slides: # 应该是同一个实例 assert template1 is template2 - @patch('core.presentation.Template') - def test_get_template_creates_new_template(self, mock_template_class, sample_template): + @patch("core.presentation.Template") + def test_get_template_creates_new_template( + self, mock_template_class, sample_template + ): """测试创建新模板""" mock_template = Mock() mock_template_class.return_value = mock_template @@ -172,7 +174,7 @@ slides: pres = Presentation(str(sample_yaml)) # 应该在调用 Template 时失败,而不是 get_template - with patch('core.presentation.Template') as mock_template_class: + with patch("core.presentation.Template") as mock_template_class: mock_template_class.side_effect = YAMLError("No template dir") with pytest.raises(YAMLError): @@ -198,12 +200,19 @@ class TestRenderSlide: assert len(result["elements"]) == 1 assert result["elements"][0].content == "Test" - @patch('core.presentation.Template') - def test_render_slide_with_template(self, mock_template_class, temp_dir, sample_template): + @patch("core.presentation.Template") + def test_render_slide_with_template( + self, mock_template_class, temp_dir, sample_template + ): """测试渲染使用模板的幻灯片""" mock_template = Mock() mock_template.render.return_value = [ - {"type": "text", "content": "Template Title", "box": [0, 0, 1, 1], "font": {}} + { + "type": "text", + "content": "Template Title", + "box": [0, 0, 1, 1], + "font": {}, + } ] mock_template_class.return_value = mock_template @@ -216,10 +225,7 @@ slides: pres = Presentation(str(yaml_path), str(sample_template)) - slide_data = { - "template": "title-slide", - "vars": {"title": "My Title"} - } + slide_data = {"template": "title-slide", "vars": {"title": "My Title"}} result = pres.render_slide(slide_data) @@ -231,10 +237,7 @@ slides: """测试渲染带背景的幻灯片""" pres = Presentation(str(sample_yaml)) - slide_data = { - "background": {"color": "#ffffff"}, - "elements": [] - } + slide_data = {"background": {"color": "#ffffff"}, "elements": []} result = pres.render_slide(slide_data) @@ -244,16 +247,16 @@ slides: """测试渲染无背景的幻灯片""" pres = Presentation(str(sample_yaml)) - slide_data = { - "elements": [] - } + slide_data = {"elements": []} result = pres.render_slide(slide_data) assert result["background"] is None - @patch('core.presentation.create_element') - def test_render_slide_converts_dict_to_objects(self, mock_create_element, sample_yaml): + @patch("core.presentation.create_element") + def test_render_slide_converts_dict_to_objects( + self, mock_create_element, sample_yaml + ): """测试字典转换为元素对象""" mock_elem = Mock() mock_create_element.return_value = mock_elem @@ -272,7 +275,9 @@ slides: mock_create_element.assert_called() assert result["elements"][0] == mock_elem - def test_render_slide_with_template_merges_background(self, mock_template_class, temp_dir, sample_template): + def test_render_slide_with_template_merges_background( + self, mock_template_class, temp_dir, sample_template + ): """测试使用模板时合并背景""" mock_template = Mock() mock_template.render.return_value = [ @@ -292,7 +297,7 @@ slides: slide_data = { "template": "test", "vars": {}, - "background": {"color": "#ff0000"} + "background": {"color": "#ff0000"}, } result = pres.render_slide(slide_data) @@ -304,16 +309,16 @@ slides: """测试空元素列表""" pres = Presentation(str(sample_yaml)) - slide_data = { - "elements": [] - } + slide_data = {"elements": []} result = pres.render_slide(slide_data) assert result["elements"] == [] - @patch('core.presentation.create_element') - def test_render_slide_with_multiple_elements(self, mock_create_element, sample_yaml): + @patch("core.presentation.create_element") + def test_render_slide_with_multiple_elements( + self, mock_create_element, sample_yaml + ): """测试多个元素""" mock_elem1 = Mock() mock_elem2 = Mock() @@ -324,7 +329,7 @@ slides: slide_data = { "elements": [ {"type": "text", "content": "T1", "box": [0, 0, 1, 1], "font": {}}, - {"type": "text", "content": "T2", "box": [1, 1, 1, 1], "font": {}} + {"type": "text", "content": "T2", "box": [1, 1, 1, 1], "font": {}}, ] }