This commit is contained in:
@@ -7,6 +7,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.EnumMap;
|
import java.util.EnumMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将 {@link CheckReport} 渲染为企微 Markdown / 控制台文本。
|
* 将 {@link CheckReport} 渲染为企微 Markdown / 控制台文本。
|
||||||
@@ -53,34 +54,32 @@ public class ReportBuilder {
|
|||||||
appendKeyLine(sb, kc.getKeyPattern());
|
appendKeyLine(sb, kc.getKeyPattern());
|
||||||
String oldJson = nvl(kc.getOldSkeletonJson());
|
String oldJson = nvl(kc.getOldSkeletonJson());
|
||||||
String newJson = nvl(kc.getNewSkeletonJson());
|
String newJson = nvl(kc.getNewSkeletonJson());
|
||||||
|
Set<String> oldHighlight = SkeletonAnnotator.pathsForOldSkeleton(kc.getFieldDetails());
|
||||||
|
Set<String> newHighlight = SkeletonAnnotator.pathsForNewSkeleton(kc.getFieldDetails());
|
||||||
|
String oldRendered = oldJson.isEmpty()
|
||||||
|
? "" : SkeletonAnnotator.annotateAsCodeBlock(oldJson, oldHighlight);
|
||||||
|
String newRendered = newJson.isEmpty()
|
||||||
|
? "" : SkeletonAnnotator.annotateAsCodeBlock(newJson, newHighlight);
|
||||||
if (oldJson.isEmpty() && !newJson.isEmpty()) {
|
if (oldJson.isEmpty() && !newJson.isEmpty()) {
|
||||||
sb.append(" value 新增为 “").append(fontWarning(newJson)).append("”\n\n");
|
sb.append(" value 新增为 ").append(newRendered).append("\n\n");
|
||||||
} else if (!oldJson.isEmpty() && newJson.isEmpty()) {
|
} else if (!oldJson.isEmpty() && newJson.isEmpty()) {
|
||||||
sb.append(" value 原结构 “").append(fontInfo(oldJson)).append("” 已删除写入\n\n");
|
sb.append(" value 原结构 ").append(oldRendered).append(" 已删除写入\n\n");
|
||||||
} else {
|
} else {
|
||||||
sb.append(" value 值从 “").append(fontInfo(oldJson)).append("”\n");
|
sb.append(" value 值从 ").append(oldRendered).append('\n');
|
||||||
sb.append(" 变更为 “").append(fontWarning(newJson)).append("”\n\n");
|
sb.append(" 变更为 ").append(newRendered).append("\n\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key 行:加粗「Key -->」+ 加粗 key 文本。
|
* Key 行:Key --> + 橙红色 key(企微 warning)。
|
||||||
* 避免 key 末尾 * 与 markdown ** 冲突,结尾加零宽空格再闭合。
|
* 末尾加零宽空格,避免 key 中的 * 干扰后续 markdown。
|
||||||
*/
|
*/
|
||||||
private void appendKeyLine(StringBuilder sb, String keyPattern) {
|
private void appendKeyLine(StringBuilder sb, String keyPattern) {
|
||||||
sb.append("- **Key --> ").append(nvl(keyPattern)).append("\u200b**\n");
|
sb.append("- Key --> <font color=\"warning\">")
|
||||||
}
|
.append(nvl(keyPattern))
|
||||||
|
.append("\u200b</font>\n");
|
||||||
/** 企微 markdown:绿色(旧骨架)。 */
|
|
||||||
private String fontInfo(String text) {
|
|
||||||
return "<font color=\"info\">" + text + "</font>";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 企微 markdown:warning 橙红(新骨架)。 */
|
|
||||||
private String fontWarning(String text) {
|
|
||||||
return "<font color=\"warning\">" + text + "</font>";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
351
src/main/java/com/codechecker/cache/report/SkeletonAnnotator.java
vendored
Normal file
351
src/main/java/com/codechecker/cache/report/SkeletonAnnotator.java
vendored
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
package com.codechecker.cache.report;
|
||||||
|
|
||||||
|
import com.codechecker.cache.diff.ChangeType;
|
||||||
|
import com.codechecker.cache.diff.SchemaChange;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在骨架 JSON 中为改动字段加企微 warning 标注。
|
||||||
|
* <ul>
|
||||||
|
* <li>删除字段 → 标在旧骨架</li>
|
||||||
|
* <li>新增字段 / 新增包装层 → 标在新骨架</li>
|
||||||
|
* <li>路径迁移 → 旧路径标旧骨架、新路径标新骨架</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
final class SkeletonAnnotator {
|
||||||
|
|
||||||
|
private static final String FONT_OPEN = "<font color=\"warning\">";
|
||||||
|
private static final String FONT_CLOSE = "</font>";
|
||||||
|
|
||||||
|
private SkeletonAnnotator() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static Set<String> pathsForOldSkeleton(List<SchemaChange> details) {
|
||||||
|
Set<String> paths = new LinkedHashSet<>();
|
||||||
|
if (details == null) {
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
for (SchemaChange c : details) {
|
||||||
|
if (c == null || c.getChangeType() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c.getChangeType() == ChangeType.FIELD_REMOVED) {
|
||||||
|
addIfPresent(paths, c.getFieldPath());
|
||||||
|
} else if (c.getChangeType() == ChangeType.FIELD_PATH_MOVED) {
|
||||||
|
addIfPresent(paths, c.getOldValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Set<String> pathsForNewSkeleton(List<SchemaChange> details) {
|
||||||
|
Set<String> paths = new LinkedHashSet<>();
|
||||||
|
if (details == null) {
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
for (SchemaChange c : details) {
|
||||||
|
if (c == null || c.getChangeType() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c.getChangeType() == ChangeType.FIELD_ADDED
|
||||||
|
|| c.getChangeType() == ChangeType.WRAPPER_ADDED) {
|
||||||
|
addIfPresent(paths, c.getFieldPath());
|
||||||
|
} else if (c.getChangeType() == ChangeType.FIELD_PATH_MOVED) {
|
||||||
|
addIfPresent(paths, c.getFieldPath());
|
||||||
|
if (c.getNewValue() != null && !c.getNewValue().equals(c.getFieldPath())) {
|
||||||
|
addIfPresent(paths, c.getNewValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标注路径对应的 {@code "field":value} 片段,并包成可与代码块混排的企微 Markdown。
|
||||||
|
* 企微代码块内不解析 font,故整段用反引号分段包裹,改动项单独用 warning+反引号。
|
||||||
|
*/
|
||||||
|
static String annotateAsCodeBlock(String json, Set<String> highlightPaths) {
|
||||||
|
if (json == null || json.isEmpty()) {
|
||||||
|
return "``";
|
||||||
|
}
|
||||||
|
String annotated = annotate(json, highlightPaths);
|
||||||
|
return toMixedCodeMarkdown(annotated);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String annotate(String json, Set<String> highlightPaths) {
|
||||||
|
if (json == null || json.isEmpty() || highlightPaths == null || highlightPaths.isEmpty()) {
|
||||||
|
return json == null ? "" : json;
|
||||||
|
}
|
||||||
|
String result = json;
|
||||||
|
List<String> sorted = new ArrayList<>(highlightPaths);
|
||||||
|
sorted.sort(Comparator.comparingInt(String::length).reversed());
|
||||||
|
for (String path : sorted) {
|
||||||
|
result = wrapOnePath(result, path);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将已含 {@code <font>} 的字符串拆成:普通段走反引号代码块,改动段走 warning+反引号。
|
||||||
|
*/
|
||||||
|
static String toMixedCodeMarkdown(String annotated) {
|
||||||
|
if (annotated == null || annotated.isEmpty()) {
|
||||||
|
return "``";
|
||||||
|
}
|
||||||
|
if (!annotated.contains(FONT_OPEN)) {
|
||||||
|
return "`" + annotated + "`";
|
||||||
|
}
|
||||||
|
StringBuilder out = new StringBuilder();
|
||||||
|
int i = 0;
|
||||||
|
boolean inCode = false;
|
||||||
|
while (i < annotated.length()) {
|
||||||
|
int start = annotated.indexOf(FONT_OPEN, i);
|
||||||
|
if (start < 0) {
|
||||||
|
if (!inCode) {
|
||||||
|
out.append('`');
|
||||||
|
inCode = true;
|
||||||
|
}
|
||||||
|
out.append(annotated.substring(i));
|
||||||
|
out.append('`');
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
if (start > i) {
|
||||||
|
if (!inCode) {
|
||||||
|
out.append('`');
|
||||||
|
inCode = true;
|
||||||
|
}
|
||||||
|
out.append(annotated, i, start);
|
||||||
|
out.append('`');
|
||||||
|
inCode = false;
|
||||||
|
} else if (inCode) {
|
||||||
|
out.append('`');
|
||||||
|
inCode = false;
|
||||||
|
}
|
||||||
|
int contentStart = start + FONT_OPEN.length();
|
||||||
|
int end = annotated.indexOf(FONT_CLOSE, contentStart);
|
||||||
|
if (end < 0) {
|
||||||
|
if (!inCode) {
|
||||||
|
out.append('`');
|
||||||
|
}
|
||||||
|
out.append(annotated.substring(start)).append('`');
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
String frag = annotated.substring(contentStart, end);
|
||||||
|
out.append(FONT_OPEN).append('`').append(frag).append('`').append(FONT_CLOSE);
|
||||||
|
i = end + FONT_CLOSE.length();
|
||||||
|
}
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addIfPresent(Set<String> paths, String path) {
|
||||||
|
if (path != null && !path.trim().isEmpty()) {
|
||||||
|
paths.add(path.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String wrapOnePath(String json, String path) {
|
||||||
|
if (path == null || path.isEmpty()) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
// 已标注过则跳过(避免重复)
|
||||||
|
String key = lastSegment(path);
|
||||||
|
if (key.isEmpty()) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
int propStart = findPropertyStart(json, path);
|
||||||
|
if (propStart < 0) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
// 已在 font 内则跳过
|
||||||
|
int fontBefore = json.lastIndexOf(FONT_OPEN, propStart);
|
||||||
|
int fontCloseBefore = json.lastIndexOf(FONT_CLOSE, propStart);
|
||||||
|
if (fontBefore > fontCloseBefore) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
String needle = "\"" + key + "\"";
|
||||||
|
int keyEnd = propStart + needle.length();
|
||||||
|
int valueEnd = findValueEnd(json, keyEnd);
|
||||||
|
if (valueEnd <= propStart) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
String frag = json.substring(propStart, valueEnd);
|
||||||
|
if (frag.contains(FONT_OPEN)) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
return json.substring(0, propStart)
|
||||||
|
+ FONT_OPEN + frag + FONT_CLOSE
|
||||||
|
+ json.substring(valueEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按点分路径定位属性名起始下标(指向开头引号)。
|
||||||
|
*/
|
||||||
|
private static int findPropertyStart(String json, String path) {
|
||||||
|
List<String> segs = splitPath(path);
|
||||||
|
if (segs.isEmpty()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int searchFrom = 0;
|
||||||
|
int scopeEnd = json.length();
|
||||||
|
for (int s = 0; s < segs.size(); s++) {
|
||||||
|
String seg = segs.get(s);
|
||||||
|
boolean last = s == segs.size() - 1;
|
||||||
|
String needle = "\"" + seg + "\"";
|
||||||
|
int idx = indexOfPropertyInScope(json, needle, searchFrom, scopeEnd);
|
||||||
|
if (idx < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (last) {
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
int keyEnd = idx + needle.length();
|
||||||
|
int valueStart = skipColon(json, keyEnd);
|
||||||
|
if (valueStart >= json.length()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
char c = json.charAt(valueStart);
|
||||||
|
if (c == '{') {
|
||||||
|
searchFrom = valueStart + 1;
|
||||||
|
scopeEnd = findMatchingBracket(json, valueStart);
|
||||||
|
if (scopeEnd < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else if (c == '[') {
|
||||||
|
// 数组:进入第一个元素对象
|
||||||
|
int arrEnd = findMatchingBracket(json, valueStart);
|
||||||
|
int elemObj = json.indexOf('{', valueStart);
|
||||||
|
if (elemObj < 0 || elemObj >= arrEnd) {
|
||||||
|
searchFrom = valueStart + 1;
|
||||||
|
scopeEnd = arrEnd;
|
||||||
|
} else {
|
||||||
|
searchFrom = elemObj + 1;
|
||||||
|
scopeEnd = findMatchingBracket(json, elemObj);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int indexOfPropertyInScope(String json, String needle, int from, int to) {
|
||||||
|
int idx = from;
|
||||||
|
while (idx >= 0 && idx < to) {
|
||||||
|
idx = json.indexOf(needle, idx);
|
||||||
|
if (idx < 0 || idx >= to) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// 粗略校验:前面应是 { 或 ,
|
||||||
|
int p = idx - 1;
|
||||||
|
while (p >= from && Character.isWhitespace(json.charAt(p))) {
|
||||||
|
p--;
|
||||||
|
}
|
||||||
|
if (p < from || json.charAt(p) == '{' || json.charAt(p) == ',' || json.charAt(p) == '[') {
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
idx += needle.length();
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int skipColon(String json, int afterKey) {
|
||||||
|
int i = afterKey;
|
||||||
|
while (i < json.length() && (json.charAt(i) == ':' || Character.isWhitespace(json.charAt(i)))) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int findMatchingBracket(String json, int openIdx) {
|
||||||
|
if (openIdx < 0 || openIdx >= json.length()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
char open = json.charAt(openIdx);
|
||||||
|
char close = open == '{' ? '}' : ']';
|
||||||
|
int depth = 0;
|
||||||
|
for (int i = openIdx; i < json.length(); i++) {
|
||||||
|
char ch = json.charAt(i);
|
||||||
|
if (ch == '"') {
|
||||||
|
i++;
|
||||||
|
while (i < json.length()) {
|
||||||
|
char x = json.charAt(i++);
|
||||||
|
if (x == '\\' && i < json.length()) {
|
||||||
|
i++;
|
||||||
|
} else if (x == '"') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch == open) {
|
||||||
|
depth++;
|
||||||
|
} else if (ch == close) {
|
||||||
|
depth--;
|
||||||
|
if (depth == 0) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int findValueEnd(String json, int afterKey) {
|
||||||
|
int i = skipColon(json, afterKey);
|
||||||
|
if (i >= json.length()) {
|
||||||
|
return json.length();
|
||||||
|
}
|
||||||
|
char c = json.charAt(i);
|
||||||
|
if (c == '"') {
|
||||||
|
i++;
|
||||||
|
while (i < json.length()) {
|
||||||
|
char ch = json.charAt(i++);
|
||||||
|
if (ch == '\\' && i < json.length()) {
|
||||||
|
i++;
|
||||||
|
} else if (ch == '"') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
if (c == '{' || c == '[') {
|
||||||
|
int end = findMatchingBracket(json, i);
|
||||||
|
return end < 0 ? json.length() : end + 1;
|
||||||
|
}
|
||||||
|
while (i < json.length() && json.charAt(i) != ',' && json.charAt(i) != '}' && json.charAt(i) != ']') {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> splitPath(String path) {
|
||||||
|
List<String> segs = new ArrayList<>();
|
||||||
|
if (path == null || path.isEmpty()) {
|
||||||
|
return segs;
|
||||||
|
}
|
||||||
|
for (String raw : path.split("\\.")) {
|
||||||
|
if (raw.isEmpty() || "[]".equals(raw)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (raw.endsWith("[]")) {
|
||||||
|
String name = raw.substring(0, raw.length() - 2);
|
||||||
|
if (!name.isEmpty()) {
|
||||||
|
segs.add(name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
segs.add(raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return segs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String lastSegment(String path) {
|
||||||
|
List<String> segs = splitPath(path);
|
||||||
|
return segs.isEmpty() ? "" : segs.get(segs.size() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,11 @@ import com.codechecker.cache.diff.SchemaChange;
|
|||||||
import com.codechecker.cache.diff.Severity;
|
import com.codechecker.cache.diff.Severity;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
@@ -20,33 +25,69 @@ class ReportBuilderTest {
|
|||||||
report.setModifier("dongzi");
|
report.setModifier("dongzi");
|
||||||
report.setModifyTime("2026-07-13 16:54:17");
|
report.setModifyTime("2026-07-13 16:54:17");
|
||||||
|
|
||||||
SchemaChange detail = new SchemaChange(ChangeType.WRAPPER_ADDED);
|
SchemaChange addedMsg = new SchemaChange(ChangeType.FIELD_ADDED);
|
||||||
detail.setSeverity(Severity.P0);
|
addedMsg.setSeverity(Severity.P1);
|
||||||
detail.setKeyPattern("tenant:db:content:*");
|
addedMsg.setKeyPattern("saas:period-config:migration:current");
|
||||||
detail.setWriteLocation("Helper#put:10");
|
addedMsg.setFieldPath("message");
|
||||||
detail.setFieldPath("vo");
|
addedMsg.setMessage("新增字段 message");
|
||||||
detail.setMessage("新增包装层 vo");
|
|
||||||
report.getChanges().add(detail);
|
SchemaChange addedErr = new SchemaChange(ChangeType.FIELD_ADDED);
|
||||||
|
addedErr.setSeverity(Severity.P1);
|
||||||
|
addedErr.setKeyPattern("saas:period-config:migration:current");
|
||||||
|
addedErr.setFieldPath("lastError");
|
||||||
|
addedErr.setMessage("新增字段 lastError");
|
||||||
|
|
||||||
|
report.getChanges().add(addedMsg);
|
||||||
|
report.getChanges().add(addedErr);
|
||||||
|
|
||||||
KeyStructureChange key = new KeyStructureChange();
|
KeyStructureChange key = new KeyStructureChange();
|
||||||
key.setKeyPattern("tenant:db:content:*");
|
key.setKeyPattern("saas:period-config:migration:current");
|
||||||
key.setOldSkeletonJson("{\"dbName\":\"\",\"linkList\":[{\"id\":\"\"}]}");
|
key.setOldSkeletonJson(
|
||||||
key.setNewSkeletonJson("{\"vo\":{\"dbName\":\"\",\"linkList\":[{\"id\":\"\"}]},\"expiresAtMs\":0}");
|
"{\"taskId\":\"\",\"status\":\"\",\"progress\":{\"totalTenants\":0}}");
|
||||||
key.setSeverity(Severity.P0);
|
key.setNewSkeletonJson(
|
||||||
|
"{\"taskId\":\"\",\"status\":\"\",\"progress\":{\"totalTenants\":0},\"message\":\"\",\"lastError\":\"\"}");
|
||||||
|
key.setSeverity(Severity.P1);
|
||||||
|
key.getFieldDetails().add(addedMsg);
|
||||||
|
key.getFieldDetails().add(addedErr);
|
||||||
report.getKeyChanges().add(key);
|
report.getKeyChanges().add(key);
|
||||||
|
|
||||||
String md = new ReportBuilder("[缓存结构变更]").toMarkdown(report);
|
String md = new ReportBuilder("[缓存结构变更]").toMarkdown(report);
|
||||||
|
|
||||||
assertTrue(md.startsWith("## [缓存结构变更] jnpf-java-cloud"));
|
assertTrue(md.startsWith("## [缓存结构变更] jnpf-java-cloud"));
|
||||||
assertTrue(md.contains("> **分支**: code/redis_change_detection_v1.0"));
|
assertTrue(md.contains("- Key --> <font color=\"warning\">saas:period-config:migration:current\u200b</font>"));
|
||||||
assertTrue(md.contains("> **时间**: 2026-07-13 16:54:17"));
|
assertTrue(md.contains("value 值从"));
|
||||||
assertTrue(md.contains("- **Key --> tenant:db:content:*\u200b**"));
|
assertTrue(md.contains("变更为"));
|
||||||
assertTrue(md.contains("<font color=\"info\">{\"dbName\":\"\",\"linkList\":[{\"id\":\"\"}]}</font>"));
|
// 新增字段仅在新骨架中 warning 标注
|
||||||
assertTrue(md.contains("<font color=\"warning\">{\"vo\":{\"dbName\":\"\",\"linkList\":[{\"id\":\"\"}]},\"expiresAtMs\":0}</font>"));
|
assertTrue(md.contains("<font color=\"warning\">`\"message\":\"\"`</font>"));
|
||||||
|
assertTrue(md.contains("<font color=\"warning\">`\"lastError\":\"\"`</font>"));
|
||||||
|
// 旧骨架不应出现这两个字段的 warning
|
||||||
|
int oldSection = md.indexOf("value 值从");
|
||||||
|
int newSection = md.indexOf("变更为");
|
||||||
|
String oldPart = md.substring(oldSection, newSection);
|
||||||
|
assertFalse(oldPart.contains("<font color=\"warning\">`\"message\":\"\"`</font>"));
|
||||||
assertFalse(md.contains("### P0"));
|
assertFalse(md.contains("### P0"));
|
||||||
assertFalse(md.contains("位置"));
|
assertFalse(md.contains("位置"));
|
||||||
assertFalse(md.contains("模式"));
|
}
|
||||||
assertFalse(md.contains("汇总"));
|
|
||||||
|
@Test
|
||||||
|
void removedFieldsHighlightedOnlyInOldSkeleton() {
|
||||||
|
SchemaChange removed = new SchemaChange(ChangeType.FIELD_REMOVED);
|
||||||
|
removed.setFieldPath("lastError");
|
||||||
|
removed.setMessage("删除字段 lastError");
|
||||||
|
|
||||||
|
String oldJson = "{\"taskId\":\"\",\"lastError\":\"\"}";
|
||||||
|
String newJson = "{\"taskId\":\"\"}";
|
||||||
|
Set<String> oldPaths = SkeletonAnnotator.pathsForOldSkeleton(Collections.singletonList(removed));
|
||||||
|
Set<String> newPaths = SkeletonAnnotator.pathsForNewSkeleton(Collections.singletonList(removed));
|
||||||
|
|
||||||
|
assertEquals(Collections.singleton("lastError"), oldPaths);
|
||||||
|
assertTrue(newPaths.isEmpty());
|
||||||
|
|
||||||
|
String oldMd = SkeletonAnnotator.annotateAsCodeBlock(oldJson, oldPaths);
|
||||||
|
String newMd = SkeletonAnnotator.annotateAsCodeBlock(newJson, newPaths);
|
||||||
|
assertTrue(oldMd.contains("<font color=\"warning\">`\"lastError\":\"\"`</font>"));
|
||||||
|
assertFalse(newMd.contains("lastError"));
|
||||||
|
assertTrue(newMd.startsWith("`"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -60,6 +101,7 @@ class ReportBuilderTest {
|
|||||||
detail.setSeverity(Severity.P0);
|
detail.setSeverity(Severity.P0);
|
||||||
detail.setKeyPattern("k1");
|
detail.setKeyPattern("k1");
|
||||||
detail.setWriteLocation("Foo#bar:1");
|
detail.setWriteLocation("Foo#bar:1");
|
||||||
|
detail.setFieldPath("x");
|
||||||
detail.setMessage("删除字段 x");
|
detail.setMessage("删除字段 x");
|
||||||
report.getChanges().add(detail);
|
report.getChanges().add(detail);
|
||||||
|
|
||||||
@@ -67,6 +109,7 @@ class ReportBuilderTest {
|
|||||||
key.setKeyPattern("k1");
|
key.setKeyPattern("k1");
|
||||||
key.setOldSkeletonJson("{\"x\":\"\"}");
|
key.setOldSkeletonJson("{\"x\":\"\"}");
|
||||||
key.setNewSkeletonJson("{}");
|
key.setNewSkeletonJson("{}");
|
||||||
|
key.getFieldDetails().add(detail);
|
||||||
report.getKeyChanges().add(key);
|
report.getKeyChanges().add(key);
|
||||||
|
|
||||||
String console = new ReportBuilder("[缓存结构变更]").toConsole(report);
|
String console = new ReportBuilder("[缓存结构变更]").toConsole(report);
|
||||||
@@ -75,8 +118,16 @@ class ReportBuilderTest {
|
|||||||
assertTrue(console.contains("**删除字段**: x"));
|
assertTrue(console.contains("**删除字段**: x"));
|
||||||
assertTrue(console.contains("======== 企微 Markdown ========"));
|
assertTrue(console.contains("======== 企微 Markdown ========"));
|
||||||
assertTrue(console.contains("value 值从"));
|
assertTrue(console.contains("value 值从"));
|
||||||
assertTrue(console.contains("- **Key --> k1\u200b**"));
|
assertTrue(console.contains("<font color=\"warning\">`\"x\":\"\"`</font>"));
|
||||||
assertTrue(console.contains("<font color=\"info\">"));
|
}
|
||||||
assertTrue(console.contains("<font color=\"warning\">"));
|
|
||||||
|
@Test
|
||||||
|
void pathAwareNestedHighlight() {
|
||||||
|
String json = "{\"vo\":{\"dbName\":\"\",\"id\":\"\"},\"expiresAtMs\":0}";
|
||||||
|
Set<String> paths = new LinkedHashSet<>();
|
||||||
|
paths.add("vo.dbName");
|
||||||
|
String out = SkeletonAnnotator.annotate(json, paths);
|
||||||
|
assertTrue(out.contains("<font color=\"warning\">\"dbName\":\"\"</font>"));
|
||||||
|
assertFalse(out.contains("<font color=\"warning\">\"id\":\"\"</font>"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user