8 小时以前 9bad721754fe8bbe2e5f459d0706e0fefac569f3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
package cn.iocoder.yudao.module.qcreport.engine;
 
import cn.iocoder.yudao.module.qcreport.dal.dataobject.version.ReportTemplateSchema;
import cn.iocoder.yudao.module.qcreport.engine.context.InspectionItem;
import cn.iocoder.yudao.module.qcreport.engine.context.ReportContext;
import cn.iocoder.yudao.module.qcreport.engine.render.RenderOutcome;
import cn.iocoder.yudao.module.qcreport.engine.report.EvaluationOutcome;
import cn.iocoder.yudao.module.qcreport.engine.report.ReportEvaluator;
import cn.iocoder.yudao.module.qcreport.engine.rule.QualityRuleDefinition;
import cn.iocoder.yudao.module.qcreport.engine.rule.RuleEngine;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
 
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
 
/**
 * 与前端引擎的一致性测试。
 * <p>
 * 渲染引擎与规则引擎在前端({@code src/components/quality/engine})各有一套实现,
 * 设计器预览用前端那套、服务端生成报告用这套,两边结果不一致就等于「预览与出件不符」,
 * 那是比少个功能更难查的问题。所以这里用同一份 fixture 跑两边,逐字比对产物。
 * <p>
 * fixture 与期望产物都在 {@code src/test/resources/qcreport/}:
 * schema.json / context.json 是输入,expected-html.html 与 expected-rules.json 是前端引擎的产物。
 * 期望产物由前端引擎跑 {@code mom-pro2-before/.qc-conformance/render-ts.ts} 生成后冻结;
 * 改动任何一侧的引擎后要重新生成,再让这个测试把差异顶出来。
 */
class FrontendConformanceTest {
 
    private static final ObjectMapper MAPPER = new ObjectMapper();
 
    private static final String FIXTURE = "qcreport/";
 
    @Test
    @DisplayName("渲染产物与前端引擎逐字一致")
    void renderMatchesFrontendEngine() throws IOException {
        ReportTemplateSchema schema = readSchema();
        ReportContext context = readContext();
 
        RenderOutcome outcome = QualityReportEngine.render(schema, context);
 
        assertEquals(readFixture("expected-html.html"), outcome.html(),
                "服务端渲染的 HTML 与前端引擎不一致,预览和出件会对不上");
        assertEquals(readErrors(), outcome.errors(), "数据缺口清单与前端引擎不一致");
    }
 
    @Test
    @DisplayName("判定结果与前端引擎一致")
    void evaluationMatchesFrontendEngine() throws IOException {
        ReportTemplateSchema schema = readSchema();
        ReportContext context = readContext();
 
        EvaluationOutcome evaluated = ReportEvaluator.evaluate(context, QualityReportEngine.rulesOf(schema));
 
        assertEquals("FAIL", evaluated.context().getReport().getResult());
        assertEquals("不合格", evaluated.context().getReport().getResultText());
        assertEquals(3, evaluated.context().getReport().getTotal());
        assertEquals(1, evaluated.context().getReport().getPassCount());
        assertEquals(2, evaluated.context().getReport().getFailCount());
        assertEquals("33.33%", evaluated.context().getReport().getPassRate());
 
        List<InspectionItem> items = evaluated.context().getInspectionItems();
        assertEquals(List.of("FAIL", "PASS", "FAIL"), List.of(
                items.get(0).getResult(), items.get(1).getResult(), items.get(2).getResult()),
                "外观无上下限但有规则、长度在规格内、硬度超上限");
    }
 
    @Test
    @DisplayName("规则求值与前端引擎一致(含报错文案)")
    void rulesMatchFrontendEngine() throws IOException {
        ReportTemplateSchema schema = readSchema();
        List<QualityRuleDefinition> rules = QualityReportEngine.rulesOf(schema);
        ReportContext evaluated = ReportEvaluator.evaluate(readContext(), rules).context();
 
        Map<String, Map<String, Object>> scopes = buildScopes(evaluated);
        JsonNode expected = MAPPER.readTree(readFixture("expected-rules.json"));
 
        for (JsonNode item : expected.get("results")) {
            String expression = item.get("expression").asText();
            String scope = item.get("scope").asText();
            Map<String, Object> context = scopes.get(scope);
            String label = scope + " / " + expression;
 
            if (item.has("error")) {
                RuntimeException error = assertThrows(RuntimeException.class,
                        () -> RuleEngine.evaluate(expression, context), label);
                assertEquals(item.get("error").asText(), error.getMessage(), label);
                continue;
            }
            Object value = RuleEngine.evaluate(expression, context);
            assertEquals(item.get("kind").asText(), kindOf(value), label + " 的结果类型不一致");
            assertEquals(item.get("text").asText(), JsValues.asText(value), label + " 的结果文本不一致");
        }
    }
 
    /* ------------------------------ 辅助 ------------------------------ */
 
    private static Map<String, Map<String, Object>> buildScopes(ReportContext evaluated) {
        List<InspectionItem> items = evaluated.getInspectionItems();
 
        Map<String, Object> report = evaluated.toScope();
        report.put("index", 0);
 
        Map<String, Object> item2 = evaluated.toScope();
        item2.put("item", ReportContext.itemMap(items.get(1)));
        item2.put("index", 2);
 
        Map<String, Object> item3 = evaluated.toScope();
        item3.put("item", ReportContext.itemMap(items.get(2)));
        item3.put("index", 3);
 
        Map<String, Map<String, Object>> scopes = new LinkedHashMap<>();
        scopes.put("report", report);
        scopes.put("item2", item2);
        scopes.put("item3", item3);
        return scopes;
    }
 
    /** 与 JS 的 typeof 对齐:null/undefined 记为 nil,数组与对象都是 object */
    private static String kindOf(Object value) {
        if (value == null) {
            return "nil";
        }
        if (value instanceof Boolean) {
            return "boolean";
        }
        if (value instanceof Number) {
            return "number";
        }
        if (value instanceof String) {
            return "string";
        }
        return "object";
    }
 
    private static List<String> readErrors() throws IOException {
        JsonNode node = MAPPER.readTree(readFixture("expected-rules.json")).get("errors");
        List<String> errors = new ArrayList<>();
        node.forEach(item -> errors.add(item.asText()));
        return errors;
    }
 
    private static ReportTemplateSchema readSchema() throws IOException {
        return MAPPER.readValue(readFixture("schema.json"), ReportTemplateSchema.class);
    }
 
    private static ReportContext readContext() throws IOException {
        return MAPPER.readValue(readFixture("context.json"), ReportContext.class);
    }
 
    private static String readFixture(String name) throws IOException {
        try (InputStream in = FrontendConformanceTest.class.getClassLoader()
                .getResourceAsStream(FIXTURE + name)) {
            if (in == null) {
                throw new IOException("测试 fixture 缺失:" + FIXTURE + name);
            }
            return new String(in.readAllBytes(), StandardCharsets.UTF_8);
        }
    }
 
}