feat: 1、企微通知中隐藏,CI中打印字段明细,另外将整体输出到企微中的md,在CI控制台中打印出来
All checks were successful
Redis序列化结构检查 / redis-schema-check (push) Has been skipped
All checks were successful
Redis序列化结构检查 / redis-schema-check (push) Has been skipped
2、整段截断,具体改动的字段不可截断
3、不再区分P0\P1\P2
This commit is contained in:
@@ -10,7 +10,9 @@ import com.codechecker.redis.diff.Severity;
|
|||||||
import com.codechecker.redis.git.GitDiffScanner;
|
import com.codechecker.redis.git.GitDiffScanner;
|
||||||
import com.codechecker.redis.git.GitException;
|
import com.codechecker.redis.git.GitException;
|
||||||
import com.codechecker.redis.report.CheckReport;
|
import com.codechecker.redis.report.CheckReport;
|
||||||
|
import com.codechecker.redis.report.KeyStructureChange;
|
||||||
import com.codechecker.redis.schema.JavaSchemaExtractor;
|
import com.codechecker.redis.schema.JavaSchemaExtractor;
|
||||||
|
import com.codechecker.redis.schema.SkeletonJsonRenderer;
|
||||||
import com.codechecker.redis.schema.SourceIndex;
|
import com.codechecker.redis.schema.SourceIndex;
|
||||||
import com.codechecker.redis.schema.TypeSchema;
|
import com.codechecker.redis.schema.TypeSchema;
|
||||||
import com.github.javaparser.StaticJavaParser;
|
import com.github.javaparser.StaticJavaParser;
|
||||||
@@ -19,6 +21,7 @@ import com.github.javaparser.ast.body.TypeDeclaration;
|
|||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
@@ -92,8 +95,10 @@ public class SchemaCheckAnalyzer {
|
|||||||
JavaSchemaExtractor extractorNew = new JavaSchemaExtractor(newIndex, config.getDetection().getMaxFieldDepth());
|
JavaSchemaExtractor extractorNew = new JavaSchemaExtractor(newIndex, config.getDetection().getMaxFieldDepth());
|
||||||
JavaSchemaExtractor extractorOld = new JavaSchemaExtractor(oldIndex, config.getDetection().getMaxFieldDepth());
|
JavaSchemaExtractor extractorOld = new JavaSchemaExtractor(oldIndex, config.getDetection().getMaxFieldDepth());
|
||||||
SchemaDiffer differ = new SchemaDiffer();
|
SchemaDiffer differ = new SchemaDiffer();
|
||||||
|
SkeletonJsonRenderer skeletonRenderer = new SkeletonJsonRenderer();
|
||||||
|
|
||||||
List<SchemaChange> allChanges = new ArrayList<>();
|
List<SchemaChange> allChanges = new ArrayList<>();
|
||||||
|
Map<String, KeyStructureChange> keyChanges = new LinkedHashMap<>();
|
||||||
|
|
||||||
for (String path : candidates) {
|
for (String path : candidates) {
|
||||||
String newContent = newContents.get(path);
|
String newContent = newContents.get(path);
|
||||||
@@ -123,16 +128,31 @@ public class SchemaCheckAnalyzer {
|
|||||||
TypeSchema oldSchema = extractorOld.extract(ow.getResolvedValueType(), ow.isRootArray());
|
TypeSchema oldSchema = extractorOld.extract(ow.getResolvedValueType(), ow.isRootArray());
|
||||||
TypeSchema newSchema = extractorNew.extract(nw.getResolvedValueType(), nw.isRootArray());
|
TypeSchema newSchema = extractorNew.extract(nw.getResolvedValueType(), nw.isRootArray());
|
||||||
List<SchemaChange> changes = differ.diff(oldSchema, newSchema);
|
List<SchemaChange> changes = differ.diff(oldSchema, newSchema);
|
||||||
|
if (changes.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
double confidence = min(nw.getConfidence(), ow.getConfidence(),
|
double confidence = min(nw.getConfidence(), ow.getConfidence(),
|
||||||
oldSchema.getConfidence(), newSchema.getConfidence());
|
oldSchema.getConfidence(), newSchema.getConfidence());
|
||||||
enrich(changes, nw, confidence);
|
enrich(changes, nw, confidence);
|
||||||
allChanges.addAll(changes);
|
allChanges.addAll(changes);
|
||||||
|
mergeKeyChange(keyChanges, nw.getResolvedKeyPattern(), changes,
|
||||||
|
skeletonRenderer.render(oldSchema, protectedPaths(changes),
|
||||||
|
SkeletonJsonRenderer.DEFAULT_MAX_LEN),
|
||||||
|
skeletonRenderer.render(newSchema, protectedPaths(changes),
|
||||||
|
SkeletonJsonRenderer.DEFAULT_MAX_LEN));
|
||||||
} else if (fileChanged) {
|
} else if (fileChanged) {
|
||||||
SchemaChange c = new SchemaChange(ChangeType.WRITE_POINT_ADDED);
|
SchemaChange c = new SchemaChange(ChangeType.WRITE_POINT_ADDED);
|
||||||
c.setKeyPattern(nw.getResolvedKeyPattern());
|
c.setKeyPattern(nw.getResolvedKeyPattern());
|
||||||
c.setWriteLocation(nw.location());
|
c.setWriteLocation(nw.location());
|
||||||
c.setMessage("新增 Redis 写入点,value 类型: " + shortType(nw.getResolvedValueType()));
|
c.setMessage("新增 Redis 写入点,value 类型: " + shortType(nw.getResolvedValueType()));
|
||||||
allChanges.add(c);
|
allChanges.add(c);
|
||||||
|
TypeSchema newSchema = extractorNew.extract(nw.getResolvedValueType(), nw.isRootArray());
|
||||||
|
mergeKeyChange(keyChanges, nw.getResolvedKeyPattern(),
|
||||||
|
Collections.singletonList(c),
|
||||||
|
"",
|
||||||
|
skeletonRenderer.render(newSchema, protectedPaths(
|
||||||
|
Collections.singletonList(c)),
|
||||||
|
SkeletonJsonRenderer.DEFAULT_MAX_LEN));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,13 +165,106 @@ public class SchemaCheckAnalyzer {
|
|||||||
c.setWriteLocation(ow.location());
|
c.setWriteLocation(ow.location());
|
||||||
c.setMessage("删除 Redis 写入点,原 value 类型: " + shortType(ow.getResolvedValueType()));
|
c.setMessage("删除 Redis 写入点,原 value 类型: " + shortType(ow.getResolvedValueType()));
|
||||||
allChanges.add(c);
|
allChanges.add(c);
|
||||||
|
TypeSchema oldSchema = extractorOld.extract(ow.getResolvedValueType(), ow.isRootArray());
|
||||||
|
mergeKeyChange(keyChanges, ow.getResolvedKeyPattern(),
|
||||||
|
Collections.singletonList(c),
|
||||||
|
skeletonRenderer.render(oldSchema, protectedPaths(
|
||||||
|
Collections.singletonList(c)),
|
||||||
|
SkeletonJsonRenderer.DEFAULT_MAX_LEN),
|
||||||
|
"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<SchemaChange> finalChanges = postProcess(allChanges);
|
List<SchemaChange> finalChanges = postProcess(allChanges);
|
||||||
return buildReport(oldSha, newSha, finalChanges);
|
// 按最终字段明细重新对齐 key 摘要(过滤被 suppress/dedup 掉的)
|
||||||
|
Map<String, KeyStructureChange> finalKeys = filterKeyChanges(keyChanges, finalChanges);
|
||||||
|
return buildReport(oldSha, newSha, finalChanges, finalKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mergeKeyChange(Map<String, KeyStructureChange> keyChanges, String keyPattern,
|
||||||
|
List<SchemaChange> changes, String oldSkeleton, String newSkeleton) {
|
||||||
|
String key = keyPattern == null ? "<unknown>" : keyPattern;
|
||||||
|
KeyStructureChange kc = keyChanges.computeIfAbsent(key, k -> {
|
||||||
|
KeyStructureChange n = new KeyStructureChange();
|
||||||
|
n.setKeyPattern(k);
|
||||||
|
return n;
|
||||||
|
});
|
||||||
|
kc.getFieldDetails().addAll(changes);
|
||||||
|
for (SchemaChange c : changes) {
|
||||||
|
kc.raiseSeverity(c.getSeverity());
|
||||||
|
}
|
||||||
|
if (oldSkeleton != null && !oldSkeleton.isEmpty()) {
|
||||||
|
kc.setOldSkeletonJson(oldSkeleton);
|
||||||
|
}
|
||||||
|
if (newSkeleton != null && !newSkeleton.isEmpty()) {
|
||||||
|
kc.setNewSkeletonJson(newSkeleton);
|
||||||
|
}
|
||||||
|
if (kc.getOldSkeletonJson() == null) {
|
||||||
|
kc.setOldSkeletonJson(oldSkeleton == null ? "" : oldSkeleton);
|
||||||
|
}
|
||||||
|
if (kc.getNewSkeletonJson() == null) {
|
||||||
|
kc.setNewSkeletonJson(newSkeleton == null ? "" : newSkeleton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> protectedPaths(List<SchemaChange> changes) {
|
||||||
|
Set<String> paths = new LinkedHashSet<>();
|
||||||
|
for (SchemaChange c : changes) {
|
||||||
|
if (c.getFieldPath() != null && !c.getFieldPath().isEmpty()) {
|
||||||
|
paths.add(c.getFieldPath());
|
||||||
|
}
|
||||||
|
// 路径迁移时 oldValue 为旧路径
|
||||||
|
if (c.getChangeType() == ChangeType.FIELD_PATH_MOVED
|
||||||
|
&& c.getOldValue() != null && !c.getOldValue().isEmpty()) {
|
||||||
|
paths.add(c.getOldValue());
|
||||||
|
}
|
||||||
|
if (c.getChangeType() == ChangeType.WRAPPER_ADDED
|
||||||
|
&& c.getFieldPath() != null) {
|
||||||
|
paths.add(c.getFieldPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, KeyStructureChange> filterKeyChanges(
|
||||||
|
Map<String, KeyStructureChange> keyChanges, List<SchemaChange> finalChanges) {
|
||||||
|
Set<String> liveKeys = new HashSet<>();
|
||||||
|
for (SchemaChange c : finalChanges) {
|
||||||
|
liveKeys.add(c.getKeyPattern() == null ? "<unknown>" : c.getKeyPattern());
|
||||||
|
}
|
||||||
|
Map<String, KeyStructureChange> result = new LinkedHashMap<>();
|
||||||
|
for (Map.Entry<String, KeyStructureChange> e : keyChanges.entrySet()) {
|
||||||
|
if (liveKeys.contains(e.getKey())) {
|
||||||
|
KeyStructureChange kc = e.getValue();
|
||||||
|
// 仅保留最终明细中仍存在的字段变更
|
||||||
|
List<SchemaChange> retained = new ArrayList<>();
|
||||||
|
Set<String> finalDedup = new HashSet<>();
|
||||||
|
for (SchemaChange c : finalChanges) {
|
||||||
|
String k = c.getKeyPattern() == null ? "<unknown>" : c.getKeyPattern();
|
||||||
|
if (!k.equals(e.getKey())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String dedupKey = c.getChangeType() + "|" + c.getKeyPattern() + "|"
|
||||||
|
+ c.getWriteLocation() + "|" + c.getFieldPath();
|
||||||
|
if (finalDedup.add(dedupKey)) {
|
||||||
|
retained.add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kc.getFieldDetails().clear();
|
||||||
|
kc.getFieldDetails().addAll(retained);
|
||||||
|
Severity max = Severity.P2;
|
||||||
|
for (SchemaChange c : retained) {
|
||||||
|
if (c.getSeverity() != null && c.getSeverity().ordinal() < max.ordinal()) {
|
||||||
|
max = c.getSeverity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kc.setSeverity(max);
|
||||||
|
result.put(e.getKey(), kc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void enrich(List<SchemaChange> changes, WritePoint wp, double confidence) {
|
private void enrich(List<SchemaChange> changes, WritePoint wp, double confidence) {
|
||||||
@@ -208,12 +321,14 @@ public class SchemaCheckAnalyzer {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private CheckReport buildReport(String oldSha, String newSha, List<SchemaChange> changes) {
|
private CheckReport buildReport(String oldSha, String newSha, List<SchemaChange> changes,
|
||||||
|
Map<String, KeyStructureChange> keyChanges) {
|
||||||
CheckReport report = new CheckReport();
|
CheckReport report = new CheckReport();
|
||||||
report.setOldSha(oldSha);
|
report.setOldSha(oldSha);
|
||||||
report.setNewSha(newSha);
|
report.setNewSha(newSha);
|
||||||
report.setMode(config.getMode());
|
report.setMode(config.getMode());
|
||||||
report.getChanges().addAll(changes);
|
report.getChanges().addAll(changes);
|
||||||
|
report.getKeyChanges().addAll(keyChanges.values());
|
||||||
|
|
||||||
boolean blocked = false;
|
boolean blocked = false;
|
||||||
if (config.isBlockMode()) {
|
if (config.isBlockMode()) {
|
||||||
|
|||||||
@@ -70,13 +70,15 @@ public class RedisSchemaCheckerMain implements Callable<Integer> {
|
|||||||
report.setRepository(repository != null ? repository : root.getFileName().toString());
|
report.setRepository(repository != null ? repository : root.getFileName().toString());
|
||||||
|
|
||||||
ReportBuilder builder = new ReportBuilder(config.getNotify().getTitlePrefix());
|
ReportBuilder builder = new ReportBuilder(config.getNotify().getTitlePrefix());
|
||||||
|
// CI:字段明细 + 完整企微 Markdown
|
||||||
System.out.println(builder.toConsole(report));
|
System.out.println(builder.toConsole(report));
|
||||||
|
|
||||||
boolean shouldNotify = config.getNotify().isEnabled()
|
boolean shouldNotify = config.getNotify().isEnabled()
|
||||||
&& (report.hasChanges() || config.getNotify().isNotifyOnClean());
|
&& (report.hasChanges() || config.getNotify().isNotifyOnClean());
|
||||||
if (shouldNotify && !dryRun) {
|
if (shouldNotify && !dryRun) {
|
||||||
String webhook = config.getNotify().getWebhookUrl();
|
String webhook = config.getNotify().getWebhookUrl();
|
||||||
boolean ok = new WeComNotifier().sendMarkdown(webhook, builder.toMarkdown(report));
|
String markdown = builder.toMarkdown(report);
|
||||||
|
boolean ok = new WeComNotifier().sendMarkdown(webhook, markdown);
|
||||||
System.out.println("[redis-schema-checker] 企微通知发送: " + (ok ? "成功" : "失败/跳过"));
|
System.out.println("[redis-schema-checker] 企微通知发送: " + (ok ? "成功" : "失败/跳过"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,12 @@ public class CheckReport {
|
|||||||
private String mode;
|
private String mode;
|
||||||
|
|
||||||
private final List<SchemaChange> changes = new ArrayList<>();
|
private final List<SchemaChange> changes = new ArrayList<>();
|
||||||
|
private final List<KeyStructureChange> keyChanges = new ArrayList<>();
|
||||||
private boolean blocked;
|
private boolean blocked;
|
||||||
private int exitCode;
|
private int exitCode;
|
||||||
|
|
||||||
public boolean hasChanges() {
|
public boolean hasChanges() {
|
||||||
return !changes.isEmpty();
|
return !changes.isEmpty() || !keyChanges.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public long count(Severity severity) {
|
public long count(Severity severity) {
|
||||||
@@ -91,6 +92,10 @@ public class CheckReport {
|
|||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<KeyStructureChange> getKeyChanges() {
|
||||||
|
return keyChanges;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isBlocked() {
|
public boolean isBlocked() {
|
||||||
return blocked;
|
return blocked;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package com.codechecker.redis.report;
|
||||||
|
|
||||||
|
import com.codechecker.redis.diff.SchemaChange;
|
||||||
|
import com.codechecker.redis.diff.Severity;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 Redis key 聚合后的结构变更摘要(用于企微骨架展示)。
|
||||||
|
*/
|
||||||
|
public class KeyStructureChange {
|
||||||
|
|
||||||
|
private String keyPattern;
|
||||||
|
private String oldSkeletonJson;
|
||||||
|
private String newSkeletonJson;
|
||||||
|
private Severity severity = Severity.P2;
|
||||||
|
private final List<SchemaChange> fieldDetails = new ArrayList<>();
|
||||||
|
|
||||||
|
public String getKeyPattern() {
|
||||||
|
return keyPattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKeyPattern(String keyPattern) {
|
||||||
|
this.keyPattern = keyPattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOldSkeletonJson() {
|
||||||
|
return oldSkeletonJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOldSkeletonJson(String oldSkeletonJson) {
|
||||||
|
this.oldSkeletonJson = oldSkeletonJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNewSkeletonJson() {
|
||||||
|
return newSkeletonJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNewSkeletonJson(String newSkeletonJson) {
|
||||||
|
this.newSkeletonJson = newSkeletonJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Severity getSeverity() {
|
||||||
|
return severity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSeverity(Severity severity) {
|
||||||
|
this.severity = severity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SchemaChange> getFieldDetails() {
|
||||||
|
return fieldDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void raiseSeverity(Severity candidate) {
|
||||||
|
if (candidate == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (severity == null || candidate.ordinal() < severity.ordinal()) {
|
||||||
|
// P0=0, P1=1, P2=2 → ordinal 更小更严重
|
||||||
|
severity = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,10 @@ import java.util.Map;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 将 {@link CheckReport} 渲染为企微 Markdown / 控制台文本。
|
* 将 {@link CheckReport} 渲染为企微 Markdown / 控制台文本。
|
||||||
|
* <ul>
|
||||||
|
* <li>企微:按 key 展示序列化骨架变更,不含位置/字段明细,不分 P0/P1/P2</li>
|
||||||
|
* <li>CI:先打字段明细,再完整输出企微 Markdown</li>
|
||||||
|
* </ul>
|
||||||
*/
|
*/
|
||||||
public class ReportBuilder {
|
public class ReportBuilder {
|
||||||
|
|
||||||
@@ -29,16 +33,60 @@ public class ReportBuilder {
|
|||||||
sb.append("> **提交人**: ").append(nvl(report.getModifier())).append('\n');
|
sb.append("> **提交人**: ").append(nvl(report.getModifier())).append('\n');
|
||||||
sb.append("> **时间**: ").append(nvl(report.getModifyTime())).append("\n\n");
|
sb.append("> **时间**: ").append(nvl(report.getModifyTime())).append("\n\n");
|
||||||
|
|
||||||
|
List<KeyStructureChange> keys = report.getKeyChanges();
|
||||||
|
if (keys == null || keys.isEmpty()) {
|
||||||
|
// 兼容仅有字段明细时退化展示
|
||||||
|
if (report.getChanges().isEmpty()) {
|
||||||
|
sb.append("未检测到 Redis 序列化结构变更。\n");
|
||||||
|
} else {
|
||||||
|
for (SchemaChange c : report.getChanges()) {
|
||||||
|
sb.append("- `").append(nvl(c.getKeyPattern())).append("`\n");
|
||||||
|
if (c.getMessage() != null) {
|
||||||
|
sb.append(" ").append(c.getMessage()).append('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (KeyStructureChange kc : keys) {
|
||||||
|
sb.append("- `").append(nvl(kc.getKeyPattern())).append("`\n");
|
||||||
|
String oldJson = nvl(kc.getOldSkeletonJson());
|
||||||
|
String newJson = nvl(kc.getNewSkeletonJson());
|
||||||
|
if (oldJson.isEmpty() && !newJson.isEmpty()) {
|
||||||
|
sb.append(" value 新增为 “").append(newJson).append("”\n\n");
|
||||||
|
} else if (!oldJson.isEmpty() && newJson.isEmpty()) {
|
||||||
|
sb.append(" value 原结构 “").append(oldJson).append("” 已删除写入\n\n");
|
||||||
|
} else {
|
||||||
|
sb.append(" value 值从 “").append(oldJson).append("”\n");
|
||||||
|
sb.append(" 变更为 “").append(newJson).append("”\n\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CI 控制台:字段明细 + 完整企微 Markdown。
|
||||||
|
*/
|
||||||
|
public String toConsole(CheckReport report) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (!report.hasChanges()) {
|
||||||
|
sb.append("未检测到 Redis 序列化结构变更。\n");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append("======== 字段明细 ========\n");
|
||||||
Map<Severity, List<SchemaChange>> grouped = new EnumMap<>(Severity.class);
|
Map<Severity, List<SchemaChange>> grouped = new EnumMap<>(Severity.class);
|
||||||
for (SchemaChange c : report.getChanges()) {
|
for (SchemaChange c : report.getChanges()) {
|
||||||
grouped.computeIfAbsent(c.getSeverity(), k -> new ArrayList<>()).add(c);
|
grouped.computeIfAbsent(c.getSeverity(), k -> new ArrayList<>()).add(c);
|
||||||
}
|
}
|
||||||
|
boolean anyDetail = false;
|
||||||
for (Severity severity : Severity.values()) {
|
for (Severity severity : Severity.values()) {
|
||||||
List<SchemaChange> list = grouped.get(severity);
|
List<SchemaChange> list = grouped.get(severity);
|
||||||
if (list == null || list.isEmpty()) {
|
if (list == null || list.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
anyDetail = true;
|
||||||
sb.append("### ").append(severity).append('\n');
|
sb.append("### ").append(severity).append('\n');
|
||||||
for (SchemaChange c : list) {
|
for (SchemaChange c : list) {
|
||||||
sb.append("- **").append(c.getChangeType().getLabel()).append("**");
|
sb.append("- **").append(c.getChangeType().getLabel()).append("**");
|
||||||
@@ -55,12 +103,15 @@ public class ReportBuilder {
|
|||||||
}
|
}
|
||||||
sb.append('\n');
|
sb.append('\n');
|
||||||
}
|
}
|
||||||
|
if (!anyDetail) {
|
||||||
|
sb.append("(无字段级明细)\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append("======== 企微 Markdown ========\n");
|
||||||
|
sb.append(toMarkdown(report));
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 将详情文案中的 key 加粗,如「删除字段 lastError」→「**删除字段**: lastError」。
|
|
||||||
*/
|
|
||||||
private String formatDetailMessage(String message) {
|
private String formatDetailMessage(String message) {
|
||||||
String trimmed = message.trim();
|
String trimmed = message.trim();
|
||||||
int colonCn = trimmed.indexOf(':');
|
int colonCn = trimmed.indexOf(':');
|
||||||
@@ -92,13 +143,6 @@ public class ReportBuilder {
|
|||||||
return trimmed;
|
return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String toConsole(CheckReport report) {
|
|
||||||
if (!report.hasChanges()) {
|
|
||||||
return "未检测到 Redis 序列化结构变更。";
|
|
||||||
}
|
|
||||||
return toMarkdown(report);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String shortSha(String sha) {
|
private String shortSha(String sha) {
|
||||||
if (sha == null) {
|
if (sha == null) {
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
@@ -0,0 +1,375 @@
|
|||||||
|
package com.codechecker.redis.schema;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将扁平 {@link TypeSchema} 还原为带占位符的序列化骨架 JSON。
|
||||||
|
* 超长时整段压缩/截断,但受保护(改动)字段路径对应片段不被截断。
|
||||||
|
*/
|
||||||
|
public class SkeletonJsonRenderer {
|
||||||
|
|
||||||
|
/** 单侧骨架默认最大长度(企微 markdown 总长约 4096)。 */
|
||||||
|
public static final int DEFAULT_MAX_LEN = 1500;
|
||||||
|
|
||||||
|
public String render(TypeSchema schema) {
|
||||||
|
return render(schema, null, Integer.MAX_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param protectedPaths 改动相关字段路径(如 vo.dbName、expiresAtMs),压缩时优先保留
|
||||||
|
* @param maxLen 输出最大字符数
|
||||||
|
*/
|
||||||
|
public String render(TypeSchema schema, Set<String> protectedPaths, int maxLen) {
|
||||||
|
if (schema == null || schema.isEmpty()) {
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
|
Set<String> protectedSet = protectedPaths == null
|
||||||
|
? new LinkedHashSet<>() : new LinkedHashSet<>(protectedPaths);
|
||||||
|
Node root = buildTree(schema);
|
||||||
|
String full = write(root, "", protectedSet, false);
|
||||||
|
if (full.length() <= maxLen) {
|
||||||
|
return full;
|
||||||
|
}
|
||||||
|
String compact = write(root, "", protectedSet, true);
|
||||||
|
if (compact.length() <= maxLen) {
|
||||||
|
return compact;
|
||||||
|
}
|
||||||
|
return truncatePreserve(compact, protectedSet, maxLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node buildTree(TypeSchema schema) {
|
||||||
|
Node root = new Node(JsonType.OBJECT);
|
||||||
|
for (FieldSchema field : schema.getFields().values()) {
|
||||||
|
putPath(root, field.getPath(), field.getJsonType());
|
||||||
|
}
|
||||||
|
// 根数组:字段以 [] / [].xxx 记录
|
||||||
|
if (root.children.size() == 1 && root.children.containsKey("[]")) {
|
||||||
|
Node arr = new Node(JsonType.ARRAY);
|
||||||
|
arr.children.put("[]", root.children.get("[]"));
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void putPath(Node root, String path, JsonType type) {
|
||||||
|
List<Seg> segs = parsePath(path);
|
||||||
|
if (segs.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Node cur = root;
|
||||||
|
for (int i = 0; i < segs.size(); i++) {
|
||||||
|
Seg seg = segs.get(i);
|
||||||
|
boolean last = i == segs.size() - 1;
|
||||||
|
if (seg.array) {
|
||||||
|
Node arr = cur.children.computeIfAbsent(seg.name, k -> new Node(JsonType.ARRAY));
|
||||||
|
arr.type = JsonType.ARRAY;
|
||||||
|
Node elem = arr.children.computeIfAbsent("[]", k -> new Node(JsonType.OBJECT));
|
||||||
|
if (last) {
|
||||||
|
if (type == JsonType.OBJECT || type == JsonType.ARRAY || type == JsonType.MAP) {
|
||||||
|
elem.type = type;
|
||||||
|
} else {
|
||||||
|
elem.type = type;
|
||||||
|
elem.leaf = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cur = elem;
|
||||||
|
} else {
|
||||||
|
Node child = cur.children.computeIfAbsent(seg.name,
|
||||||
|
k -> new Node(last ? type : JsonType.OBJECT));
|
||||||
|
if (last) {
|
||||||
|
child.type = type;
|
||||||
|
child.leaf = type != JsonType.OBJECT && type != JsonType.ARRAY && type != JsonType.MAP;
|
||||||
|
} else if (child.type != JsonType.ARRAY) {
|
||||||
|
child.type = JsonType.OBJECT;
|
||||||
|
}
|
||||||
|
cur = child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Seg> parsePath(String path) {
|
||||||
|
List<Seg> segs = new ArrayList<>();
|
||||||
|
if (path == null || path.isEmpty()) {
|
||||||
|
return segs;
|
||||||
|
}
|
||||||
|
for (String raw : path.split("\\.")) {
|
||||||
|
if (raw.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ("[]".equals(raw)) {
|
||||||
|
segs.add(new Seg("[]", false));
|
||||||
|
} else if (raw.endsWith("[]")) {
|
||||||
|
segs.add(new Seg(raw.substring(0, raw.length() - 2), true));
|
||||||
|
} else {
|
||||||
|
segs.add(new Seg(raw, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return segs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String write(Node node, String pathPrefix, Set<String> protectedPaths, boolean compact) {
|
||||||
|
if (node == null) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
if (node.type == JsonType.ARRAY) {
|
||||||
|
Node elem = node.children.get("[]");
|
||||||
|
if (elem == null) {
|
||||||
|
return "[]";
|
||||||
|
}
|
||||||
|
String elemPath = pathPrefix.isEmpty() ? "[]" : pathPrefix + "[]";
|
||||||
|
if (compact && !isProtectedUnder(pathPrefix, protectedPaths)
|
||||||
|
&& !isProtectedUnder(elemPath, protectedPaths)) {
|
||||||
|
return "[...]";
|
||||||
|
}
|
||||||
|
return "[" + write(elem, elemPath, protectedPaths, compact) + "]";
|
||||||
|
}
|
||||||
|
if (node.leaf || isScalar(node.type)) {
|
||||||
|
return placeholder(node.type);
|
||||||
|
}
|
||||||
|
if (node.type == JsonType.MAP) {
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append('{');
|
||||||
|
boolean first = true;
|
||||||
|
for (Map.Entry<String, Node> e : node.children.entrySet()) {
|
||||||
|
String name = e.getKey();
|
||||||
|
if (name == null || name.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Node child = e.getValue();
|
||||||
|
String childPath = pathPrefix.isEmpty() ? name : pathPrefix + "." + name;
|
||||||
|
// 根数组占位名 [] 不作为 JSON key 输出(由上层 ARRAY 处理)
|
||||||
|
if ("[]".equals(name) && pathPrefix.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!first) {
|
||||||
|
sb.append(',');
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
sb.append('"').append(escape(name)).append("\":");
|
||||||
|
|
||||||
|
if (compact && !isProtectedUnder(childPath, protectedPaths) && child.type != JsonType.ARRAY) {
|
||||||
|
sb.append(collapsedValue(child));
|
||||||
|
} else if (child.type == JsonType.ARRAY) {
|
||||||
|
sb.append(write(child, childPath, protectedPaths, compact));
|
||||||
|
} else if (child.leaf || isScalar(child.type)) {
|
||||||
|
sb.append(placeholder(child.type));
|
||||||
|
} else {
|
||||||
|
sb.append(write(child, childPath, protectedPaths, compact));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append('}');
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String collapsedValue(Node child) {
|
||||||
|
if (child.type == JsonType.ARRAY) {
|
||||||
|
return "[...]";
|
||||||
|
}
|
||||||
|
if (child.type == JsonType.OBJECT || child.type == JsonType.MAP) {
|
||||||
|
return "\"...\"";
|
||||||
|
}
|
||||||
|
return placeholder(child.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isProtectedUnder(String pathPrefix, Set<String> protectedPaths) {
|
||||||
|
if (protectedPaths == null || protectedPaths.isEmpty() || pathPrefix == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (String p : protectedPaths) {
|
||||||
|
if (p == null || p.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (p.equals(pathPrefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (pathPrefix.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (p.startsWith(pathPrefix + ".") || p.startsWith(pathPrefix + "[")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String truncatePreserve(String json, Set<String> protectedPaths, int maxLen) {
|
||||||
|
List<String> fragments = new ArrayList<>();
|
||||||
|
for (String path : protectedPaths) {
|
||||||
|
String key = lastSegment(path);
|
||||||
|
if (key.isEmpty() || "[]".equals(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String needle = "\"" + key + "\"";
|
||||||
|
int idx = json.indexOf(needle);
|
||||||
|
if (idx < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int end = findValueEnd(json, idx + needle.length());
|
||||||
|
String frag = json.substring(idx, Math.min(json.length(), end));
|
||||||
|
if (!fragments.contains(frag)) {
|
||||||
|
fragments.add(frag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder kept = new StringBuilder();
|
||||||
|
for (String f : fragments) {
|
||||||
|
if (kept.length() > 0 && kept.length() + f.length() + 1 > maxLen) {
|
||||||
|
// 不截断改动字段:装不下则整段保留已收集的改动片段
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (kept.length() > 0) {
|
||||||
|
kept.append(',');
|
||||||
|
}
|
||||||
|
kept.append(f);
|
||||||
|
// 单条改动字段允许超过 maxLen(不可截断)
|
||||||
|
if (fragments.size() == 1 && kept.length() > maxLen) {
|
||||||
|
return kept.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String focus = kept.toString();
|
||||||
|
if (focus.length() >= maxLen) {
|
||||||
|
return focus;
|
||||||
|
}
|
||||||
|
int markerLen = 20;
|
||||||
|
int budget = maxLen - focus.length() - (focus.isEmpty() ? 0 : markerLen);
|
||||||
|
if (budget < 8) {
|
||||||
|
return focus.isEmpty()
|
||||||
|
? json.substring(0, Math.min(maxLen, json.length()))
|
||||||
|
: "{...(truncated)," + focus + "}";
|
||||||
|
}
|
||||||
|
String head = json.substring(0, Math.min(budget, json.length()));
|
||||||
|
if (focus.isEmpty()) {
|
||||||
|
return head + (json.length() > head.length() ? "..." : "");
|
||||||
|
}
|
||||||
|
return head + "...[改动字段]..." + focus;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int findValueEnd(String json, int afterKey) {
|
||||||
|
int i = afterKey;
|
||||||
|
while (i < json.length() && (json.charAt(i) == ':' || Character.isWhitespace(json.charAt(i)))) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
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 depth = 0;
|
||||||
|
for (; i < json.length(); i++) {
|
||||||
|
char ch = json.charAt(i);
|
||||||
|
if (ch == '{' || ch == '[') {
|
||||||
|
depth++;
|
||||||
|
} else if (ch == '}' || ch == ']') {
|
||||||
|
depth--;
|
||||||
|
if (depth == 0) {
|
||||||
|
return i + 1;
|
||||||
|
}
|
||||||
|
} else if (ch == '"') {
|
||||||
|
i++;
|
||||||
|
while (i < json.length()) {
|
||||||
|
char x = json.charAt(i++);
|
||||||
|
if (x == '\\' && i < json.length()) {
|
||||||
|
i++;
|
||||||
|
} else if (x == '"') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return json.length();
|
||||||
|
}
|
||||||
|
while (i < json.length() && json.charAt(i) != ',' && json.charAt(i) != '}' && json.charAt(i) != ']') {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String lastSegment(String path) {
|
||||||
|
if (path == null || path.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String p = path;
|
||||||
|
if (p.endsWith("[]")) {
|
||||||
|
p = p.substring(0, p.length() - 2);
|
||||||
|
}
|
||||||
|
int dot = p.lastIndexOf('.');
|
||||||
|
String seg = dot >= 0 ? p.substring(dot + 1) : p;
|
||||||
|
if (seg.endsWith("[]")) {
|
||||||
|
seg = seg.substring(0, seg.length() - 2);
|
||||||
|
}
|
||||||
|
return seg.replace("[]", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isScalar(JsonType type) {
|
||||||
|
return type == JsonType.STRING || type == JsonType.NUMBER
|
||||||
|
|| type == JsonType.BOOLEAN || type == JsonType.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String placeholder(JsonType type) {
|
||||||
|
if (type == null) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case NUMBER:
|
||||||
|
return "0";
|
||||||
|
case BOOLEAN:
|
||||||
|
return "false";
|
||||||
|
case STRING:
|
||||||
|
return "\"\"";
|
||||||
|
case MAP:
|
||||||
|
case OBJECT:
|
||||||
|
return "{}";
|
||||||
|
case ARRAY:
|
||||||
|
return "[]";
|
||||||
|
case UNKNOWN:
|
||||||
|
default:
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String escape(String s) {
|
||||||
|
return s.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Seg {
|
||||||
|
final String name;
|
||||||
|
final boolean array;
|
||||||
|
|
||||||
|
Seg(String name, boolean array) {
|
||||||
|
this.name = name;
|
||||||
|
this.array = array;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Node {
|
||||||
|
JsonType type;
|
||||||
|
boolean leaf;
|
||||||
|
final Map<String, Node> children = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
Node(JsonType type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||||||
class ReportBuilderTest {
|
class ReportBuilderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void markdownTemplateWithoutModeAndSummary() {
|
void wecomMarkdownShowsSkeletonWithoutSeverityAndLocation() {
|
||||||
CheckReport report = new CheckReport();
|
CheckReport report = new CheckReport();
|
||||||
report.setRepository("jnpf-java-cloud");
|
report.setRepository("jnpf-java-cloud");
|
||||||
report.setBranch("code/redis_change_detection_v1.0");
|
report.setBranch("code/redis_change_detection_v1.0");
|
||||||
@@ -19,25 +19,61 @@ class ReportBuilderTest {
|
|||||||
report.setNewSha("67c8a6eb");
|
report.setNewSha("67c8a6eb");
|
||||||
report.setModifier("dongzi");
|
report.setModifier("dongzi");
|
||||||
report.setModifyTime("2026-07-13 16:54:17");
|
report.setModifyTime("2026-07-13 16:54:17");
|
||||||
report.setMode("notify");
|
|
||||||
|
|
||||||
SchemaChange change = new SchemaChange(ChangeType.FIELD_REMOVED);
|
SchemaChange detail = new SchemaChange(ChangeType.WRAPPER_ADDED);
|
||||||
change.setSeverity(Severity.P0);
|
detail.setSeverity(Severity.P0);
|
||||||
change.setKeyPattern("saas:period-config:migration:current");
|
detail.setKeyPattern("tenant:db:content:*");
|
||||||
change.setWriteLocation("SaasPeriodConfigMigrationRedisSupport#putCurrent:41");
|
detail.setWriteLocation("Helper#put:10");
|
||||||
change.setMessage("删除字段 lastError1");
|
detail.setFieldPath("vo");
|
||||||
report.getChanges().add(change);
|
detail.setMessage("新增包装层 vo");
|
||||||
|
report.getChanges().add(detail);
|
||||||
|
|
||||||
|
KeyStructureChange key = new KeyStructureChange();
|
||||||
|
key.setKeyPattern("tenant:db:content:*");
|
||||||
|
key.setOldSkeletonJson("{\"dbName\":\"\",\"linkList\":[{\"id\":\"\"}]}");
|
||||||
|
key.setNewSkeletonJson("{\"vo\":{\"dbName\":\"\",\"linkList\":[{\"id\":\"\"}]},\"expiresAtMs\":0}");
|
||||||
|
key.setSeverity(Severity.P0);
|
||||||
|
report.getKeyChanges().add(key);
|
||||||
|
|
||||||
String md = new ReportBuilder("[Redis数据结构变更]").toMarkdown(report);
|
String md = new ReportBuilder("[Redis数据结构变更]").toMarkdown(report);
|
||||||
|
|
||||||
assertTrue(md.startsWith("## [Redis数据结构变更] jnpf-java-cloud"));
|
assertTrue(md.startsWith("## [Redis数据结构变更] jnpf-java-cloud"));
|
||||||
assertTrue(md.contains("> **分支**: code/redis_change_detection_v1.0"));
|
assertTrue(md.contains("> **分支**: code/redis_change_detection_v1.0"));
|
||||||
assertTrue(md.contains("> **提交**: cedd161c → 67c8a6eb"));
|
|
||||||
assertTrue(md.contains("> **提交人**: dongzi"));
|
|
||||||
assertTrue(md.contains("> **时间**: 2026-07-13 16:54:17"));
|
assertTrue(md.contains("> **时间**: 2026-07-13 16:54:17"));
|
||||||
assertTrue(md.contains(" - **位置**: SaasPeriodConfigMigrationRedisSupport#putCurrent:41"));
|
assertTrue(md.contains("`tenant:db:content:*`"));
|
||||||
assertTrue(md.contains(" - **删除字段**: lastError1"));
|
assertTrue(md.contains("value 值从 “{\"dbName\":\"\""));
|
||||||
|
assertTrue(md.contains("变更为 “{\"vo\":"));
|
||||||
|
assertFalse(md.contains("### P0"));
|
||||||
|
assertFalse(md.contains("位置"));
|
||||||
assertFalse(md.contains("模式"));
|
assertFalse(md.contains("模式"));
|
||||||
assertFalse(md.contains("汇总"));
|
assertFalse(md.contains("汇总"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void consoleContainsFieldDetailsAndWecomMarkdown() {
|
||||||
|
CheckReport report = new CheckReport();
|
||||||
|
report.setRepository("demo");
|
||||||
|
report.setOldSha("aaa");
|
||||||
|
report.setNewSha("bbb");
|
||||||
|
|
||||||
|
SchemaChange detail = new SchemaChange(ChangeType.FIELD_REMOVED);
|
||||||
|
detail.setSeverity(Severity.P0);
|
||||||
|
detail.setKeyPattern("k1");
|
||||||
|
detail.setWriteLocation("Foo#bar:1");
|
||||||
|
detail.setMessage("删除字段 x");
|
||||||
|
report.getChanges().add(detail);
|
||||||
|
|
||||||
|
KeyStructureChange key = new KeyStructureChange();
|
||||||
|
key.setKeyPattern("k1");
|
||||||
|
key.setOldSkeletonJson("{\"x\":\"\"}");
|
||||||
|
key.setNewSkeletonJson("{}");
|
||||||
|
report.getKeyChanges().add(key);
|
||||||
|
|
||||||
|
String console = new ReportBuilder("[Redis数据结构变更]").toConsole(report);
|
||||||
|
assertTrue(console.contains("======== 字段明细 ========"));
|
||||||
|
assertTrue(console.contains("**位置**: Foo#bar:1"));
|
||||||
|
assertTrue(console.contains("**删除字段**: x"));
|
||||||
|
assertTrue(console.contains("======== 企微 Markdown ========"));
|
||||||
|
assertTrue(console.contains("value 值从"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package com.codechecker.redis.schema;
|
||||||
|
|
||||||
|
import com.codechecker.redis.TestSupport;
|
||||||
|
import com.codechecker.redis.detector.RedisWritePointDetector;
|
||||||
|
import com.codechecker.redis.detector.WritePoint;
|
||||||
|
import com.codechecker.redis.diff.SchemaChange;
|
||||||
|
import com.codechecker.redis.diff.SchemaDiffer;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class SkeletonJsonRendererTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rendersTenantVoThenEnvelopeSkeletons() {
|
||||||
|
String tenantVo = TestSupport.fixture("fixtures/tenant/TenantVO.txt");
|
||||||
|
String tenantLink = TestSupport.fixture("fixtures/tenant/TenantLinkModel.txt");
|
||||||
|
String helperOld = TestSupport.fixture("fixtures/tenant/HelperOld.txt");
|
||||||
|
String helperNew = TestSupport.fixture("fixtures/tenant/HelperNew.txt");
|
||||||
|
|
||||||
|
Set<String> patterns = new HashSet<>();
|
||||||
|
patterns.add("W01");
|
||||||
|
patterns.add("W02");
|
||||||
|
patterns.add("W03");
|
||||||
|
|
||||||
|
SourceIndex oldIndex = new SourceIndex();
|
||||||
|
oldIndex.addSource(tenantVo);
|
||||||
|
oldIndex.addSource(tenantLink);
|
||||||
|
oldIndex.addSource(helperOld);
|
||||||
|
|
||||||
|
SourceIndex newIndex = new SourceIndex();
|
||||||
|
newIndex.addSource(tenantVo);
|
||||||
|
newIndex.addSource(tenantLink);
|
||||||
|
newIndex.addSource(helperNew);
|
||||||
|
|
||||||
|
WritePoint oldWp = new RedisWritePointDetector(oldIndex, patterns)
|
||||||
|
.detect("Helper.java", helperOld).get(0);
|
||||||
|
WritePoint newWp = new RedisWritePointDetector(newIndex, patterns)
|
||||||
|
.detect("Helper.java", helperNew).get(0);
|
||||||
|
|
||||||
|
TypeSchema oldSchema = new JavaSchemaExtractor(oldIndex, 8)
|
||||||
|
.extract(oldWp.getResolvedValueType(), oldWp.isRootArray());
|
||||||
|
TypeSchema newSchema = new JavaSchemaExtractor(newIndex, 8)
|
||||||
|
.extract(newWp.getResolvedValueType(), newWp.isRootArray());
|
||||||
|
|
||||||
|
List<SchemaChange> changes = new SchemaDiffer().diff(oldSchema, newSchema);
|
||||||
|
Set<String> protectedPaths = changes.stream()
|
||||||
|
.map(SchemaChange::getFieldPath)
|
||||||
|
.filter(p -> p != null && !p.isEmpty())
|
||||||
|
.collect(Collectors.toCollection(HashSet::new));
|
||||||
|
changes.stream()
|
||||||
|
.filter(c -> c.getOldValue() != null && c.getOldValue().contains("."))
|
||||||
|
.forEach(c -> protectedPaths.add(c.getOldValue()));
|
||||||
|
// also plain old paths without dot
|
||||||
|
changes.forEach(c -> {
|
||||||
|
if (c.getOldValue() != null && !c.getOldValue().isEmpty()
|
||||||
|
&& c.getOldValue().indexOf(' ') < 0) {
|
||||||
|
protectedPaths.add(c.getOldValue());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
SkeletonJsonRenderer renderer = new SkeletonJsonRenderer();
|
||||||
|
String oldJson = renderer.render(oldSchema);
|
||||||
|
String newJson = renderer.render(newSchema);
|
||||||
|
|
||||||
|
assertTrue(oldJson.contains("\"dbName\":\"\""));
|
||||||
|
assertTrue(oldJson.contains("\"linkList\":["));
|
||||||
|
assertTrue(newJson.contains("\"vo\":{"));
|
||||||
|
assertTrue(newJson.contains("\"expiresAtMs\":0"));
|
||||||
|
assertTrue(newJson.contains("\"dbName\":\"\""));
|
||||||
|
|
||||||
|
String truncated = renderer.render(newSchema, protectedPaths, 80);
|
||||||
|
assertTrue(truncated.contains("expiresAtMs") || truncated.contains("vo"),
|
||||||
|
"截断后仍应保留改动相关字段: " + truncated);
|
||||||
|
assertEquals(true, truncated.length() >= 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user