1
0
Files
PPTX/tests/conftest_pptx.py
lanyuanxiaoyao ab2510a400 test: add comprehensive pytest test suite
Add complete test infrastructure for yaml2pptx project with 245+ tests
covering unit, integration, and end-to-end scenarios.

Test structure:
- Unit tests: elements, template system, validators, loaders, utils
- Integration tests: presentation and rendering flows
- E2E tests: CLI commands (convert, check, preview)

Key features:
- PptxFileValidator for Level 2 PPTX validation (file structure,
  element count, content matching, position tolerance)
- Comprehensive fixtures for test data consistency
- Mock-based testing for external dependencies
- Test images generated with PIL/Pillow
- Boundary case coverage for edge scenarios

Dependencies added:
- pytest, pytest-cov, pytest-mock
- pillow (for test image generation)

Documentation updated:
- README.md: test running instructions
- README_DEV.md: test development guide

Co-authored-by: OpenSpec change: add-comprehensive-tests
2026-03-02 23:11:34 +08:00

315 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
PPTX 文件验证工具类
提供 Level 2 验证深度的 PPTX 文件检查功能:
- 文件级别:存在、可打开
- 幻灯片级别:数量、尺寸
- 元素级别:类型、数量、内容、位置(容差 0.1 英寸)
"""
from pathlib import Path
from typing import List, Dict, Any, Optional
from pptx import Presentation
from pptx.util import Inches
from pptx.enum.shapes import MSO_SHAPE
class PptxValidationError:
"""验证错误信息"""
def __init__(self, level: str, message: str):
self.level = level # 'ERROR' or 'WARNING'
self.message = message
def __repr__(self):
return f"[{self.level}] {self.message}"
class PptxFileValidator:
"""PPTX 文件验证器Level 2 验证深度)"""
# 位置容忍度(英寸)
TOLERANCE = 0.1
# 幻灯片尺寸常量
SIZE_16_9 = (10.0, 5.625)
SIZE_4_3 = (10.0, 7.5)
def __init__(self):
self.errors: List[PptxValidationError] = []
self.warnings: List[PptxValidationError] = []
def validate_file(self, pptx_path: Path) -> bool:
"""
验证 PPTX 文件
Args:
pptx_path: PPTX 文件路径
Returns:
验证是否通过
"""
self.errors.clear()
self.warnings.clear()
# 1. 文件级别验证
if not self._validate_file_exists(pptx_path):
return False
# 2. 打开文件
try:
prs = Presentation(str(pptx_path))
except Exception as e:
self.errors.append(PptxValidationError("ERROR", f"无法打开 PPTX 文件: {e}"))
return False
return True
def validate_slides_count(self, prs: Presentation, expected_count: int) -> bool:
"""验证幻灯片数量"""
actual = len(prs.slides)
if actual != expected_count:
self.errors.append(
PptxValidationError("ERROR",
f"幻灯片数量不匹配: 期望 {expected_count}, 实际 {actual}")
)
return False
return True
def validate_slide_size(self, prs: Presentation, expected_size: str = "16:9") -> bool:
"""验证幻灯片尺寸"""
expected = self.SIZE_16_9 if expected_size == "16:9" else self.SIZE_4_3
actual_width = prs.slide_width.inches
actual_height = prs.slide_height.inches
if abs(actual_width - expected[0]) > self.TOLERANCE:
self.errors.append(
PptxValidationError("ERROR",
f"幻灯片宽度不匹配: 期望 {expected[0]}, 实际 {actual_width}")
)
return False
if abs(actual_height - expected[1]) > self.TOLERANCE:
self.errors.append(
PptxValidationError("ERROR",
f"幻灯片高度不匹配: 期望 {expected[1]}, 实际 {actual_height}")
)
return False
return True
def count_elements_by_type(self, slide) -> Dict[str, int]:
"""统计幻灯片中各类型元素的数量"""
counts = {
"text_box": 0,
"picture": 0,
"shape": 0,
"table": 0,
"group": 0,
"other": 0,
}
for shape in slide.shapes:
if shape.shape_type == MSO_SHAPE.TEXT_BOX:
counts["text_box"] += 1
elif hasattr(shape, "image"):
counts["picture"] += 1
elif shape.shape_type in [MSO_SHAPE.RECTANGLE, MSO_SHAPE.OVAL,
MSO_SHAPE.ROUNDED_RECTANGLE]:
counts["shape"] += 1
elif shape.has_table:
counts["table"] += 1
elif shape.shape_type == MSO_SHAPE.GROUP:
counts["group"] += 1
else:
counts["other"] += 1
return counts
def validate_text_element(self, slide, index: int = 0,
expected_content: Optional[str] = None,
expected_font_size: Optional[int] = None,
expected_color: Optional[tuple] = None) -> bool:
"""
验证文本元素
Args:
slide: 幻灯片对象
index: 文本框索引
expected_content: 期望的文本内容
expected_font_size: 期望的字体大小
expected_color: 期望的颜色 (R, G, B)
Returns:
验证是否通过
"""
text_boxes = [s for s in slide.shapes if s.shape_type == MSO_SHAPE.TEXT_BOX]
if index >= len(text_boxes):
self.errors.append(
PptxValidationError("ERROR", f"找不到索引 {index} 的文本框")
)
return False
textbox = text_boxes[index]
text_frame = textbox.text_frame
# 验证内容
if expected_content is not None:
actual_content = text_frame.text
if actual_content != expected_content:
self.errors.append(
PptxValidationError("ERROR",
f"文本内容不匹配: 期望 '{expected_content}', 实际 '{actual_content}'")
)
return False
# 验证字体大小
if expected_font_size is not None:
actual_size = text_frame.paragraphs[0].font.size.pt
if abs(actual_size - expected_font_size) > 1:
self.errors.append(
PptxValidationError("ERROR",
f"字体大小不匹配: 期望 {expected_font_size}pt, 实际 {actual_size}pt")
)
return False
# 验证颜色
if expected_color is not None:
try:
actual_rgb = text_frame.paragraphs[0].font.color.rgb
actual_color = (actual_rgb[0], actual_rgb[1], actual_rgb[2])
if actual_color != expected_color:
self.errors.append(
PptxValidationError("ERROR",
f"字体颜色不匹配: 期望 RGB{expected_color}, 实际 RGB{actual_color}")
)
return False
except Exception:
self.errors.append(
PptxValidationError("WARNING", "无法获取字体颜色")
)
return True
def validate_position(self, shape, expected_left: float, expected_top: float,
expected_width: Optional[float] = None,
expected_height: Optional[float] = None) -> bool:
"""
验证元素位置和尺寸
Args:
shape: 形状对象
expected_left: 期望的左边距(英寸)
expected_top: 期望的上边距(英寸)
expected_width: 期望的宽度(英寸)
expected_height: 期望的高度(英寸)
Returns:
验证是否通过
"""
actual_left = shape.left.inches
actual_top = shape.top.inches
if abs(actual_left - expected_left) > self.TOLERANCE:
self.errors.append(
PptxValidationError("ERROR",
f"左边距不匹配: 期望 {expected_left}, 实际 {actual_left}")
)
return False
if abs(actual_top - expected_top) > self.TOLERANCE:
self.errors.append(
PptxValidationError("ERROR",
f"上边距不匹配: 期望 {expected_top}, 实际 {actual_top}")
)
return False
if expected_width is not None:
actual_width = shape.width.inches
if abs(actual_width - expected_width) > self.TOLERANCE:
self.errors.append(
PptxValidationError("ERROR",
f"宽度不匹配: 期望 {expected_width}, 实际 {actual_width}")
)
return False
if expected_height is not None:
actual_height = shape.height.inches
if abs(actual_height - expected_height) > self.TOLERANCE:
self.errors.append(
PptxValidationError("ERROR",
f"高度不匹配: 期望 {expected_height}, 实际 {actual_height}")
)
return False
return True
def validate_background_color(self, slide, expected_rgb: tuple) -> bool:
"""
验证幻灯片背景颜色
Args:
slide: 幻灯片对象
expected_rgb: 期望的 RGB 颜色 (R, G, B)
Returns:
验证是否通过
"""
try:
fill = slide.background.fill
if fill.type == 1: # Solid fill
actual_rgb = (
fill.fore_color.rgb[0],
fill.fore_color.rgb[1],
fill.fore_color.rgb[2],
)
if actual_rgb != expected_rgb:
self.errors.append(
PptxValidationError("ERROR",
f"背景颜色不匹配: 期望 RGB{expected_rgb}, 实际 RGB{actual_rgb}")
)
return False
except Exception as e:
self.errors.append(
PptxValidationError("WARNING", f"无法获取背景颜色: {e}")
)
return True
def _validate_file_exists(self, pptx_path: Path) -> bool:
"""验证文件存在且大小大于 0"""
if not pptx_path.exists():
self.errors.append(
PptxValidationError("ERROR", f"文件不存在: {pptx_path}")
)
return False
if pptx_path.stat().st_size == 0:
self.errors.append(
PptxValidationError("ERROR", f"文件大小为 0: {pptx_path}")
)
return False
return True
def has_errors(self) -> bool:
"""是否有错误"""
return len(self.errors) > 0
def has_warnings(self) -> bool:
"""是否有警告"""
return len(self.warnings) > 0
def get_errors(self) -> List[str]:
"""获取所有错误信息"""
return [e.message for e in self.errors]
def get_warnings(self) -> List[str]:
"""获取所有警告信息"""
return [w.message for w in self.warnings]
def clear(self):
"""清除所有错误和警告"""
self.errors.clear()
self.warnings.clear()