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);
|
}
|
}
|
|
}
|