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; /** * 与前端引擎的一致性测试。 *

* 渲染引擎与规则引擎在前端({@code src/components/quality/engine})各有一套实现, * 设计器预览用前端那套、服务端生成报告用这套,两边结果不一致就等于「预览与出件不符」, * 那是比少个功能更难查的问题。所以这里用同一份 fixture 跑两边,逐字比对产物。 *

* 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 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 rules = QualityReportEngine.rulesOf(schema); ReportContext evaluated = ReportEvaluator.evaluate(readContext(), rules).context(); Map> 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 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> buildScopes(ReportContext evaluated) { List items = evaluated.getInspectionItems(); Map report = evaluated.toScope(); report.put("index", 0); Map item2 = evaluated.toScope(); item2.put("item", ReportContext.itemMap(items.get(1))); item2.put("index", 2); Map item3 = evaluated.toScope(); item3.put("item", ReportContext.itemMap(items.get(2))); item3.put("index", 3); Map> 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 readErrors() throws IOException { JsonNode node = MAPPER.readTree(readFixture("expected-rules.json")).get("errors"); List 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); } } }