1
0
Files
PPTX/tests/unit/test_template.py
lanyuanxiaoyao f1aae96a04 refactor: 重构外部模板系统,改为单文件模板库模式
主要变更:
- 将 templates_dir 参数改为 template_file,支持单个模板库 YAML 文件
- 添加模板库 YAML 验证功能
- 为模板添加 base_dir 支持,正确解析相对路径资源
- 内联模板与外部模板同名时改为警告(内联优先)
- 移除模板缓存机制,直接使用模板库字典
- 更新所有相关测试以适配新的模板加载方式

此重构简化了模板管理,使模板资源的路径解析更加清晰明确。
2026-03-05 13:27:12 +08:00

577 lines
22 KiB
Python

"""
模板系统单元测试
测试 Template 类的初始化、变量解析、条件渲染和模板渲染功能
"""
import pytest
from pathlib import Path
from loaders.yaml_loader import YAMLError
from core.template import Template
from core.presentation import Presentation
# ============= 模板初始化测试 =============
class TestTemplateFromData:
"""Template.from_data 方法测试类"""
def test_from_data_with_valid_template(self, temp_dir):
"""测试从有效数据创建模板"""
template_data = {
"vars": [
{"name": "title", "required": True},
{"name": "subtitle", "required": False, "default": ""}
],
"elements": [
{"type": "text", "box": [1, 2, 8, 1], "content": "{title}"}
]
}
template = Template.from_data(template_data, "test-template", base_dir=temp_dir)
assert template.data is not None
assert "elements" in template.data
assert "vars" in template.data
assert template.base_dir == temp_dir
def test_from_data_without_vars(self, temp_dir):
"""测试创建没有变量的模板"""
template_data = {
"elements": [
{"type": "text", "box": [1, 2, 8, 1], "content": "Static Text"}
]
}
template = Template.from_data(template_data, "static-template", base_dir=temp_dir)
assert len(template.vars_def) == 0
assert len(template.elements) == 1
def test_from_data_with_empty_vars(self, temp_dir):
"""测试创建空变量列表的模板"""
template_data = {
"vars": [],
"elements": []
}
template = Template.from_data(template_data, "empty-template", base_dir=temp_dir)
assert len(template.vars_def) == 0
assert len(template.elements) == 0
def test_from_data_render_with_vars(self, temp_dir):
"""测试使用 from_data 创建的模板可以正常渲染"""
template_data = {
"vars": [{"name": "title", "required": True}],
"elements": [
{"type": "text", "box": [1, 2, 8, 1], "content": "{title}"}
]
}
template = Template.from_data(template_data, "test-template", base_dir=temp_dir)
rendered = template.render({"title": "Hello"})
assert rendered[0]["content"] == "Hello"
def test_from_data_render_with_default_value(self, temp_dir):
"""测试使用默认值渲染"""
template_data = {
"vars": [
{"name": "title", "required": False, "default": "Default Title"}
],
"elements": [
{"type": "text", "box": [1, 2, 8, 1], "content": "{title}"}
]
}
template = Template.from_data(template_data, "test-template", base_dir=temp_dir)
rendered = template.render({})
assert rendered[0]["content"] == "Default Title"
def test_from_data_with_conditional_element(self, temp_dir):
"""测试条件元素"""
template_data = {
"vars": [{"name": "show", "required": False, "default": ""}],
"elements": [
{"type": "text", "box": [1, 2, 8, 1], "content": "Always", "visible": "True"},
{"type": "text", "box": [1, 3, 8, 1], "content": "Conditional", "visible": "{show != ''}"}
]
}
template = Template.from_data(template_data, "test-template", base_dir=temp_dir)
# 不提供 show 变量
rendered = template.render({})
assert len(rendered) == 1
assert rendered[0]["content"] == "Always"
# 提供 show 变量
rendered = template.render({"show": "yes"})
assert len(rendered) == 2
def test_from_data_with_nested_variable_in_default(self, temp_dir):
"""测试默认值中的嵌套变量"""
template_data = {
"vars": [
{"name": "prefix", "required": True},
{"name": "title", "required": False, "default": "{prefix} Title"}
],
"elements": [
{"type": "text", "box": [1, 2, 8, 1], "content": "{title}"}
]
}
template = Template.from_data(template_data, "test-template", base_dir=temp_dir)
rendered = template.render({"prefix": "My"})
assert rendered[0]["content"] == "My Title"
def test_from_data_rejects_template_reference_in_elements(self, temp_dir):
"""测试元素中包含 template 字段会引发错误"""
template_data = {
"vars": [],
"elements": [
{"template": "nested-template"} # 内联模板不支持相互引用
]
}
template = Template.from_data(template_data, "test-template", base_dir=temp_dir)
# 渲染时应该引发错误
with pytest.raises(YAMLError, match="内联模板不支持相互引用"):
template.render({})
def test_from_data_with_description(self, temp_dir):
"""测试模板描述字段"""
template_data = {
"description": "这是一个测试模板",
"vars": [],
"elements": []
}
template = Template.from_data(template_data, "test-template", base_dir=temp_dir)
assert template.description == "这是一个测试模板"
def test_from_data_without_description(self, temp_dir):
"""测试没有描述字段的模板"""
template_data = {
"vars": [],
"elements": []
}
template = Template.from_data(template_data, "test-template", base_dir=temp_dir)
assert template.description is None
# ============= 变量解析测试 =============
class TestResolveValue:
"""resolve_value 方法测试类"""
def test_resolve_value_simple_variable(self, temp_dir):
"""测试解析简单变量"""
template_data = {"vars": [], "elements": []}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
result = template.resolve_value("{title}", {"title": "My Title"})
assert result == "My Title"
def test_resolve_value_multiple_variables(self, temp_dir):
"""测试解析多个变量"""
template_data = {"vars": [], "elements": []}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
result = template.resolve_value(
"{title} - {subtitle}", {"title": "Main", "subtitle": "Sub"}
)
assert result == "Main - Sub"
def test_resolve_value_undefined_variable_raises_error(self, temp_dir):
"""测试未定义变量会引发错误"""
template_data = {"vars": [], "elements": []}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
with pytest.raises(YAMLError, match="未定义的变量"):
template.resolve_value("{undefined}", {"title": "Test"})
def test_resolve_value_preserves_non_string(self, temp_dir):
"""测试非字符串值保持原样"""
template_data = {"vars": [], "elements": []}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
assert template.resolve_value(123, {}) == 123
assert template.resolve_value(None, {}) is None
assert template.resolve_value(["list"], {}) == ["list"]
def test_resolve_value_converts_to_integer(self, temp_dir):
"""测试结果转换为整数"""
template_data = {"vars": [], "elements": []}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
result = template.resolve_value("{value}", {"value": "42"})
assert result == 42
assert isinstance(result, int)
def test_resolve_value_converts_to_float(self, temp_dir):
"""测试结果转换为浮点数"""
template_data = {"vars": [], "elements": []}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
result = template.resolve_value("{value}", {"value": "3.14"})
assert result == 3.14
assert isinstance(result, float)
# ============= resolve_element 测试 =============
class TestResolveElement:
"""resolve_element 方法测试类"""
def test_resolve_element_dict(self, temp_dir):
"""测试解析字典元素"""
template_data = {"vars": [], "elements": []}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
elem = {"content": "{title}", "box": [1, 2, 3, 4]}
result = template.resolve_element(elem, {"title": "Test"})
assert result["content"] == "Test"
assert result["box"] == [1, 2, 3, 4]
def test_resolve_element_list(self, temp_dir):
"""测试解析列表元素"""
template_data = {"vars": [], "elements": []}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
elem = ["{title}", "{subtitle}"]
result = template.resolve_element(elem, {"title": "A", "subtitle": "B"})
assert result == ["A", "B"]
def test_resolve_element_nested_structure(self, temp_dir):
"""测试解析嵌套结构"""
template_data = {"vars": [], "elements": []}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
elem = {
"content": "{title}",
"font": {"size": "{size}", "color": "#000"}
}
result = template.resolve_element(elem, {"title": "Test", "size": "24"})
assert result["content"] == "Test"
assert result["font"]["size"] == 24
assert result["font"]["color"] == "#000"
def test_resolve_element_excludes_visible(self, temp_dir):
"""测试 visible 字段在 resolve_element 中被排除"""
template_data = {"vars": [], "elements": []}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
elem = {"content": "{title}", "visible": "{show}"}
result = template.resolve_element(elem, {"title": "Test", "show": "true"})
assert result["content"] == "Test"
# visible 字段在 resolve_element 中被排除,在 render 中单独处理
assert "visible" not in result
# ============= 条件评估测试 =============
class TestEvaluateCondition:
"""evaluate_condition 方法测试类"""
def test_evaluate_condition_with_non_empty_variable(self, temp_dir):
"""测试非空变量条件"""
template_data = {"vars": [], "elements": []}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
result = template.evaluate_condition("{title != ''}", {"title": "Hello"})
assert result is True
def test_evaluate_condition_with_empty_variable(self, temp_dir):
"""测试空变量条件"""
template_data = {"vars": [], "elements": []}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
result = template.evaluate_condition("{title != ''}", {"title": ""})
assert result is False
def test_evaluate_condition_with_missing_variable(self, temp_dir):
"""测试缺失变量条件会引发错误"""
template_data = {"vars": [], "elements": []}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
# 缺失变量应该引发错误
with pytest.raises(YAMLError, match="变量未定义"):
template.evaluate_condition("{title != ''}", {})
def test_evaluate_condition_complex_logic(self, temp_dir):
"""测试复杂逻辑条件"""
template_data = {"vars": [], "elements": []}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
result = template.evaluate_condition(
"{a == 'yes' and b != ''}",
{"a": "yes", "b": "value"}
)
assert result is True
def test_evaluate_condition_member_test(self, temp_dir):
"""测试成员测试条件"""
template_data = {"vars": [], "elements": []}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
result = template.evaluate_condition("{type in ['A', 'B']}", {"type": "A"})
assert result is True
def test_evaluate_condition_math_operation(self, temp_dir):
"""测试数学运算条件"""
template_data = {"vars": [], "elements": []}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
# 使用整数而不是字符串进行比较
result = template.evaluate_condition("{count > 5}", {"count": 10})
assert result is True
# ============= 渲染测试 =============
class TestRender:
"""render 方法测试类"""
def test_render_with_required_variable(self, temp_dir):
"""测试使用必需变量渲染"""
template_data = {
"vars": [{"name": "title", "required": True}],
"elements": [
{"type": "text", "box": [1, 2, 8, 1], "content": "{title}"}
]
}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
rendered = template.render({"title": "Hello"})
assert len(rendered) == 1
assert rendered[0]["content"] == "Hello"
def test_render_with_optional_variable(self, temp_dir):
"""测试使用可选变量渲染"""
template_data = {
"vars": [{"name": "subtitle", "required": False, "default": ""}],
"elements": [
{"type": "text", "box": [1, 2, 8, 1], "content": "{subtitle}"}
]
}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
rendered = template.render({"subtitle": "Sub"})
assert rendered[0]["content"] == "Sub"
def test_render_without_optional_variable(self, temp_dir):
"""测试不提供可选变量时使用默认值"""
template_data = {
"vars": [{"name": "subtitle", "required": False, "default": "Default"}],
"elements": [
{"type": "text", "box": [1, 2, 8, 1], "content": "{subtitle}"}
]
}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
rendered = template.render({})
assert rendered[0]["content"] == "Default"
def test_render_missing_required_variable_raises_error(self, temp_dir):
"""测试缺少必需变量会引发错误"""
template_data = {
"vars": [{"name": "title", "required": True}],
"elements": [
{"type": "text", "box": [1, 2, 8, 1], "content": "{title}"}
]
}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
with pytest.raises(YAMLError, match="缺少必需变量"):
template.render({})
def test_render_with_default_value(self, temp_dir):
"""测试使用默认值渲染"""
template_data = {
"vars": [{"name": "title", "required": False, "default": "Default Title"}],
"elements": [
{"type": "text", "box": [1, 2, 8, 1], "content": "{title}"}
]
}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
rendered = template.render({})
assert rendered[0]["content"] == "Default Title"
def test_render_filters_elements_by_visible_condition(self, temp_dir):
"""测试根据 visible 条件过滤元素"""
template_data = {
"vars": [{"name": "show", "required": False, "default": ""}],
"elements": [
{"type": "text", "box": [1, 2, 8, 1], "content": "Always"},
{"type": "text", "box": [1, 3, 8, 1], "content": "Conditional", "visible": "{show != ''}"}
]
}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
# 不显示条件元素
rendered = template.render({})
assert len(rendered) == 1
# 显示条件元素
rendered = template.render({"show": "yes"})
assert len(rendered) == 2
# ============= 边界情况测试 =============
class TestTemplateBoundaryCases:
"""模板边界情况测试"""
def test_nested_variable_resolution(self, temp_dir):
"""测试嵌套变量解析"""
template_data = {
"vars": [
{"name": "prefix", "required": True},
{"name": "title", "required": False, "default": "{prefix} Title"}
],
"elements": [
{"type": "text", "box": [1, 2, 8, 1], "content": "{title}"}
]
}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
rendered = template.render({"prefix": "My"})
assert rendered[0]["content"] == "My Title"
def test_variable_with_special_characters(self, temp_dir):
"""测试包含特殊字符的变量"""
template_data = {
"vars": [{"name": "text", "required": True}],
"elements": [
{"type": "text", "box": [1, 2, 8, 1], "content": "{text}"}
]
}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
rendered = template.render({"text": "Hello & <World>"})
assert rendered[0]["content"] == "Hello & <World>"
def test_variable_with_numeric_value(self, temp_dir):
"""测试数值变量"""
template_data = {
"vars": [{"name": "count", "required": True}],
"elements": [
{"type": "text", "box": [1, 2, 8, 1], "content": "Count: {count}"}
]
}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
rendered = template.render({"count": 42})
assert rendered[0]["content"] == "Count: 42"
def test_empty_vars_list(self, temp_dir):
"""测试空变量列表"""
template_data = {
"vars": [],
"elements": [
{"type": "text", "box": [1, 2, 8, 1], "content": "Static"}
]
}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
rendered = template.render({})
assert len(rendered) == 1
assert rendered[0]["content"] == "Static"
def test_multiple_visible_conditions(self, temp_dir):
"""测试多个可见性条件"""
template_data = {
"vars": [
{"name": "show_a", "required": False, "default": ""},
{"name": "show_b", "required": False, "default": ""}
],
"elements": [
{"type": "text", "box": [1, 2, 8, 1], "content": "A", "visible": "{show_a != ''}"},
{"type": "text", "box": [1, 3, 8, 1], "content": "B", "visible": "{show_b != ''}"}
]
}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
rendered = template.render({"show_a": "yes"})
assert len(rendered) == 1
assert rendered[0]["content"] == "A"
def test_variable_in_position(self, temp_dir):
"""测试位置中的变量"""
template_data = {
"vars": [{"name": "x", "required": True}],
"elements": [
{"type": "text", "box": ["{x}", 2, 8, 1], "content": "Text"}
]
}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
rendered = template.render({"x": "5"})
assert rendered[0]["box"][0] == 5
def test_empty_template_elements(self, temp_dir):
"""测试空元素列表"""
template_data = {
"vars": [],
"elements": []
}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
rendered = template.render({})
assert len(rendered) == 0
def test_variable_replacement_in_font(self, temp_dir):
"""测试字体属性中的变量替换"""
template_data = {
"vars": [{"name": "size", "required": True}],
"elements": [
{
"type": "text",
"box": [1, 2, 8, 1],
"content": "Text",
"font": {"size": "{size}"}
}
]
}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
rendered = template.render({"size": "24"})
assert rendered[0]["font"]["size"] == 24
# ============= 图片路径解析测试 =============
class TestImagePathResolution:
"""图片路径解析测试"""
def test_render_resolves_relative_image_paths(self, temp_dir, sample_image):
"""测试渲染时解析相对图片路径"""
template_data = {
"vars": [],
"elements": [
{"type": "image", "box": [1, 2, 8, 4], "src": sample_image.name}
]
}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
rendered = template.render({})
# 相对路径应该被解析为绝对路径
assert Path(rendered[0]["src"]).is_absolute()
assert rendered[0]["src"] == str(temp_dir / sample_image.name)
def test_render_preserves_absolute_image_paths(self, temp_dir, sample_image):
"""测试渲染时保留绝对图片路径"""
template_data = {
"vars": [],
"elements": [
{"type": "image", "box": [1, 2, 8, 4], "src": str(sample_image)}
]
}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
rendered = template.render({})
# 绝对路径应该保持不变
assert rendered[0]["src"] == str(sample_image)
def test_render_without_base_dir(self, temp_dir):
"""测试没有 base_dir 时不解析路径"""
template_data = {
"vars": [],
"elements": [
{"type": "image", "box": [1, 2, 8, 4], "src": "image.png"}
]
}
template = Template.from_data(template_data, "test", base_dir=None)
rendered = template.render({})
# 没有 base_dir 时,相对路径保持不变
assert rendered[0]["src"] == "image.png"
def test_render_resolves_nested_image_paths(self, temp_dir):
"""测试解析嵌套结构中的图片路径"""
template_data = {
"vars": [],
"elements": [
{
"type": "image",
"box": [1, 2, 8, 4],
"src": "images/logo.png"
}
]
}
template = Template.from_data(template_data, "test", base_dir=temp_dir)
rendered = template.render({})
# 相对路径应该被解析
assert Path(rendered[0]["src"]).is_absolute()
assert rendered[0]["src"] == str(temp_dir / "images" / "logo.png")