Repair and Optimize the Chart Rendering
This commit is contained in:
@@ -0,0 +1,456 @@
|
||||
"""
|
||||
图表验证器和修复器的测试用例。
|
||||
|
||||
运行测试:
|
||||
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"])
|
||||
Reference in New Issue
Block a user