457 lines
13 KiB
Python
457 lines
13 KiB
Python
"""
|
|
图表验证器和修复器的测试用例。
|
|
|
|
运行测试:
|
|
python -m pytest ReportEngine/utils/test_chart_validator.py -v
|
|
"""
|
|
|
|
import pytest
|
|
from ReportEngine.utils.chart_validator import (
|
|
ChartValidator,
|
|
ChartRepairer,
|
|
ValidationResult,
|
|
RepairResult,
|
|
create_chart_validator,
|
|
create_chart_repairer
|
|
)
|
|
|
|
|
|
class TestChartValidator:
|
|
"""测试ChartValidator类"""
|
|
|
|
def setup_method(self):
|
|
"""每个测试前初始化"""
|
|
self.validator = create_chart_validator()
|
|
|
|
def test_valid_bar_chart(self):
|
|
"""测试有效的柱状图"""
|
|
widget_block = {
|
|
"type": "widget",
|
|
"widgetType": "chart.js/bar",
|
|
"widgetId": "chart-001",
|
|
"props": {
|
|
"type": "bar",
|
|
"title": "销售数据"
|
|
},
|
|
"data": {
|
|
"labels": ["一月", "二月", "三月"],
|
|
"datasets": [
|
|
{
|
|
"label": "销售额",
|
|
"data": [100, 200, 150]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
result = self.validator.validate(widget_block)
|
|
assert result.is_valid
|
|
assert len(result.errors) == 0
|
|
|
|
def test_valid_line_chart(self):
|
|
"""测试有效的折线图"""
|
|
widget_block = {
|
|
"type": "widget",
|
|
"widgetType": "chart.js/line",
|
|
"widgetId": "chart-002",
|
|
"props": {
|
|
"type": "line"
|
|
},
|
|
"data": {
|
|
"labels": ["周一", "周二", "周三"],
|
|
"datasets": [
|
|
{
|
|
"label": "访问量",
|
|
"data": [50, 75, 60]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
result = self.validator.validate(widget_block)
|
|
assert result.is_valid
|
|
|
|
def test_valid_pie_chart(self):
|
|
"""测试有效的饼图"""
|
|
widget_block = {
|
|
"widgetType": "chart.js/pie",
|
|
"props": {"type": "pie"},
|
|
"data": {
|
|
"labels": ["A", "B", "C"],
|
|
"datasets": [
|
|
{
|
|
"data": [30, 40, 30]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
result = self.validator.validate(widget_block)
|
|
assert result.is_valid
|
|
|
|
def test_missing_widgetType(self):
|
|
"""测试缺少widgetType"""
|
|
widget_block = {
|
|
"props": {},
|
|
"data": {}
|
|
}
|
|
|
|
result = self.validator.validate(widget_block)
|
|
assert not result.is_valid
|
|
assert "widgetType" in result.errors[0]
|
|
|
|
def test_missing_data_field(self):
|
|
"""测试缺少data字段"""
|
|
widget_block = {
|
|
"widgetType": "chart.js/bar",
|
|
"props": {"type": "bar"}
|
|
}
|
|
|
|
result = self.validator.validate(widget_block)
|
|
assert not result.is_valid
|
|
assert "data" in result.errors[0]
|
|
|
|
def test_missing_datasets(self):
|
|
"""测试缺少datasets"""
|
|
widget_block = {
|
|
"widgetType": "chart.js/bar",
|
|
"props": {"type": "bar"},
|
|
"data": {
|
|
"labels": ["A", "B"]
|
|
}
|
|
}
|
|
|
|
result = self.validator.validate(widget_block)
|
|
assert not result.is_valid
|
|
assert "datasets" in result.errors[0]
|
|
|
|
def test_empty_datasets(self):
|
|
"""测试空datasets"""
|
|
widget_block = {
|
|
"widgetType": "chart.js/bar",
|
|
"props": {"type": "bar"},
|
|
"data": {
|
|
"labels": ["A", "B"],
|
|
"datasets": []
|
|
}
|
|
}
|
|
|
|
result = self.validator.validate(widget_block)
|
|
assert not result.is_valid
|
|
assert "空" in result.errors[0]
|
|
|
|
def test_missing_labels_for_bar_chart(self):
|
|
"""测试柱状图缺少labels"""
|
|
widget_block = {
|
|
"widgetType": "chart.js/bar",
|
|
"props": {"type": "bar"},
|
|
"data": {
|
|
"datasets": [
|
|
{
|
|
"label": "系列1",
|
|
"data": [10, 20, 30]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
result = self.validator.validate(widget_block)
|
|
assert not result.is_valid
|
|
assert "labels" in result.errors[0]
|
|
|
|
def test_invalid_data_type(self):
|
|
"""测试数据类型错误"""
|
|
widget_block = {
|
|
"widgetType": "chart.js/bar",
|
|
"props": {"type": "bar"},
|
|
"data": {
|
|
"labels": ["A", "B"],
|
|
"datasets": [
|
|
{
|
|
"label": "系列1",
|
|
"data": ["abc", "def"] # 应该是数值
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
result = self.validator.validate(widget_block)
|
|
assert not result.is_valid
|
|
assert "数值类型" in result.errors[0]
|
|
|
|
def test_data_length_mismatch_warning(self):
|
|
"""测试数据长度不匹配(警告)"""
|
|
widget_block = {
|
|
"widgetType": "chart.js/bar",
|
|
"props": {"type": "bar"},
|
|
"data": {
|
|
"labels": ["A", "B", "C"],
|
|
"datasets": [
|
|
{
|
|
"label": "系列1",
|
|
"data": [10, 20] # 长度不匹配
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
result = self.validator.validate(widget_block)
|
|
# 长度不匹配是警告,不是错误
|
|
assert len(result.warnings) > 0
|
|
assert "不匹配" in result.warnings[0]
|
|
|
|
def test_scatter_chart(self):
|
|
"""测试散点图(特殊数据格式)"""
|
|
widget_block = {
|
|
"widgetType": "chart.js/scatter",
|
|
"props": {"type": "scatter"},
|
|
"data": {
|
|
"datasets": [
|
|
{
|
|
"label": "数据点",
|
|
"data": [
|
|
{"x": 10, "y": 20},
|
|
{"x": 15, "y": 25}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
result = self.validator.validate(widget_block)
|
|
assert result.is_valid
|
|
|
|
def test_non_chart_widget(self):
|
|
"""测试非图表类型的widget(应该跳过验证)"""
|
|
widget_block = {
|
|
"widgetType": "custom/widget",
|
|
"props": {},
|
|
"data": {}
|
|
}
|
|
|
|
result = self.validator.validate(widget_block)
|
|
# 非chart.js类型,跳过验证,返回valid
|
|
assert result.is_valid
|
|
|
|
|
|
class TestChartRepairer:
|
|
"""测试ChartRepairer类"""
|
|
|
|
def setup_method(self):
|
|
"""每个测试前初始化"""
|
|
self.validator = create_chart_validator()
|
|
self.repairer = create_chart_repairer(validator=self.validator)
|
|
|
|
def test_repair_missing_props(self):
|
|
"""测试修复缺少props字段"""
|
|
widget_block = {
|
|
"widgetType": "chart.js/bar",
|
|
"data": {
|
|
"labels": ["A", "B"],
|
|
"datasets": [
|
|
{
|
|
"label": "系列1",
|
|
"data": [10, 20]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
result = self.repairer.repair(widget_block)
|
|
assert result.success
|
|
assert "props" in result.repaired_block
|
|
assert result.method == "local"
|
|
|
|
def test_repair_missing_chart_type(self):
|
|
"""测试修复缺少图表类型"""
|
|
widget_block = {
|
|
"widgetType": "chart.js/bar",
|
|
"props": {},
|
|
"data": {
|
|
"labels": ["A", "B"],
|
|
"datasets": [
|
|
{
|
|
"label": "系列1",
|
|
"data": [10, 20]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
result = self.repairer.repair(widget_block)
|
|
assert result.success
|
|
assert result.repaired_block["props"]["type"] == "bar"
|
|
assert "图表类型" in str(result.changes)
|
|
|
|
def test_repair_missing_datasets(self):
|
|
"""测试修复缺少datasets"""
|
|
widget_block = {
|
|
"widgetType": "chart.js/bar",
|
|
"props": {"type": "bar"},
|
|
"data": {
|
|
"labels": ["A", "B"]
|
|
}
|
|
}
|
|
|
|
result = self.repairer.repair(widget_block)
|
|
assert result.success
|
|
assert "datasets" in result.repaired_block["data"]
|
|
assert isinstance(result.repaired_block["data"]["datasets"], list)
|
|
|
|
def test_repair_missing_labels(self):
|
|
"""测试修复缺少labels"""
|
|
widget_block = {
|
|
"widgetType": "chart.js/bar",
|
|
"props": {"type": "bar"},
|
|
"data": {
|
|
"datasets": [
|
|
{
|
|
"label": "系列1",
|
|
"data": [10, 20, 30]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
result = self.repairer.repair(widget_block)
|
|
assert result.success
|
|
assert "labels" in result.repaired_block["data"]
|
|
assert len(result.repaired_block["data"]["labels"]) == 3
|
|
|
|
def test_repair_data_length_mismatch(self):
|
|
"""测试修复数据长度不匹配"""
|
|
widget_block = {
|
|
"widgetType": "chart.js/bar",
|
|
"props": {"type": "bar"},
|
|
"data": {
|
|
"labels": ["A", "B", "C", "D"],
|
|
"datasets": [
|
|
{
|
|
"label": "系列1",
|
|
"data": [10, 20] # 长度不足
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
result = self.repairer.repair(widget_block)
|
|
assert result.success
|
|
# 应该补充到4个元素
|
|
assert len(result.repaired_block["data"]["datasets"][0]["data"]) == 4
|
|
|
|
def test_repair_string_to_number(self):
|
|
"""测试修复字符串类型的数值"""
|
|
widget_block = {
|
|
"widgetType": "chart.js/bar",
|
|
"props": {"type": "bar"},
|
|
"data": {
|
|
"labels": ["A", "B"],
|
|
"datasets": [
|
|
{
|
|
"label": "系列1",
|
|
"data": ["10", "20"] # 字符串数值
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
result = self.repairer.repair(widget_block)
|
|
assert result.success
|
|
# 应该转换为数值
|
|
assert isinstance(result.repaired_block["data"]["datasets"][0]["data"][0], float)
|
|
|
|
def test_repair_construct_datasets_from_values(self):
|
|
"""测试从values字段构造datasets"""
|
|
widget_block = {
|
|
"widgetType": "chart.js/bar",
|
|
"props": {"type": "bar"},
|
|
"data": {
|
|
"labels": ["A", "B"],
|
|
"values": [10, 20] # 使用values而不是datasets
|
|
}
|
|
}
|
|
|
|
result = self.repairer.repair(widget_block)
|
|
assert result.success
|
|
assert "datasets" in result.repaired_block["data"]
|
|
assert len(result.repaired_block["data"]["datasets"]) > 0
|
|
|
|
def test_no_repair_needed(self):
|
|
"""测试不需要修复的情况"""
|
|
widget_block = {
|
|
"widgetType": "chart.js/bar",
|
|
"props": {"type": "bar"},
|
|
"data": {
|
|
"labels": ["A", "B"],
|
|
"datasets": [
|
|
{
|
|
"label": "系列1",
|
|
"data": [10, 20]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
result = self.repairer.repair(widget_block)
|
|
assert result.success
|
|
assert result.method == "none"
|
|
assert len(result.changes) == 0
|
|
|
|
def test_repair_adds_default_label(self):
|
|
"""测试修复添加默认label"""
|
|
widget_block = {
|
|
"widgetType": "chart.js/bar",
|
|
"props": {"type": "bar"},
|
|
"data": {
|
|
"labels": ["A", "B"],
|
|
"datasets": [
|
|
{
|
|
# 缺少label
|
|
"data": [10, 20]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
result = self.repairer.repair(widget_block)
|
|
assert result.success
|
|
assert "label" in result.repaired_block["data"]["datasets"][0]
|
|
|
|
|
|
class TestValidatorIntegration:
|
|
"""集成测试"""
|
|
|
|
def test_full_validation_and_repair_workflow(self):
|
|
"""测试完整的验证和修复流程"""
|
|
validator = create_chart_validator()
|
|
repairer = create_chart_repairer(validator=validator)
|
|
|
|
# 一个有多个问题的图表
|
|
widget_block = {
|
|
"widgetType": "chart.js/bar",
|
|
"data": {
|
|
"datasets": [
|
|
{
|
|
"data": ["10", "20", "30"] # 字符串数值
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
# 1. 验证(应该失败)
|
|
validation = validator.validate(widget_block)
|
|
assert not validation.is_valid
|
|
|
|
# 2. 修复
|
|
repair_result = repairer.repair(widget_block, validation)
|
|
assert repair_result.success
|
|
|
|
# 3. 再次验证(应该通过)
|
|
final_validation = validator.validate(repair_result.repaired_block)
|
|
assert final_validation.is_valid
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# 运行测试
|
|
pytest.main([__file__, "-v", "--tb=short"])
|