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
This commit is contained in:
3
tests/unit/test_validators/__init__.py
Normal file
3
tests/unit/test_validators/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
验证器测试包
|
||||
"""
|
||||
128
tests/unit/test_validators/test_geometry.py
Normal file
128
tests/unit/test_validators/test_geometry.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
几何验证器单元测试
|
||||
|
||||
测试 GeometryValidator 的元素边界检查功能
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from validators.geometry import GeometryValidator, TOLERANCE
|
||||
from validators.result import ValidationIssue
|
||||
|
||||
|
||||
class TestGeometryValidator:
|
||||
"""GeometryValidator 测试类"""
|
||||
|
||||
def test_init(self):
|
||||
"""测试初始化"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
assert validator.slide_width == 10
|
||||
assert validator.slide_height == 5.625
|
||||
|
||||
def test_validate_element_within_bounds(self):
|
||||
"""测试元素在边界内"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
elem = type('Element', (), {
|
||||
'box': [1, 1, 5, 2]
|
||||
})()
|
||||
issues = validator.validate_element(elem, 1, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_validate_element_right_boundary_exceeded(self):
|
||||
"""测试元素右边界超出"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
elem = type('Element', (), {
|
||||
'box': [8, 1, 3, 1] # right = 11 > 10
|
||||
})()
|
||||
issues = validator.validate_element(elem, 1, 1)
|
||||
assert len(issues) == 1
|
||||
assert issues[0].level == "WARNING"
|
||||
assert issues[0].code == "ELEMENT_OUT_OF_BOUNDS"
|
||||
assert "右边界" in issues[0].message
|
||||
|
||||
def test_validate_element_bottom_boundary_exceeded(self):
|
||||
"""测试元素下边界超出"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
elem = type('Element', (), {
|
||||
'box': [1, 5, 2, 1] # bottom = 6 > 5.625
|
||||
})()
|
||||
issues = validator.validate_element(elem, 1, 1)
|
||||
assert len(issues) == 1
|
||||
assert issues[0].level == "WARNING"
|
||||
assert issues[0].code == "ELEMENT_OUT_OF_BOUNDS"
|
||||
assert "下边界" in issues[0].message
|
||||
|
||||
def test_validate_element_completely_out_of_bounds(self):
|
||||
"""测试元素完全在页面外"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
elem = type('Element', (), {
|
||||
'box': [12, 7, 2, 1] # 完全在页面外
|
||||
})()
|
||||
issues = validator.validate_element(elem, 1, 1)
|
||||
assert len(issues) == 1
|
||||
assert issues[0].level == "WARNING"
|
||||
assert issues[0].code == "ELEMENT_COMPLETELY_OUT_OF_BOUNDS"
|
||||
|
||||
def test_validate_element_within_tolerance(self):
|
||||
"""测试元素在容忍度范围内"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
# 右边界 = 10.05,容忍度 0.1,应该通过
|
||||
elem = type('Element', (), {
|
||||
'box': [8, 1, 2.05, 1]
|
||||
})()
|
||||
issues = validator.validate_element(elem, 1, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_validate_element_slightly_beyond_tolerance(self):
|
||||
"""测试元素稍微超出容忍度"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
# 右边界 = 10.15,超出容忍度 0.1
|
||||
elem = type('Element', (), {
|
||||
'box': [8, 1, 2.15, 1]
|
||||
})()
|
||||
issues = validator.validate_element(elem, 1, 1)
|
||||
assert len(issues) == 1
|
||||
|
||||
def test_validate_table_within_bounds(self):
|
||||
"""测试表格在边界内"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
elem = type('Element', (), {
|
||||
'position': [1, 1],
|
||||
'col_widths': [2, 2, 2]
|
||||
})()
|
||||
issues = validator.validate_element(elem, 1, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_validate_table_exceeds_width(self):
|
||||
"""测试表格超出页面宽度"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
elem = type('Element', (), {
|
||||
'position': [1, 1],
|
||||
'col_widths': [3, 3, 4] # 总宽 10,位置 1,右边界 11
|
||||
})()
|
||||
issues = validator.validate_element(elem, 1, 1)
|
||||
assert len(issues) == 1
|
||||
assert issues[0].code == "TABLE_OUT_OF_BOUNDS"
|
||||
|
||||
def test_validate_element_without_box(self):
|
||||
"""测试没有 box 属性的元素"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
elem = type('Element', (), {})() # 没有 box 属性
|
||||
issues = validator.validate_element(elem, 1, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_validate_table_without_col_widths(self):
|
||||
"""测试没有 col_widths 的表格"""
|
||||
validator = GeometryValidator(10, 5.625)
|
||||
elem = type('Element', (), {
|
||||
'position': [1, 1]
|
||||
})()
|
||||
issues = validator.validate_element(elem, 1, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
|
||||
class TestToleranceConstant:
|
||||
"""容忍度常量测试"""
|
||||
|
||||
def test_tolerance_value(self):
|
||||
"""测试容忍度值为 0.1"""
|
||||
assert TOLERANCE == 0.1
|
||||
135
tests/unit/test_validators/test_resource.py
Normal file
135
tests/unit/test_validators/test_resource.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""
|
||||
资源验证器单元测试
|
||||
|
||||
测试 ResourceValidator 的图片和模板文件验证功能
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from validators.resource import ResourceValidator
|
||||
|
||||
|
||||
class TestResourceValidator:
|
||||
"""ResourceValidator 测试类"""
|
||||
|
||||
def test_init(self, temp_dir):
|
||||
"""测试初始化"""
|
||||
validator = ResourceValidator(yaml_dir=temp_dir)
|
||||
assert validator.yaml_dir == temp_dir
|
||||
assert validator.template_dir is None
|
||||
|
||||
def test_init_with_template_dir(self, temp_dir):
|
||||
"""测试带模板目录初始化"""
|
||||
template_dir = temp_dir / "templates"
|
||||
validator = ResourceValidator(
|
||||
yaml_dir=temp_dir,
|
||||
template_dir=template_dir
|
||||
)
|
||||
assert validator.template_dir == template_dir
|
||||
|
||||
|
||||
class TestValidateImage:
|
||||
"""validate_image 方法测试"""
|
||||
|
||||
def test_existing_image(self, temp_dir, sample_image):
|
||||
"""测试存在的图片文件"""
|
||||
validator = ResourceValidator(yaml_dir=temp_dir)
|
||||
elem = type('Element', (), {'src': sample_image.name})()
|
||||
issues = validator.validate_image(elem, 1, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_nonexistent_image(self, temp_dir):
|
||||
"""测试不存在的图片文件"""
|
||||
validator = ResourceValidator(yaml_dir=temp_dir)
|
||||
elem = type('Element', (), {'src': 'nonexistent.png'})()
|
||||
issues = validator.validate_image(elem, 1, 1)
|
||||
assert len(issues) == 1
|
||||
assert issues[0].level == "ERROR"
|
||||
assert issues[0].code == "IMAGE_FILE_NOT_FOUND"
|
||||
|
||||
def test_image_with_absolute_path(self, temp_dir, sample_image):
|
||||
"""测试绝对路径图片"""
|
||||
validator = ResourceValidator(yaml_dir=temp_dir)
|
||||
elem = type('Element', (), {'src': str(sample_image)})()
|
||||
issues = validator.validate_image(elem, 1, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_image_without_src_attribute(self, temp_dir):
|
||||
"""测试没有 src 属性的元素"""
|
||||
validator = ResourceValidator(yaml_dir=temp_dir)
|
||||
elem = type('Element', (), {})() # 没有 src 属性
|
||||
issues = validator.validate_image(elem, 1, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_image_with_empty_src(self, temp_dir):
|
||||
"""测试空 src 字符串"""
|
||||
validator = ResourceValidator(yaml_dir=temp_dir)
|
||||
elem = type('Element', (), {'src': ''})()
|
||||
issues = validator.validate_image(elem, 1, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
|
||||
class TestValidateTemplate:
|
||||
"""validate_template 方法测试"""
|
||||
|
||||
def test_slide_without_template(self, temp_dir):
|
||||
"""测试不使用模板的幻灯片"""
|
||||
validator = ResourceValidator(yaml_dir=temp_dir)
|
||||
slide_data = {} # 没有 template 字段
|
||||
issues = validator.validate_template(slide_data, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_template_with_dir_not_specified(self, temp_dir):
|
||||
"""测试使用模板但未指定模板目录"""
|
||||
validator = ResourceValidator(yaml_dir=temp_dir, template_dir=None)
|
||||
slide_data = {'template': 'title-slide'}
|
||||
issues = validator.validate_template(slide_data, 1)
|
||||
assert len(issues) == 1
|
||||
assert issues[0].level == "ERROR"
|
||||
assert issues[0].code == "TEMPLATE_DIR_NOT_SPECIFIED"
|
||||
|
||||
def test_nonexistent_template_file(self, temp_dir):
|
||||
"""测试不存在的模板文件"""
|
||||
template_dir = temp_dir / "templates"
|
||||
template_dir.mkdir()
|
||||
validator = ResourceValidator(yaml_dir=temp_dir, template_dir=template_dir)
|
||||
slide_data = {'template': 'nonexistent'}
|
||||
issues = validator.validate_template(slide_data, 1)
|
||||
assert len(issues) == 1
|
||||
assert issues[0].level == "ERROR"
|
||||
assert issues[0].code == "TEMPLATE_FILE_NOT_FOUND"
|
||||
|
||||
def test_valid_template_file(self, sample_template):
|
||||
"""测试有效的模板文件"""
|
||||
validator = ResourceValidator(
|
||||
yaml_dir=sample_template.parent,
|
||||
template_dir=sample_template
|
||||
)
|
||||
slide_data = {'template': 'title-slide'}
|
||||
issues = validator.validate_template(slide_data, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_template_with_yaml_extension(self, temp_dir):
|
||||
"""测试带 .yaml 扩展名的模板"""
|
||||
template_dir = temp_dir / "templates"
|
||||
template_dir.mkdir()
|
||||
(template_dir / "test.yaml").write_text("vars: []\nelements: []")
|
||||
|
||||
validator = ResourceValidator(yaml_dir=temp_dir, template_dir=template_dir)
|
||||
slide_data = {'template': 'test.yaml'}
|
||||
issues = validator.validate_template(slide_data, 1)
|
||||
assert len(issues) == 0
|
||||
|
||||
def test_invalid_template_structure(self, temp_dir):
|
||||
"""测试无效的模板结构"""
|
||||
template_dir = temp_dir / "templates"
|
||||
template_dir.mkdir()
|
||||
# 创建缺少 elements 字段的无效模板
|
||||
(template_dir / "invalid.yaml").write_text("vars: []")
|
||||
|
||||
validator = ResourceValidator(yaml_dir=temp_dir, template_dir=template_dir)
|
||||
slide_data = {'template': 'invalid'}
|
||||
issues = validator.validate_template(slide_data, 1)
|
||||
assert len(issues) == 1
|
||||
assert issues[0].level == "ERROR"
|
||||
assert issues[0].code == "TEMPLATE_STRUCTURE_ERROR"
|
||||
155
tests/unit/test_validators/test_result.py
Normal file
155
tests/unit/test_validators/test_result.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""
|
||||
验证结果数据结构单元测试
|
||||
|
||||
测试 ValidationIssue 和 ValidationResult 类
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from validators.result import ValidationIssue, ValidationResult
|
||||
|
||||
|
||||
class TestValidationIssue:
|
||||
"""ValidationIssue 测试类"""
|
||||
|
||||
def test_create_error_issue(self):
|
||||
"""测试创建错误级别问题"""
|
||||
issue = ValidationIssue(
|
||||
level="ERROR",
|
||||
message="Test error",
|
||||
location="Slide 1",
|
||||
code="TEST_ERROR"
|
||||
)
|
||||
assert issue.level == "ERROR"
|
||||
assert issue.message == "Test error"
|
||||
assert issue.location == "Slide 1"
|
||||
assert issue.code == "TEST_ERROR"
|
||||
|
||||
def test_create_warning_issue(self):
|
||||
"""测试创建警告级别问题"""
|
||||
issue = ValidationIssue(
|
||||
level="WARNING",
|
||||
message="Test warning",
|
||||
location="",
|
||||
code="TEST_WARNING"
|
||||
)
|
||||
assert issue.level == "WARNING"
|
||||
|
||||
def test_create_info_issue(self):
|
||||
"""测试创建提示级别问题"""
|
||||
issue = ValidationIssue(
|
||||
level="INFO",
|
||||
message="Test info",
|
||||
location="",
|
||||
code="TEST_INFO"
|
||||
)
|
||||
assert issue.level == "INFO"
|
||||
|
||||
|
||||
class TestValidationResult:
|
||||
"""ValidationResult 测试类"""
|
||||
|
||||
def test_create_with_defaults(self):
|
||||
"""测试使用默认值创建"""
|
||||
result = ValidationResult(valid=True)
|
||||
assert result.valid is True
|
||||
assert result.errors == []
|
||||
assert result.warnings == []
|
||||
assert result.infos == []
|
||||
|
||||
def test_create_with_issues(self):
|
||||
"""测试创建包含问题的结果"""
|
||||
errors = [ValidationIssue("ERROR", "Error 1", "", "ERR1")]
|
||||
warnings = [ValidationIssue("WARNING", "Warning 1", "", "WARN1")]
|
||||
infos = [ValidationIssue("INFO", "Info 1", "", "INFO1")]
|
||||
|
||||
result = ValidationResult(
|
||||
valid=False,
|
||||
errors=errors,
|
||||
warnings=warnings,
|
||||
infos=infos
|
||||
)
|
||||
assert result.valid is False
|
||||
assert len(result.errors) == 1
|
||||
assert len(result.warnings) == 1
|
||||
assert len(result.infos) == 1
|
||||
|
||||
def test_has_errors(self):
|
||||
"""测试 has_errors 方法"""
|
||||
result = ValidationResult(
|
||||
valid=True,
|
||||
errors=[ValidationIssue("ERROR", "Error", "", "ERR")]
|
||||
)
|
||||
assert result.has_errors() is True
|
||||
|
||||
def test_has_errors_no_errors(self):
|
||||
"""测试没有错误时 has_errors 返回 False"""
|
||||
result = ValidationResult(valid=True)
|
||||
assert result.has_errors() is False
|
||||
|
||||
def test_format_output_clean(self):
|
||||
"""测试格式化输出 - 无问题"""
|
||||
result = ValidationResult(valid=True)
|
||||
output = result.format_output()
|
||||
assert "验证通过" in output
|
||||
|
||||
def test_format_output_with_errors(self):
|
||||
"""测试格式化输出 - 包含错误"""
|
||||
result = ValidationResult(
|
||||
valid=False,
|
||||
errors=[
|
||||
ValidationIssue("ERROR", "Error 1", "Slide 1", "ERR1"),
|
||||
ValidationIssue("ERROR", "Error 2", "Slide 2", "ERR2")
|
||||
]
|
||||
)
|
||||
output = result.format_output()
|
||||
assert "2 个错误" in output
|
||||
assert "Error 1" in output
|
||||
assert "Error 2" in output
|
||||
assert "[Slide 1]" in output
|
||||
|
||||
def test_format_output_with_warnings(self):
|
||||
"""测试格式化输出 - 包含警告"""
|
||||
result = ValidationResult(
|
||||
valid=True,
|
||||
warnings=[
|
||||
ValidationIssue("WARNING", "Warning 1", "Slide 1", "WARN1")
|
||||
]
|
||||
)
|
||||
output = result.format_output()
|
||||
assert "1 个警告" in output
|
||||
assert "Warning 1" in output
|
||||
|
||||
def test_format_output_with_infos(self):
|
||||
"""测试格式化输出 - 包含提示"""
|
||||
result = ValidationResult(
|
||||
valid=True,
|
||||
infos=[
|
||||
ValidationIssue("INFO", "Info 1", "", "INFO1")
|
||||
]
|
||||
)
|
||||
output = result.format_output()
|
||||
assert "1 个提示" in output
|
||||
|
||||
def test_format_output_mixed_issues(self):
|
||||
"""测试格式化输出 - 混合问题"""
|
||||
result = ValidationResult(
|
||||
valid=False,
|
||||
errors=[ValidationIssue("ERROR", "Error", "", "ERR")],
|
||||
warnings=[ValidationIssue("WARNING", "Warning", "", "WARN")],
|
||||
infos=[ValidationIssue("INFO", "Info", "", "INFO")]
|
||||
)
|
||||
output = result.format_output()
|
||||
assert "1 个错误" in output
|
||||
assert "1 个警告" in output
|
||||
assert "1 个提示" in output
|
||||
|
||||
def test_format_output_issue_without_location(self):
|
||||
"""测试没有位置信息的问题"""
|
||||
result = ValidationResult(
|
||||
valid=False,
|
||||
errors=[ValidationIssue("ERROR", "Error", "", "ERR")]
|
||||
)
|
||||
output = result.format_output()
|
||||
assert "Error" in output
|
||||
# 不应该有位置前缀
|
||||
assert "[]" not in output
|
||||
119
tests/unit/test_validators/test_validator.py
Normal file
119
tests/unit/test_validators/test_validator.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
主验证器单元测试
|
||||
|
||||
测试 Validator 类的协调和分类功能
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from validators.validator import Validator
|
||||
from validators.result import ValidationResult
|
||||
|
||||
|
||||
class TestValidator:
|
||||
"""Validator 测试类"""
|
||||
|
||||
def test_init(self):
|
||||
"""测试初始化"""
|
||||
validator = Validator()
|
||||
assert validator.SLIDE_SIZES == {
|
||||
"16:9": (10, 5.625),
|
||||
"4:3": (10, 7.5)
|
||||
}
|
||||
|
||||
def test_validate_valid_yaml(self, sample_yaml):
|
||||
"""测试验证有效的 YAML 文件"""
|
||||
validator = Validator()
|
||||
result = validator.validate(sample_yaml)
|
||||
assert isinstance(result, ValidationResult)
|
||||
assert result.valid is True
|
||||
|
||||
def test_validate_nonexistent_file(self, temp_dir):
|
||||
"""测试验证不存在的文件"""
|
||||
validator = Validator()
|
||||
nonexistent = temp_dir / "nonexistent.yaml"
|
||||
result = validator.validate(nonexistent)
|
||||
assert result.valid is False
|
||||
assert len(result.errors) > 0
|
||||
|
||||
def test_categorize_issues_error(self):
|
||||
"""测试分类问题为错误"""
|
||||
validator = Validator()
|
||||
from validators.result import ValidationIssue
|
||||
|
||||
errors = []
|
||||
warnings = []
|
||||
infos = []
|
||||
|
||||
issue = ValidationIssue("ERROR", "Test", "", "ERR")
|
||||
validator._categorize_issues([issue], errors, warnings, infos)
|
||||
|
||||
assert len(errors) == 1
|
||||
assert len(warnings) == 0
|
||||
assert len(infos) == 0
|
||||
|
||||
def test_categorize_issues_warning(self):
|
||||
"""测试分类问题为警告"""
|
||||
validator = Validator()
|
||||
from validators.result import ValidationIssue
|
||||
|
||||
errors = []
|
||||
warnings = []
|
||||
infos = []
|
||||
|
||||
issue = ValidationIssue("WARNING", "Test", "", "WARN")
|
||||
validator._categorize_issues([issue], errors, warnings, infos)
|
||||
|
||||
assert len(errors) == 0
|
||||
assert len(warnings) == 1
|
||||
assert len(infos) == 0
|
||||
|
||||
def test_categorize_issues_info(self):
|
||||
"""测试分类问题为提示"""
|
||||
validator = Validator()
|
||||
from validators.result import ValidationIssue
|
||||
|
||||
errors = []
|
||||
warnings = []
|
||||
infos = []
|
||||
|
||||
issue = ValidationIssue("INFO", "Test", "", "INFO")
|
||||
validator._categorize_issues([issue], errors, warnings, infos)
|
||||
|
||||
assert len(errors) == 0
|
||||
assert len(warnings) == 0
|
||||
assert len(infos) == 1
|
||||
|
||||
def test_categorize_mixed_issues(self):
|
||||
"""测试分类混合问题"""
|
||||
validator = Validator()
|
||||
from validators.result import ValidationIssue
|
||||
|
||||
errors = []
|
||||
warnings = []
|
||||
infos = []
|
||||
|
||||
issues = [
|
||||
ValidationIssue("ERROR", "Error", "", "ERR"),
|
||||
ValidationIssue("WARNING", "Warning", "", "WARN"),
|
||||
ValidationIssue("INFO", "Info", "", "INFO"),
|
||||
]
|
||||
validator._categorize_issues(issues, errors, warnings, infos)
|
||||
|
||||
assert len(errors) == 1
|
||||
assert len(warnings) == 1
|
||||
assert len(infos) == 1
|
||||
|
||||
|
||||
class TestSlideSizes:
|
||||
"""幻灯片尺寸常量测试"""
|
||||
|
||||
def test_16_9_size(self):
|
||||
"""测试 16:9 尺寸"""
|
||||
validator = Validator()
|
||||
assert validator.SLIDE_SIZES["16:9"] == (10, 5.625)
|
||||
|
||||
def test_4_3_size(self):
|
||||
"""测试 4:3 尺寸"""
|
||||
validator = Validator()
|
||||
assert validator.SLIDE_SIZES["4:3"] == (10, 7.5)
|
||||
Reference in New Issue
Block a user