Skip to content

Commit 9bb7722

Browse files
mashraf-222claude
andcommitted
fix: reject vacuous equivalence and deserialization error false matches in Java comparator
Guard against two correctness gaps where the comparator could return equivalent=true with zero evidence: empty databases and identical DeserializationError maps. Add tracking counters and stderr logging. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 919551e commit 9bb7722

3 files changed

Lines changed: 339 additions & 23 deletions

File tree

codeflash-java-runtime/src/main/java/com/codeflash/Comparator.java

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,6 @@ public static void main(String[] args) {
5050
return;
5151
}
5252

53-
String originalDbPath = args[0];
54-
String candidateDbPath = args[1];
55-
5653
try {
5754
Class.forName("org.sqlite.JDBC");
5855
} catch (ClassNotFoundException e) {
@@ -61,80 +58,100 @@ public static void main(String[] args) {
6158
return;
6259
}
6360

64-
Map<String, byte[]> originalResults;
65-
Map<String, byte[]> candidateResults;
66-
61+
String result;
6762
try {
68-
originalResults = readTestResults(originalDbPath);
63+
result = compareDatabases(args[0], args[1]);
6964
} catch (Exception e) {
70-
printError("Failed to read original database: " + e.getMessage());
65+
printError(e.getMessage());
7166
System.exit(2);
7267
return;
7368
}
7469

75-
try {
76-
candidateResults = readTestResults(candidateDbPath);
77-
} catch (Exception e) {
78-
printError("Failed to read candidate database: " + e.getMessage());
79-
System.exit(2);
80-
return;
81-
}
70+
System.out.println(result);
71+
boolean equivalent = result.startsWith("{\"equivalent\":true");
72+
System.exit(equivalent ? 0 : 1);
73+
}
74+
75+
static String compareDatabases(String originalDbPath, String candidateDbPath) throws Exception {
76+
Map<String, byte[]> originalResults = readTestResults(originalDbPath);
77+
Map<String, byte[]> candidateResults = readTestResults(candidateDbPath);
8278

8379
Set<String> allKeys = new LinkedHashSet<>();
8480
allKeys.addAll(originalResults.keySet());
8581
allKeys.addAll(candidateResults.keySet());
8682

8783
List<String> diffs = new ArrayList<>();
8884
int totalInvocations = allKeys.size();
85+
int actualComparisons = 0;
86+
int skippedPlaceholders = 0;
87+
int skippedDeserializationErrors = 0;
8988

9089
for (String key : allKeys) {
9190
byte[] origBytes = originalResults.get(key);
9291
byte[] candBytes = candidateResults.get(key);
9392

9493
if (origBytes == null && candBytes == null) {
95-
// Both null (void methods) — equivalent
94+
// Both null (void methods) — a real comparison (void-to-void match)
95+
actualComparisons++;
9696
continue;
9797
}
9898

9999
if (origBytes == null) {
100100
Object candObj = safeDeserialize(candBytes);
101101
diffs.add(formatDiff("missing", key, 0, null, safeToString(candObj)));
102+
actualComparisons++;
102103
continue;
103104
}
104105

105106
if (candBytes == null) {
106107
Object origObj = safeDeserialize(origBytes);
107108
diffs.add(formatDiff("missing", key, 0, safeToString(origObj), null));
109+
actualComparisons++;
108110
continue;
109111
}
110112

111113
Object origObj = safeDeserialize(origBytes);
112114
Object candObj = safeDeserialize(candBytes);
113115

116+
if (isDeserializationError(origObj) || isDeserializationError(candObj)) {
117+
skippedDeserializationErrors++;
118+
continue;
119+
}
120+
114121
try {
115122
if (!compare(origObj, candObj)) {
116123
diffs.add(formatDiff("return_value", key, 0, safeToString(origObj), safeToString(candObj)));
117124
}
125+
actualComparisons++;
118126
} catch (KryoPlaceholderAccessException e) {
119-
// Placeholder detected — skip comparison for this invocation
127+
skippedPlaceholders++;
120128
continue;
121129
}
122130
}
123131

124-
boolean equivalent = diffs.isEmpty();
132+
boolean equivalent = diffs.isEmpty() && actualComparisons > 0;
133+
134+
System.err.println("[codeflash-comparator] total=" + totalInvocations
135+
+ " compared=" + actualComparisons
136+
+ " skipped_placeholders=" + skippedPlaceholders
137+
+ " skipped_deser_errors=" + skippedDeserializationErrors
138+
+ " diffs=" + diffs.size()
139+
+ " equivalent=" + equivalent);
125140

126141
StringBuilder json = new StringBuilder();
127142
json.append("{\"equivalent\":").append(equivalent);
128143
json.append(",\"totalInvocations\":").append(totalInvocations);
144+
json.append(",\"actualComparisons\":").append(actualComparisons);
145+
json.append(",\"skippedPlaceholders\":").append(skippedPlaceholders);
146+
json.append(",\"skippedDeserializationErrors\":").append(skippedDeserializationErrors);
129147
json.append(",\"diffs\":[");
130148
for (int i = 0; i < diffs.size(); i++) {
131149
if (i > 0) json.append(",");
132150
json.append(diffs.get(i));
133151
}
134152
json.append("]}");
135153

136-
System.out.println(json.toString());
137-
System.exit(equivalent ? 0 : 1);
154+
return json.toString();
138155
}
139156

140157
private static Map<String, byte[]> readTestResults(String dbPath) throws Exception {
@@ -178,6 +195,11 @@ private static Object safeDeserialize(byte[] data) {
178195
}
179196
}
180197

198+
static boolean isDeserializationError(Object obj) {
199+
if (!(obj instanceof Map)) return false;
200+
return "DeserializationError".equals(((Map<?, ?>) obj).get("__type"));
201+
}
202+
181203
private static String safeToString(Object obj) {
182204
if (obj == null) {
183205
return "null";

0 commit comments

Comments
 (0)