package com.codechecker.cache.diff; import com.codechecker.cache.schema.FieldSchema; import com.codechecker.cache.schema.JsonType; import com.codechecker.cache.schema.TypeSchema; import org.junit.jupiter.api.Test; import java.util.List; import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; class SchemaDifferTest { @Test void detectsWrapperMoveAndAddition() { TypeSchema oldS = new TypeSchema("TenantVO"); oldS.add(new FieldSchema("dbName", JsonType.STRING, "String")); oldS.add(new FieldSchema("linkList", JsonType.ARRAY, "List")); oldS.add(new FieldSchema("linkList[]", JsonType.OBJECT, "TenantLinkModel")); oldS.add(new FieldSchema("linkList[].id", JsonType.STRING, "String")); TypeSchema newS = new TypeSchema("CacheEnvelope"); newS.add(new FieldSchema("vo", JsonType.OBJECT, "TenantVO")); newS.add(new FieldSchema("vo.dbName", JsonType.STRING, "String")); newS.add(new FieldSchema("vo.linkList", JsonType.ARRAY, "List")); newS.add(new FieldSchema("vo.linkList[]", JsonType.OBJECT, "TenantLinkModel")); newS.add(new FieldSchema("vo.linkList[].id", JsonType.STRING, "String")); newS.add(new FieldSchema("expiresAtMs", JsonType.NUMBER, "Long")); List changes = new SchemaDiffer().diff(oldS, newS); List types = changes.stream().map(SchemaChange::getChangeType).collect(Collectors.toList()); assertTrue(types.contains(ChangeType.WRAPPER_ADDED), "应检测到包装层新增"); assertTrue(types.contains(ChangeType.FIELD_PATH_MOVED), "应检测到字段路径迁移"); assertTrue(types.contains(ChangeType.FIELD_ADDED), "应检测到新增字段 expiresAtMs"); long moved = changes.stream().filter(c -> c.getChangeType() == ChangeType.FIELD_PATH_MOVED).count(); assertEquals(2, moved, "dbName 与 linkList[].id 均应迁移"); } @Test void detectsWrapperRemovalAndUpwardMoves() { TypeSchema oldS = new TypeSchema("CacheEnvelope"); oldS.add(new FieldSchema("vo", JsonType.OBJECT, "TenantVO")); oldS.add(new FieldSchema("vo.dbName", JsonType.STRING, "String")); oldS.add(new FieldSchema("vo.linkList", JsonType.ARRAY, "List")); oldS.add(new FieldSchema("vo.linkList[]", JsonType.OBJECT, "TenantLinkModel")); oldS.add(new FieldSchema("vo.linkList[].id", JsonType.STRING, "String")); TypeSchema newS = new TypeSchema("TenantVO"); newS.add(new FieldSchema("dbName", JsonType.STRING, "String")); newS.add(new FieldSchema("linkList", JsonType.ARRAY, "List")); newS.add(new FieldSchema("linkList[]", JsonType.OBJECT, "TenantLinkModel")); newS.add(new FieldSchema("linkList[].id", JsonType.STRING, "String")); List changes = new SchemaDiffer().diff(oldS, newS); List types = changes.stream().map(SchemaChange::getChangeType).collect(Collectors.toList()); assertTrue(types.contains(ChangeType.WRAPPER_REMOVED), "应检测到包装层删除"); assertTrue(types.contains(ChangeType.FIELD_PATH_MOVED), "应检测到字段上提迁移"); assertTrue(types.stream().noneMatch(t -> t == ChangeType.FIELD_ADDED), "上提字段不应再被当成新增"); assertTrue(types.stream().noneMatch(t -> t == ChangeType.FIELD_REMOVED), "上提字段不应再被当成删除"); SchemaChange wrapper = changes.stream() .filter(c -> c.getChangeType() == ChangeType.WRAPPER_REMOVED) .findFirst() .orElseThrow(IllegalStateException::new); assertEquals("vo", wrapper.getFieldPath()); long moved = changes.stream().filter(c -> c.getChangeType() == ChangeType.FIELD_PATH_MOVED).count(); assertEquals(2, moved, "vo.dbName / vo.linkList[].id 均应上提"); } @Test void detectsTypeChange() { TypeSchema oldS = new TypeSchema("A"); oldS.add(new FieldSchema("count", JsonType.STRING, "String")); TypeSchema newS = new TypeSchema("A"); newS.add(new FieldSchema("count", JsonType.NUMBER, "Integer")); List changes = new SchemaDiffer().diff(oldS, newS); assertEquals(1, changes.size()); assertEquals(ChangeType.TYPE_CHANGED, changes.get(0).getChangeType()); assertEquals(Severity.P0, changes.get(0).getSeverity()); assertTrue(changes.get(0).getMessage().contains("String")); assertTrue(changes.get(0).getMessage().contains("Integer")); } @Test void detectsBigDecimalToStringEvenWhenBothWereStringBucketHistorically() { // 即便 JsonType 同为 STRING,javaType 变化也必须检出(用户真实漏报场景) TypeSchema oldS = new TypeSchema("A"); oldS.add(new FieldSchema("amount", JsonType.STRING, "BigDecimal")); TypeSchema newS = new TypeSchema("A"); newS.add(new FieldSchema("amount", JsonType.STRING, "String")); List changes = new SchemaDiffer().diff(oldS, newS); assertEquals(1, changes.size()); assertEquals(ChangeType.TYPE_CHANGED, changes.get(0).getChangeType()); assertEquals("amount", changes.get(0).getFieldPath()); assertTrue(changes.get(0).getOldValue().contains("BigDecimal")); assertTrue(changes.get(0).getNewValue().contains("String")); } @Test void detectsBigDecimalToStringViaJsonTypeAndJavaType() { TypeSchema oldS = new TypeSchema("A"); oldS.add(new FieldSchema("amount", JsonType.NUMBER, "BigDecimal")); TypeSchema newS = new TypeSchema("A"); newS.add(new FieldSchema("amount", JsonType.STRING, "String")); List changes = new SchemaDiffer().diff(oldS, newS); assertEquals(1, changes.size()); assertEquals(ChangeType.TYPE_CHANGED, changes.get(0).getChangeType()); } @Test void detectsIntegerToLongWithinNumberBucket() { TypeSchema oldS = new TypeSchema("A"); oldS.add(new FieldSchema("id", JsonType.NUMBER, "Integer")); TypeSchema newS = new TypeSchema("A"); newS.add(new FieldSchema("id", JsonType.NUMBER, "Long")); List changes = new SchemaDiffer().diff(oldS, newS); assertEquals(1, changes.size()); assertEquals(ChangeType.TYPE_CHANGED, changes.get(0).getChangeType()); } @Test void noChangeWhenPrimitiveAndWrapperEquivalent() { TypeSchema a = new TypeSchema("A"); a.add(new FieldSchema("n", JsonType.NUMBER, "int")); TypeSchema b = new TypeSchema("A"); b.add(new FieldSchema("n", JsonType.NUMBER, "Integer")); assertTrue(new SchemaDiffer().diff(a, b).isEmpty()); } @Test void noChangeWhenIdentical() { TypeSchema a = new TypeSchema("A"); a.add(new FieldSchema("x", JsonType.STRING, "String")); TypeSchema b = new TypeSchema("A"); b.add(new FieldSchema("x", JsonType.STRING, "String")); assertTrue(new SchemaDiffer().diff(a, b).isEmpty()); } }