package cn.iocoder.yudao.module.qcreport.engine.context;
|
|
import cn.iocoder.yudao.module.qcreport.dal.dataobject.version.ReportTemplateSchema;
|
import cn.iocoder.yudao.module.qcreport.engine.QualityReportEngine;
|
import cn.iocoder.yudao.module.qcreport.engine.render.RenderOutcome;
|
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.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
/**
|
* 数据快照的编解码。
|
* <p>
|
* 这里是「历史报告可复现」的唯一实现路径,所以第一条用例不是比对快照本身,
|
* 而是<b>走完整链路</b>:出件 → 冻结快照 → 从快照读回 → 重渲,产物必须与首次逐字相同。
|
* 只要这条断了,报告实例就会出现「生成时一个样、重新生成后另一个样」,
|
* 而库里的数据看不出任何异常——这种问题事后极难追。
|
* <p>
|
* 用的是与对拍同一份 fixture({@code schema.json} / {@code context.json} /
|
* {@code expected-html.html}),所以「重渲一致」与「前后端一致」在这里是一次性验证的。
|
*/
|
class ReportContextCodecTest {
|
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
|
private static final String FIXTURE = "qcreport/";
|
|
@Test
|
@DisplayName("冻结快照读回后重渲,产物与首次出件逐字相同")
|
void roundTripReproducesHtml() throws IOException {
|
ReportTemplateSchema schema = readSchema();
|
|
// 1. 首次出件:判定 + 渲染
|
RenderOutcome first = QualityReportEngine.render(schema, readContext());
|
assertEquals(readFixture("expected-html.html"), first.html(), "首次出件就与前端引擎不一致");
|
|
// 2. 冻结快照(存的就是判定后的上下文)
|
Map<String, Object> snapshot = ReportContextCodec.toMap(first.context());
|
|
// 3. 从快照读回,重渲
|
RenderOutcome second = QualityReportEngine.render(schema, ReportContextCodec.fromMap(snapshot));
|
|
assertEquals(first.html(), second.html(),
|
"从数据快照重渲的 HTML 与首次出件不一致,历史报告不再可复现");
|
assertEquals(first.errors(), second.errors(), "重渲的数据缺口清单与首次出件不一致");
|
}
|
|
@Test
|
@DisplayName("快照里带着算好的判定结果与合格率")
|
void snapshotCarriesEvaluation() throws IOException {
|
RenderOutcome outcome = QualityReportEngine.render(readSchema(), readContext());
|
|
Map<String, Object> snapshot = ReportContextCodec.toMap(outcome.context());
|
|
@SuppressWarnings("unchecked")
|
Map<String, Object> report = (Map<String, Object>) snapshot.get("report");
|
assertEquals("FAIL", report.get("result"));
|
assertEquals(3, report.get("total"));
|
assertEquals("33.33%", report.get("passRate"));
|
|
@SuppressWarnings("unchecked")
|
List<Object> items = (List<Object>) snapshot.get("inspectionItems");
|
assertEquals(3, items.size());
|
assertEquals("FAIL", ((Map<?, ?>) items.get(0)).get("result"));
|
assertEquals("PASS", ((Map<?, ?>) items.get(1)).get("result"));
|
}
|
|
@Test
|
@DisplayName("编解码往返不丢字段:文本、整数、上下限、空值都原样回来")
|
void roundTripIsLossless() {
|
InspectionItem item = new InspectionItem()
|
.setIndex(7)
|
.setItemCode("LEN")
|
.setItemName("长度")
|
.setStandardValue("10±0.5")
|
.setActualValue("10.2")
|
.setUnit("mm")
|
// 检测要求与检测方法各占一处:模板里常各写一列,往返时必须都不丢
|
.setRequirement("10±0.5")
|
.setCheckMethod("GB/T 1804-2000")
|
.setUpperLimit(10.5D)
|
.setLowerLimit(9.5D)
|
.setResult("PASS")
|
.setResultText("合格")
|
.setRemark("抽检");
|
ReportContext original = new ReportContext()
|
.setReport(new ReportFields()
|
.setReportNo("QR20260918-0001")
|
.setReportName("出货检验报告")
|
.setProductName("法兰")
|
.setInspector("张三")
|
.setResult("PASS")
|
.setResultText("合格")
|
.setConclusion("准予出货")
|
.setPassRate("100.00%")
|
.setTotal(1)
|
.setPassCount(1)
|
.setFailCount(0))
|
.setInspectionItems(new ArrayList<>(List.of(item)));
|
|
ReportContext restored = ReportContextCodec.fromMap(ReportContextCodec.toMap(original));
|
|
assertEquals(original, restored, "往返后有字段变了");
|
}
|
|
@Test
|
@DisplayName("没有规格区间的项,上下限保持为 null,不会被填成 0")
|
void absentLimitsStayNull() {
|
String itemName = "外观";
|
InspectionItem original = new InspectionItem().setIndex(1).setItemName(itemName);
|
|
ReportContext restored = ReportContextCodec.fromMap(
|
ReportContextCodec.toMap(new ReportContext().setInspectionItems(new ArrayList<>(List.of(original)))));
|
|
InspectionItem item = restored.getInspectionItems().get(0);
|
assertEquals(itemName, item.getItemName());
|
assertNull(item.getUpperLimit(), "没写上限的项被判成「上限 0」,规格判定会全错");
|
assertNull(item.getLowerLimit());
|
}
|
|
@Test
|
@DisplayName("字段不全的快照照常读回:缺的字段是「没值」(空串),不是报错")
|
void partialSnapshotReadsAsEmpty() {
|
Map<String, Object> snapshot = new LinkedHashMap<>();
|
Map<String, Object> report = new LinkedHashMap<>();
|
report.put("reportNo", "QR20260918-0001");
|
report.put("reportName", "来料检验报告");
|
snapshot.put("report", report);
|
snapshot.put("inspectionItems", List.of(Map.of("index", 1, "itemName", "外观")));
|
|
ReportContext context = ReportContextCodec.fromMap(snapshot);
|
|
assertEquals("QR20260918-0001", context.getReport().getReportNo());
|
assertEquals("来料检验报告", context.getReport().getReportName());
|
assertEquals("", context.getReport().getProductName(), "缺的字段应当是空串");
|
assertEquals(0, context.getReport().getTotal());
|
assertEquals(1, context.getInspectionItems().size());
|
assertEquals("", context.getInspectionItems().get(0).getActualValue());
|
}
|
|
@Test
|
@DisplayName("空快照读回空白上下文,不抛异常")
|
void nullAndEmptySnapshotYieldEmptyContext() {
|
assertNotNull(ReportContextCodec.fromMap(null).getReport());
|
assertTrue(ReportContextCodec.fromMap(null).getInspectionItems().isEmpty());
|
assertTrue(ReportContextCodec.fromMap(new LinkedHashMap<>()).getInspectionItems().isEmpty());
|
assertNotNull(ReportContextCodec.toMap(null).get("report"));
|
}
|
|
@Test
|
@DisplayName("检验项数组里混进非对象元素时跳过该元素,不让整份报告打不开")
|
void malformedItemIsSkipped() {
|
Map<String, Object> snapshot = new LinkedHashMap<>();
|
snapshot.put("inspectionItems", List.of(
|
Map.of("index", 1, "itemName", "外观"),
|
"不是对象",
|
Map.of("index", 3, "itemName", "硬度")));
|
|
ReportContext context = ReportContextCodec.fromMap(snapshot);
|
|
assertEquals(2, context.getInspectionItems().size());
|
assertEquals("外观", context.getInspectionItems().get(0).getItemName());
|
assertEquals("硬度", context.getInspectionItems().get(1).getItemName());
|
}
|
|
/* ------------------------------ 辅助 ------------------------------ */
|
|
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 = ReportContextCodecTest.class.getClassLoader()
|
.getResourceAsStream(FIXTURE + name)) {
|
if (in == null) {
|
throw new IOException("测试 fixture 缺失:" + FIXTURE + name);
|
}
|
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
}
|
}
|
|
}
|