refactor
This commit is contained in:
parent
0c079494af
commit
f681e221f5
13 changed files with 3877 additions and 2403 deletions
|
|
@ -88,8 +88,8 @@ public final class CodeFlash {
|
|||
*/
|
||||
public static void captureInput(String methodId, Object... args) {
|
||||
long callId = callIdCounter.incrementAndGet();
|
||||
String argsJson = Serializer.toJson(args);
|
||||
getWriter().recordInput(callId, methodId, argsJson, System.nanoTime());
|
||||
byte[] argsBytes = Serializer.serialize(args);
|
||||
getWriter().recordInput(callId, methodId, argsBytes, System.nanoTime());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -102,8 +102,8 @@ public final class CodeFlash {
|
|||
*/
|
||||
public static <T> T captureOutput(String methodId, T result) {
|
||||
long callId = callIdCounter.get(); // Use same callId as input
|
||||
String resultJson = Serializer.toJson(result);
|
||||
getWriter().recordOutput(callId, methodId, resultJson, System.nanoTime());
|
||||
byte[] resultBytes = Serializer.serialize(result);
|
||||
getWriter().recordOutput(callId, methodId, resultBytes, System.nanoTime());
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -115,8 +115,8 @@ public final class CodeFlash {
|
|||
*/
|
||||
public static void captureException(String methodId, Throwable error) {
|
||||
long callId = callIdCounter.get();
|
||||
String errorJson = Serializer.exceptionToJson(error);
|
||||
getWriter().recordError(callId, methodId, errorJson, System.nanoTime());
|
||||
byte[] errorBytes = Serializer.serializeException(error);
|
||||
getWriter().recordError(callId, methodId, errorBytes, System.nanoTime());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,38 +1,27 @@
|
|||
package com.codeflash;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Compares test results between original and optimized code.
|
||||
* Deep object comparison for verifying serialization/deserialization correctness.
|
||||
*
|
||||
* Used by CodeFlash to verify that optimized code produces the
|
||||
* same outputs as the original code for the same inputs.
|
||||
*
|
||||
* Can be run as a CLI tool:
|
||||
* java -jar codeflash-runtime.jar original.db candidate.db
|
||||
* This comparator is used to verify that objects survive the serialize-deserialize
|
||||
* cycle correctly. It handles:
|
||||
* - Primitives and wrappers with epsilon tolerance for floats
|
||||
* - Collections, Maps, and Arrays
|
||||
* - Custom objects via reflection
|
||||
* - NaN and Infinity special cases
|
||||
* - Exception comparison
|
||||
* - Placeholder rejection
|
||||
*/
|
||||
public final class Comparator {
|
||||
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
.serializeNulls()
|
||||
.setPrettyPrinting()
|
||||
.create();
|
||||
|
||||
// Tolerance for floating point comparison
|
||||
private static final double EPSILON = 1e-9;
|
||||
|
||||
private Comparator() {
|
||||
|
|
@ -40,346 +29,481 @@ public final class Comparator {
|
|||
}
|
||||
|
||||
/**
|
||||
* Main entry point for CLI usage.
|
||||
* Compare two objects for deep equality.
|
||||
*
|
||||
* @param args [originalDb, candidateDb]
|
||||
* @param orig The original object
|
||||
* @param newObj The object to compare against
|
||||
* @return true if objects are equivalent
|
||||
* @throws KryoPlaceholderAccessException if comparison involves a placeholder
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
if (args.length != 2) {
|
||||
System.err.println("Usage: java -jar codeflash-runtime.jar <original.db> <candidate.db>");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
ComparisonResult result = compare(args[0], args[1]);
|
||||
System.out.println(GSON.toJson(result));
|
||||
System.exit(result.isEquivalent() ? 0 : 1);
|
||||
} catch (Exception e) {
|
||||
JsonObject error = new JsonObject();
|
||||
error.addProperty("error", e.getMessage());
|
||||
System.out.println(GSON.toJson(error));
|
||||
System.exit(2);
|
||||
}
|
||||
public static boolean compare(Object orig, Object newObj) {
|
||||
return compareInternal(orig, newObj, new IdentityHashMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two result databases.
|
||||
* Compare two objects, returning a detailed result.
|
||||
*
|
||||
* @param originalDbPath Path to original results database
|
||||
* @param candidateDbPath Path to candidate results database
|
||||
* @return Comparison result with list of differences
|
||||
* @param orig The original object
|
||||
* @param newObj The object to compare against
|
||||
* @return ComparisonResult with details about the comparison
|
||||
*/
|
||||
public static ComparisonResult compare(String originalDbPath, String candidateDbPath) throws SQLException {
|
||||
List<Diff> diffs = new ArrayList<>();
|
||||
|
||||
try (Connection originalConn = DriverManager.getConnection("jdbc:sqlite:" + originalDbPath);
|
||||
Connection candidateConn = DriverManager.getConnection("jdbc:sqlite:" + candidateDbPath)) {
|
||||
|
||||
// Get all invocations from original
|
||||
List<Invocation> originalInvocations = getInvocations(originalConn);
|
||||
List<Invocation> candidateInvocations = getInvocations(candidateConn);
|
||||
|
||||
// Create lookup map for candidate invocations
|
||||
java.util.Map<Long, Invocation> candidateMap = new java.util.HashMap<>();
|
||||
for (Invocation inv : candidateInvocations) {
|
||||
candidateMap.put(inv.callId, inv);
|
||||
}
|
||||
|
||||
// Compare each original invocation with candidate
|
||||
for (Invocation original : originalInvocations) {
|
||||
Invocation candidate = candidateMap.get(original.callId);
|
||||
|
||||
if (candidate == null) {
|
||||
diffs.add(new Diff(
|
||||
original.callId,
|
||||
original.methodId,
|
||||
DiffType.MISSING_IN_CANDIDATE,
|
||||
"Invocation not found in candidate",
|
||||
original.resultJson,
|
||||
null
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compare results
|
||||
if (!compareJsonValues(original.resultJson, candidate.resultJson)) {
|
||||
diffs.add(new Diff(
|
||||
original.callId,
|
||||
original.methodId,
|
||||
DiffType.RETURN_VALUE,
|
||||
"Return values differ",
|
||||
original.resultJson,
|
||||
candidate.resultJson
|
||||
));
|
||||
}
|
||||
|
||||
// Compare errors
|
||||
boolean originalHasError = original.errorJson != null && !original.errorJson.isEmpty();
|
||||
boolean candidateHasError = candidate.errorJson != null && !candidate.errorJson.isEmpty();
|
||||
|
||||
if (originalHasError != candidateHasError) {
|
||||
diffs.add(new Diff(
|
||||
original.callId,
|
||||
original.methodId,
|
||||
DiffType.EXCEPTION,
|
||||
originalHasError ? "Original threw exception, candidate did not" :
|
||||
"Candidate threw exception, original did not",
|
||||
original.errorJson,
|
||||
candidate.errorJson
|
||||
));
|
||||
} else if (originalHasError && !compareExceptions(original.errorJson, candidate.errorJson)) {
|
||||
diffs.add(new Diff(
|
||||
original.callId,
|
||||
original.methodId,
|
||||
DiffType.EXCEPTION,
|
||||
"Exception details differ",
|
||||
original.errorJson,
|
||||
candidate.errorJson
|
||||
));
|
||||
}
|
||||
|
||||
// Remove from map to track extra invocations
|
||||
candidateMap.remove(original.callId);
|
||||
}
|
||||
|
||||
// Check for extra invocations in candidate
|
||||
for (Invocation extra : candidateMap.values()) {
|
||||
diffs.add(new Diff(
|
||||
extra.callId,
|
||||
extra.methodId,
|
||||
DiffType.EXTRA_IN_CANDIDATE,
|
||||
"Extra invocation in candidate",
|
||||
null,
|
||||
extra.resultJson
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return new ComparisonResult(diffs.isEmpty(), diffs);
|
||||
}
|
||||
|
||||
private static List<Invocation> getInvocations(Connection conn) throws SQLException {
|
||||
List<Invocation> invocations = new ArrayList<>();
|
||||
String sql = "SELECT test_class_name, function_getting_tested, loop_index, iteration_id, return_value " +
|
||||
"FROM test_results ORDER BY loop_index, iteration_id";
|
||||
|
||||
try (PreparedStatement stmt = conn.prepareStatement(sql);
|
||||
ResultSet rs = stmt.executeQuery()) {
|
||||
|
||||
while (rs.next()) {
|
||||
String testClassName = rs.getString("test_class_name");
|
||||
String functionName = rs.getString("function_getting_tested");
|
||||
int loopIndex = rs.getInt("loop_index");
|
||||
String iterationId = rs.getString("iteration_id");
|
||||
String returnValue = rs.getString("return_value");
|
||||
|
||||
// Create unique call_id from loop_index and iteration_id
|
||||
// Parse iteration_id which is in format "iter_testIteration" (e.g., "1_0")
|
||||
long callId = (loopIndex * 10000L) + parseIterationId(iterationId);
|
||||
|
||||
// Construct method_id as "ClassName.methodName"
|
||||
String methodId = testClassName + "." + functionName;
|
||||
|
||||
invocations.add(new Invocation(
|
||||
callId,
|
||||
methodId,
|
||||
null, // args_json not captured in test_results schema
|
||||
returnValue, // return_value maps to resultJson
|
||||
null // error_json not captured in test_results schema
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return invocations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse iteration_id string to extract the numeric iteration number.
|
||||
* Format: "iter_testIteration" (e.g., "1_0" → 1)
|
||||
*/
|
||||
private static long parseIterationId(String iterationId) {
|
||||
if (iterationId == null || iterationId.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
public static ComparisonResult compareWithDetails(Object orig, Object newObj) {
|
||||
try {
|
||||
// Split by underscore and take the first part
|
||||
String[] parts = iterationId.split("_");
|
||||
return Long.parseLong(parts[0]);
|
||||
} catch (Exception e) {
|
||||
// If parsing fails, try to parse the whole string
|
||||
try {
|
||||
return Long.parseLong(iterationId);
|
||||
} catch (Exception ex) {
|
||||
return 0;
|
||||
boolean equal = compareInternal(orig, newObj, new IdentityHashMap<>());
|
||||
return new ComparisonResult(equal, null);
|
||||
} catch (KryoPlaceholderAccessException e) {
|
||||
return new ComparisonResult(false, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean compareInternal(Object orig, Object newObj,
|
||||
IdentityHashMap<Object, Object> seen) {
|
||||
// Handle nulls
|
||||
if (orig == null && newObj == null) {
|
||||
return true;
|
||||
}
|
||||
if (orig == null || newObj == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Detect and reject KryoPlaceholder
|
||||
if (orig instanceof KryoPlaceholder) {
|
||||
KryoPlaceholder p = (KryoPlaceholder) orig;
|
||||
throw new KryoPlaceholderAccessException(
|
||||
"Cannot compare: original contains placeholder for unserializable object",
|
||||
p.getObjType(), p.getPath());
|
||||
}
|
||||
if (newObj instanceof KryoPlaceholder) {
|
||||
KryoPlaceholder p = (KryoPlaceholder) newObj;
|
||||
throw new KryoPlaceholderAccessException(
|
||||
"Cannot compare: new object contains placeholder for unserializable object",
|
||||
p.getObjType(), p.getPath());
|
||||
}
|
||||
|
||||
// Handle exceptions specially
|
||||
if (orig instanceof Throwable && newObj instanceof Throwable) {
|
||||
return compareExceptions((Throwable) orig, (Throwable) newObj);
|
||||
}
|
||||
|
||||
Class<?> origClass = orig.getClass();
|
||||
Class<?> newClass = newObj.getClass();
|
||||
|
||||
// Check type compatibility
|
||||
if (!origClass.equals(newClass)) {
|
||||
if (!areTypesCompatible(origClass, newClass)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle primitives and wrappers
|
||||
if (orig instanceof Boolean) {
|
||||
return orig.equals(newObj);
|
||||
}
|
||||
if (orig instanceof Character) {
|
||||
return orig.equals(newObj);
|
||||
}
|
||||
if (orig instanceof String) {
|
||||
return orig.equals(newObj);
|
||||
}
|
||||
if (orig instanceof Number) {
|
||||
return compareNumbers((Number) orig, (Number) newObj);
|
||||
}
|
||||
|
||||
// Handle enums
|
||||
if (origClass.isEnum()) {
|
||||
return orig.equals(newObj);
|
||||
}
|
||||
|
||||
// Handle Class objects
|
||||
if (orig instanceof Class) {
|
||||
return orig.equals(newObj);
|
||||
}
|
||||
|
||||
// Handle date/time types
|
||||
if (orig instanceof Date || orig instanceof LocalDateTime ||
|
||||
orig instanceof LocalDate || orig instanceof LocalTime) {
|
||||
return orig.equals(newObj);
|
||||
}
|
||||
|
||||
// Handle Optional
|
||||
if (orig instanceof Optional && newObj instanceof Optional) {
|
||||
return compareOptionals((Optional<?>) orig, (Optional<?>) newObj, seen);
|
||||
}
|
||||
|
||||
// Check for circular reference to prevent infinite recursion
|
||||
if (seen.containsKey(orig)) {
|
||||
// If we've seen this object before, just check identity
|
||||
return seen.get(orig) == newObj;
|
||||
}
|
||||
seen.put(orig, newObj);
|
||||
|
||||
try {
|
||||
// Handle arrays
|
||||
if (origClass.isArray()) {
|
||||
return compareArrays(orig, newObj, seen);
|
||||
}
|
||||
|
||||
// Handle collections
|
||||
if (orig instanceof Collection && newObj instanceof Collection) {
|
||||
return compareCollections((Collection<?>) orig, (Collection<?>) newObj, seen);
|
||||
}
|
||||
|
||||
// Handle maps
|
||||
if (orig instanceof Map && newObj instanceof Map) {
|
||||
return compareMaps((Map<?, ?>) orig, (Map<?, ?>) newObj, seen);
|
||||
}
|
||||
|
||||
// Handle general objects via reflection
|
||||
return compareObjects(orig, newObj, seen);
|
||||
|
||||
} finally {
|
||||
seen.remove(orig);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two JSON values for equivalence.
|
||||
* Check if two types are compatible for comparison.
|
||||
*/
|
||||
private static boolean compareJsonValues(String json1, String json2) {
|
||||
if (json1 == null && json2 == null) return true;
|
||||
if (json1 == null || json2 == null) return false;
|
||||
if (json1.equals(json2)) return true;
|
||||
|
||||
try {
|
||||
JsonElement elem1 = JsonParser.parseString(json1);
|
||||
JsonElement elem2 = JsonParser.parseString(json2);
|
||||
return compareJsonElements(elem1, elem2);
|
||||
} catch (Exception e) {
|
||||
// If parsing fails, fall back to string comparison
|
||||
return json1.equals(json2);
|
||||
private static boolean areTypesCompatible(Class<?> type1, Class<?> type2) {
|
||||
// Allow comparing different Collection implementations
|
||||
if (Collection.class.isAssignableFrom(type1) && Collection.class.isAssignableFrom(type2)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean compareJsonElements(JsonElement elem1, JsonElement elem2) {
|
||||
if (elem1 == null && elem2 == null) return true;
|
||||
if (elem1 == null || elem2 == null) return false;
|
||||
if (elem1.isJsonNull() && elem2.isJsonNull()) return true;
|
||||
|
||||
// Compare primitives
|
||||
if (elem1.isJsonPrimitive() && elem2.isJsonPrimitive()) {
|
||||
return comparePrimitives(elem1.getAsJsonPrimitive(), elem2.getAsJsonPrimitive());
|
||||
// Allow comparing different Map implementations
|
||||
if (Map.class.isAssignableFrom(type1) && Map.class.isAssignableFrom(type2)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compare arrays
|
||||
if (elem1.isJsonArray() && elem2.isJsonArray()) {
|
||||
return compareArrays(elem1.getAsJsonArray(), elem2.getAsJsonArray());
|
||||
// Allow comparing different Number types
|
||||
if (Number.class.isAssignableFrom(type1) && Number.class.isAssignableFrom(type2)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compare objects
|
||||
if (elem1.isJsonObject() && elem2.isJsonObject()) {
|
||||
return compareObjects(elem1.getAsJsonObject(), elem2.getAsJsonObject());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean comparePrimitives(com.google.gson.JsonPrimitive p1, com.google.gson.JsonPrimitive p2) {
|
||||
// Handle numeric comparison with epsilon
|
||||
if (p1.isNumber() && p2.isNumber()) {
|
||||
double d1 = p1.getAsDouble();
|
||||
double d2 = p2.getAsDouble();
|
||||
/**
|
||||
* Compare two numbers with epsilon tolerance for floating point.
|
||||
*/
|
||||
private static boolean compareNumbers(Number n1, Number n2) {
|
||||
// Handle BigDecimal - exact comparison using compareTo
|
||||
if (n1 instanceof java.math.BigDecimal && n2 instanceof java.math.BigDecimal) {
|
||||
return ((java.math.BigDecimal) n1).compareTo((java.math.BigDecimal) n2) == 0;
|
||||
}
|
||||
|
||||
// Handle BigInteger - exact comparison using equals
|
||||
if (n1 instanceof java.math.BigInteger && n2 instanceof java.math.BigInteger) {
|
||||
return n1.equals(n2);
|
||||
}
|
||||
|
||||
// Handle BigDecimal vs other number types
|
||||
if (n1 instanceof java.math.BigDecimal || n2 instanceof java.math.BigDecimal) {
|
||||
java.math.BigDecimal bd1 = toBigDecimal(n1);
|
||||
java.math.BigDecimal bd2 = toBigDecimal(n2);
|
||||
return bd1.compareTo(bd2) == 0;
|
||||
}
|
||||
|
||||
// Handle BigInteger vs other number types
|
||||
if (n1 instanceof java.math.BigInteger || n2 instanceof java.math.BigInteger) {
|
||||
java.math.BigInteger bi1 = toBigInteger(n1);
|
||||
java.math.BigInteger bi2 = toBigInteger(n2);
|
||||
return bi1.equals(bi2);
|
||||
}
|
||||
|
||||
// Handle floating point with epsilon
|
||||
if (n1 instanceof Double || n1 instanceof Float ||
|
||||
n2 instanceof Double || n2 instanceof Float) {
|
||||
|
||||
double d1 = n1.doubleValue();
|
||||
double d2 = n2.doubleValue();
|
||||
|
||||
// Handle NaN
|
||||
if (Double.isNaN(d1) && Double.isNaN(d2)) return true;
|
||||
// Handle infinity
|
||||
if (Double.isNaN(d1) && Double.isNaN(d2)) {
|
||||
return true;
|
||||
}
|
||||
if (Double.isNaN(d1) || Double.isNaN(d2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle Infinity
|
||||
if (Double.isInfinite(d1) && Double.isInfinite(d2)) {
|
||||
return (d1 > 0) == (d2 > 0);
|
||||
return (d1 > 0) == (d2 > 0); // Same sign
|
||||
}
|
||||
// Compare with epsilon
|
||||
return Math.abs(d1 - d2) < EPSILON;
|
||||
if (Double.isInfinite(d1) || Double.isInfinite(d2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare with relative and absolute epsilon
|
||||
double diff = Math.abs(d1 - d2);
|
||||
if (diff < EPSILON) {
|
||||
return true; // Absolute tolerance
|
||||
}
|
||||
// Relative tolerance for large numbers
|
||||
double maxAbs = Math.max(Math.abs(d1), Math.abs(d2));
|
||||
return diff <= EPSILON * maxAbs;
|
||||
}
|
||||
|
||||
return Objects.equals(p1, p2);
|
||||
// Integer types - exact comparison
|
||||
return n1.longValue() == n2.longValue();
|
||||
}
|
||||
|
||||
private static boolean compareArrays(JsonArray arr1, JsonArray arr2) {
|
||||
if (arr1.size() != arr2.size()) return false;
|
||||
/**
|
||||
* Convert a Number to BigDecimal.
|
||||
*/
|
||||
private static java.math.BigDecimal toBigDecimal(Number n) {
|
||||
if (n instanceof java.math.BigDecimal) {
|
||||
return (java.math.BigDecimal) n;
|
||||
}
|
||||
if (n instanceof java.math.BigInteger) {
|
||||
return new java.math.BigDecimal((java.math.BigInteger) n);
|
||||
}
|
||||
if (n instanceof Double || n instanceof Float) {
|
||||
return java.math.BigDecimal.valueOf(n.doubleValue());
|
||||
}
|
||||
return java.math.BigDecimal.valueOf(n.longValue());
|
||||
}
|
||||
|
||||
for (int i = 0; i < arr1.size(); i++) {
|
||||
if (!compareJsonElements(arr1.get(i), arr2.get(i))) {
|
||||
/**
|
||||
* Convert a Number to BigInteger.
|
||||
*/
|
||||
private static java.math.BigInteger toBigInteger(Number n) {
|
||||
if (n instanceof java.math.BigInteger) {
|
||||
return (java.math.BigInteger) n;
|
||||
}
|
||||
if (n instanceof java.math.BigDecimal) {
|
||||
return ((java.math.BigDecimal) n).toBigInteger();
|
||||
}
|
||||
return java.math.BigInteger.valueOf(n.longValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two exceptions.
|
||||
*/
|
||||
private static boolean compareExceptions(Throwable orig, Throwable newEx) {
|
||||
// Must be same type
|
||||
if (!orig.getClass().equals(newEx.getClass())) {
|
||||
return false;
|
||||
}
|
||||
// Compare message (both may be null)
|
||||
return Objects.equals(orig.getMessage(), newEx.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two Optional values.
|
||||
*/
|
||||
private static boolean compareOptionals(Optional<?> orig, Optional<?> newOpt,
|
||||
IdentityHashMap<Object, Object> seen) {
|
||||
if (orig.isPresent() != newOpt.isPresent()) {
|
||||
return false;
|
||||
}
|
||||
if (!orig.isPresent()) {
|
||||
return true; // Both empty
|
||||
}
|
||||
return compareInternal(orig.get(), newOpt.get(), seen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two arrays.
|
||||
*/
|
||||
private static boolean compareArrays(Object orig, Object newObj,
|
||||
IdentityHashMap<Object, Object> seen) {
|
||||
int length1 = Array.getLength(orig);
|
||||
int length2 = Array.getLength(newObj);
|
||||
|
||||
if (length1 != length2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < length1; i++) {
|
||||
Object elem1 = Array.get(orig, i);
|
||||
Object elem2 = Array.get(newObj, i);
|
||||
if (!compareInternal(elem1, elem2, seen)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two collections.
|
||||
*/
|
||||
private static boolean compareCollections(Collection<?> orig, Collection<?> newColl,
|
||||
IdentityHashMap<Object, Object> seen) {
|
||||
if (orig.size() != newColl.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For Sets, compare element-by-element (order doesn't matter)
|
||||
if (orig instanceof Set && newColl instanceof Set) {
|
||||
return compareSets((Set<?>) orig, (Set<?>) newColl, seen);
|
||||
}
|
||||
|
||||
// For ordered collections (List, etc.), compare in order
|
||||
Iterator<?> iter1 = orig.iterator();
|
||||
Iterator<?> iter2 = newColl.iterator();
|
||||
|
||||
while (iter1.hasNext() && iter2.hasNext()) {
|
||||
if (!compareInternal(iter1.next(), iter2.next(), seen)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return !iter1.hasNext() && !iter2.hasNext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two sets (order-independent).
|
||||
*/
|
||||
private static boolean compareSets(Set<?> orig, Set<?> newSet,
|
||||
IdentityHashMap<Object, Object> seen) {
|
||||
if (orig.size() != newSet.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For each element in orig, find a matching element in newSet
|
||||
for (Object elem1 : orig) {
|
||||
boolean found = false;
|
||||
for (Object elem2 : newSet) {
|
||||
try {
|
||||
if (compareInternal(elem1, elem2, new IdentityHashMap<>(seen))) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
} catch (KryoPlaceholderAccessException e) {
|
||||
// Propagate placeholder exceptions
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean compareObjects(JsonObject obj1, JsonObject obj2) {
|
||||
// Skip type metadata for comparison
|
||||
java.util.Set<String> keys1 = new java.util.HashSet<>(obj1.keySet());
|
||||
java.util.Set<String> keys2 = new java.util.HashSet<>(obj2.keySet());
|
||||
keys1.remove("__type__");
|
||||
keys2.remove("__type__");
|
||||
/**
|
||||
* Compare two maps.
|
||||
* Uses deep comparison for keys instead of relying on equals()/hashCode().
|
||||
*/
|
||||
private static boolean compareMaps(Map<?, ?> orig, Map<?, ?> newMap,
|
||||
IdentityHashMap<Object, Object> seen) {
|
||||
if (orig.size() != newMap.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!keys1.equals(keys2)) return false;
|
||||
// For each entry in orig, find a matching entry in newMap using deep comparison
|
||||
for (Map.Entry<?, ?> entry1 : orig.entrySet()) {
|
||||
Object key1 = entry1.getKey();
|
||||
Object value1 = entry1.getValue();
|
||||
|
||||
for (String key : keys1) {
|
||||
if (!compareJsonElements(obj1.get(key), obj2.get(key))) {
|
||||
boolean foundMatch = false;
|
||||
|
||||
// Search for matching key in newMap using deep comparison
|
||||
for (Map.Entry<?, ?> entry2 : newMap.entrySet()) {
|
||||
Object key2 = entry2.getKey();
|
||||
|
||||
// Use deep comparison for keys
|
||||
try {
|
||||
if (compareInternal(key1, key2, new IdentityHashMap<>(seen))) {
|
||||
// Found matching key - now compare values
|
||||
Object value2 = entry2.getValue();
|
||||
if (!compareInternal(value1, value2, seen)) {
|
||||
return false;
|
||||
}
|
||||
foundMatch = true;
|
||||
break;
|
||||
}
|
||||
} catch (KryoPlaceholderAccessException e) {
|
||||
// Propagate placeholder exceptions
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundMatch) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean compareExceptions(String error1, String error2) {
|
||||
/**
|
||||
* Compare two objects via reflection.
|
||||
*/
|
||||
private static boolean compareObjects(Object orig, Object newObj,
|
||||
IdentityHashMap<Object, Object> seen) {
|
||||
Class<?> clazz = orig.getClass();
|
||||
|
||||
// If class has a custom equals method, use it
|
||||
try {
|
||||
JsonObject e1 = JsonParser.parseString(error1).getAsJsonObject();
|
||||
JsonObject e2 = JsonParser.parseString(error2).getAsJsonObject();
|
||||
|
||||
// Compare exception type and message
|
||||
String type1 = e1.has("type") ? e1.get("type").getAsString() : "";
|
||||
String type2 = e2.has("type") ? e2.get("type").getAsString() : "";
|
||||
|
||||
// Types must match
|
||||
return type1.equals(type2);
|
||||
if (hasCustomEquals(clazz)) {
|
||||
return orig.equals(newObj);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return error1.equals(error2);
|
||||
// Fall through to field comparison
|
||||
}
|
||||
|
||||
// Compare all fields via reflection
|
||||
Class<?> currentClass = clazz;
|
||||
while (currentClass != null && currentClass != Object.class) {
|
||||
for (Field field : currentClass.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(field.getModifiers()) ||
|
||||
Modifier.isTransient(field.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
Object value1 = field.get(orig);
|
||||
Object value2 = field.get(newObj);
|
||||
|
||||
if (!compareInternal(value1, value2, seen)) {
|
||||
return false;
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
// Can't access field - assume not equal
|
||||
return false;
|
||||
}
|
||||
}
|
||||
currentClass = currentClass.getSuperclass();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a class has a custom equals method (not from Object).
|
||||
*/
|
||||
private static boolean hasCustomEquals(Class<?> clazz) {
|
||||
try {
|
||||
java.lang.reflect.Method equalsMethod = clazz.getMethod("equals", Object.class);
|
||||
return equalsMethod.getDeclaringClass() != Object.class;
|
||||
} catch (NoSuchMethodException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Data classes
|
||||
|
||||
private static class Invocation {
|
||||
final long callId;
|
||||
final String methodId;
|
||||
final String argsJson;
|
||||
final String resultJson;
|
||||
final String errorJson;
|
||||
|
||||
Invocation(long callId, String methodId, String argsJson, String resultJson, String errorJson) {
|
||||
this.callId = callId;
|
||||
this.methodId = methodId;
|
||||
this.argsJson = argsJson;
|
||||
this.resultJson = resultJson;
|
||||
this.errorJson = errorJson;
|
||||
}
|
||||
}
|
||||
|
||||
public enum DiffType {
|
||||
RETURN_VALUE,
|
||||
EXCEPTION,
|
||||
MISSING_IN_CANDIDATE,
|
||||
EXTRA_IN_CANDIDATE
|
||||
}
|
||||
|
||||
public static class Diff {
|
||||
private final long callId;
|
||||
private final String methodId;
|
||||
private final DiffType type;
|
||||
private final String message;
|
||||
private final String originalValue;
|
||||
private final String candidateValue;
|
||||
|
||||
public Diff(long callId, String methodId, DiffType type, String message,
|
||||
String originalValue, String candidateValue) {
|
||||
this.callId = callId;
|
||||
this.methodId = methodId;
|
||||
this.type = type;
|
||||
this.message = message;
|
||||
this.originalValue = originalValue;
|
||||
this.candidateValue = candidateValue;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public long getCallId() { return callId; }
|
||||
public String getMethodId() { return methodId; }
|
||||
public DiffType getType() { return type; }
|
||||
public String getMessage() { return message; }
|
||||
public String getOriginalValue() { return originalValue; }
|
||||
public String getCandidateValue() { return candidateValue; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a comparison with optional error details.
|
||||
*/
|
||||
public static class ComparisonResult {
|
||||
private final boolean equivalent;
|
||||
private final List<Diff> diffs;
|
||||
private final boolean equal;
|
||||
private final String errorMessage;
|
||||
|
||||
public ComparisonResult(boolean equivalent, List<Diff> diffs) {
|
||||
this.equivalent = equivalent;
|
||||
this.diffs = diffs;
|
||||
public ComparisonResult(boolean equal, String errorMessage) {
|
||||
this.equal = equal;
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public boolean isEquivalent() { return equivalent; }
|
||||
public List<Diff> getDiffs() { return diffs; }
|
||||
public boolean isEqual() {
|
||||
return equal;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public boolean hasError() {
|
||||
return errorMessage != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import java.util.Objects;
|
|||
/**
|
||||
* Placeholder for objects that could not be serialized.
|
||||
*
|
||||
* When KryoSerializer encounters an object that cannot be serialized
|
||||
* When Serializer encounters an object that cannot be serialized
|
||||
* (e.g., Socket, Connection, Stream), it replaces it with a KryoPlaceholder
|
||||
* that stores metadata about the original object.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,490 +0,0 @@
|
|||
package com.codeflash;
|
||||
|
||||
import com.esotericsoftware.kryo.Kryo;
|
||||
import com.esotericsoftware.kryo.io.Input;
|
||||
import com.esotericsoftware.kryo.io.Output;
|
||||
import com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy;
|
||||
import org.objenesis.strategy.StdInstantiatorStrategy;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.Statement;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Binary serializer using Kryo with graceful handling of unserializable objects.
|
||||
*
|
||||
* This class provides Python-like dill behavior:
|
||||
* 1. Attempts direct Kryo serialization first
|
||||
* 2. On failure, recursively processes containers (Map, Collection, Array)
|
||||
* 3. Replaces truly unserializable objects with KryoPlaceholder
|
||||
*
|
||||
* Thread-safe via ThreadLocal Kryo instances.
|
||||
*/
|
||||
public final class KryoSerializer {
|
||||
|
||||
private static final int MAX_DEPTH = 10;
|
||||
private static final int MAX_COLLECTION_SIZE = 1000;
|
||||
private static final int BUFFER_SIZE = 4096;
|
||||
|
||||
// Thread-local Kryo instances (Kryo is not thread-safe)
|
||||
private static final ThreadLocal<Kryo> KRYO = ThreadLocal.withInitial(() -> {
|
||||
Kryo kryo = new Kryo();
|
||||
kryo.setRegistrationRequired(false);
|
||||
kryo.setReferences(true);
|
||||
kryo.setInstantiatorStrategy(new DefaultInstantiatorStrategy(
|
||||
new StdInstantiatorStrategy()));
|
||||
|
||||
// Register common types for efficiency
|
||||
kryo.register(ArrayList.class);
|
||||
kryo.register(LinkedList.class);
|
||||
kryo.register(HashMap.class);
|
||||
kryo.register(LinkedHashMap.class);
|
||||
kryo.register(HashSet.class);
|
||||
kryo.register(LinkedHashSet.class);
|
||||
kryo.register(TreeMap.class);
|
||||
kryo.register(TreeSet.class);
|
||||
kryo.register(KryoPlaceholder.class);
|
||||
|
||||
return kryo;
|
||||
});
|
||||
|
||||
// Cache of known unserializable types
|
||||
private static final Set<Class<?>> UNSERIALIZABLE_TYPES = ConcurrentHashMap.newKeySet();
|
||||
|
||||
static {
|
||||
// Pre-populate with known unserializable types
|
||||
UNSERIALIZABLE_TYPES.add(Socket.class);
|
||||
UNSERIALIZABLE_TYPES.add(ServerSocket.class);
|
||||
UNSERIALIZABLE_TYPES.add(InputStream.class);
|
||||
UNSERIALIZABLE_TYPES.add(OutputStream.class);
|
||||
UNSERIALIZABLE_TYPES.add(Connection.class);
|
||||
UNSERIALIZABLE_TYPES.add(Statement.class);
|
||||
UNSERIALIZABLE_TYPES.add(ResultSet.class);
|
||||
UNSERIALIZABLE_TYPES.add(Thread.class);
|
||||
UNSERIALIZABLE_TYPES.add(ThreadGroup.class);
|
||||
UNSERIALIZABLE_TYPES.add(ClassLoader.class);
|
||||
}
|
||||
|
||||
private KryoSerializer() {
|
||||
// Utility class
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize an object to bytes with graceful handling of unserializable parts.
|
||||
*
|
||||
* @param obj The object to serialize
|
||||
* @return Serialized bytes (may contain KryoPlaceholder for unserializable parts)
|
||||
*/
|
||||
public static byte[] serialize(Object obj) {
|
||||
Object processed = recursiveProcess(obj, new IdentityHashMap<>(), 0, "");
|
||||
return directSerialize(processed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize bytes back to an object.
|
||||
* The returned object may contain KryoPlaceholder instances for parts
|
||||
* that could not be serialized originally.
|
||||
*
|
||||
* @param data Serialized bytes
|
||||
* @return Deserialized object
|
||||
*/
|
||||
public static Object deserialize(byte[] data) {
|
||||
if (data == null || data.length == 0) {
|
||||
return null;
|
||||
}
|
||||
Kryo kryo = KRYO.get();
|
||||
try (Input input = new Input(data)) {
|
||||
return kryo.readClassAndObject(input);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize an exception with its metadata.
|
||||
*
|
||||
* @param error The exception to serialize
|
||||
* @return Serialized bytes containing exception information
|
||||
*/
|
||||
public static byte[] serializeException(Throwable error) {
|
||||
Map<String, Object> exceptionData = new LinkedHashMap<>();
|
||||
exceptionData.put("__exception__", true);
|
||||
exceptionData.put("type", error.getClass().getName());
|
||||
exceptionData.put("message", error.getMessage());
|
||||
|
||||
// Capture stack trace as strings
|
||||
List<String> stackTrace = new ArrayList<>();
|
||||
for (StackTraceElement element : error.getStackTrace()) {
|
||||
stackTrace.add(element.toString());
|
||||
}
|
||||
exceptionData.put("stackTrace", stackTrace);
|
||||
|
||||
// Capture cause if present
|
||||
if (error.getCause() != null) {
|
||||
exceptionData.put("causeType", error.getCause().getClass().getName());
|
||||
exceptionData.put("causeMessage", error.getCause().getMessage());
|
||||
}
|
||||
|
||||
return serialize(exceptionData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct serialization without recursive processing.
|
||||
*/
|
||||
private static byte[] directSerialize(Object obj) {
|
||||
Kryo kryo = KRYO.get();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(BUFFER_SIZE);
|
||||
try (Output output = new Output(baos)) {
|
||||
kryo.writeClassAndObject(output, obj);
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to serialize directly; returns null on failure.
|
||||
*/
|
||||
private static byte[] tryDirectSerialize(Object obj) {
|
||||
try {
|
||||
return directSerialize(obj);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively process an object, replacing unserializable parts with placeholders.
|
||||
*/
|
||||
private static Object recursiveProcess(Object obj, IdentityHashMap<Object, Object> seen,
|
||||
int depth, String path) {
|
||||
// Handle null
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Class<?> clazz = obj.getClass();
|
||||
|
||||
// Check if known unserializable type
|
||||
if (isKnownUnserializable(clazz)) {
|
||||
return KryoPlaceholder.create(obj, "Known unserializable type: " + clazz.getName(), path);
|
||||
}
|
||||
|
||||
// Check max depth
|
||||
if (depth > MAX_DEPTH) {
|
||||
return KryoPlaceholder.create(obj, "Max recursion depth exceeded", path);
|
||||
}
|
||||
|
||||
// Primitives and common immutable types - try direct serialization
|
||||
if (isPrimitiveOrWrapper(clazz) || obj instanceof String || obj instanceof Enum) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Try direct serialization first
|
||||
byte[] serialized = tryDirectSerialize(obj);
|
||||
if (serialized != null) {
|
||||
// Verify it can be deserialized
|
||||
try {
|
||||
deserialize(serialized);
|
||||
return obj; // Success - return original
|
||||
} catch (Exception e) {
|
||||
// Fall through to recursive handling
|
||||
}
|
||||
}
|
||||
|
||||
// Check for circular reference
|
||||
if (seen.containsKey(obj)) {
|
||||
return KryoPlaceholder.create(obj, "Circular reference detected", path);
|
||||
}
|
||||
seen.put(obj, Boolean.TRUE);
|
||||
|
||||
try {
|
||||
// Handle containers recursively
|
||||
if (obj instanceof Map) {
|
||||
return handleMap((Map<?, ?>) obj, seen, depth, path);
|
||||
}
|
||||
if (obj instanceof Collection) {
|
||||
return handleCollection((Collection<?>) obj, seen, depth, path);
|
||||
}
|
||||
if (clazz.isArray()) {
|
||||
return handleArray(obj, seen, depth, path);
|
||||
}
|
||||
|
||||
// Handle objects with fields
|
||||
return handleObject(obj, seen, depth, path);
|
||||
|
||||
} finally {
|
||||
seen.remove(obj);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a class is known to be unserializable.
|
||||
*/
|
||||
private static boolean isKnownUnserializable(Class<?> clazz) {
|
||||
if (UNSERIALIZABLE_TYPES.contains(clazz)) {
|
||||
return true;
|
||||
}
|
||||
// Check superclasses and interfaces
|
||||
for (Class<?> unserializable : UNSERIALIZABLE_TYPES) {
|
||||
if (unserializable.isAssignableFrom(clazz)) {
|
||||
UNSERIALIZABLE_TYPES.add(clazz); // Cache for future
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a class is a primitive or wrapper type.
|
||||
*/
|
||||
private static boolean isPrimitiveOrWrapper(Class<?> clazz) {
|
||||
return clazz.isPrimitive() ||
|
||||
clazz == Boolean.class ||
|
||||
clazz == Byte.class ||
|
||||
clazz == Character.class ||
|
||||
clazz == Short.class ||
|
||||
clazz == Integer.class ||
|
||||
clazz == Long.class ||
|
||||
clazz == Float.class ||
|
||||
clazz == Double.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Map serialization with recursive processing of values.
|
||||
*/
|
||||
private static Object handleMap(Map<?, ?> map, IdentityHashMap<Object, Object> seen,
|
||||
int depth, String path) {
|
||||
Map<Object, Object> result = new LinkedHashMap<>();
|
||||
int count = 0;
|
||||
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
if (count >= MAX_COLLECTION_SIZE) {
|
||||
result.put("__truncated__", map.size() - count + " more entries");
|
||||
break;
|
||||
}
|
||||
|
||||
Object key = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
|
||||
// Process key
|
||||
String keyStr = key != null ? key.toString() : "null";
|
||||
String keyPath = path.isEmpty() ? "[" + keyStr + "]" : path + "[" + keyStr + "]";
|
||||
|
||||
Object processedKey;
|
||||
try {
|
||||
processedKey = recursiveProcess(key, seen, depth + 1, keyPath + ".key");
|
||||
} catch (Exception e) {
|
||||
processedKey = KryoPlaceholder.create(key, e.getMessage(), keyPath + ".key");
|
||||
}
|
||||
|
||||
// Process value
|
||||
Object processedValue;
|
||||
try {
|
||||
processedValue = recursiveProcess(value, seen, depth + 1, keyPath);
|
||||
} catch (Exception e) {
|
||||
processedValue = KryoPlaceholder.create(value, e.getMessage(), keyPath);
|
||||
}
|
||||
|
||||
result.put(processedKey, processedValue);
|
||||
count++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Collection serialization with recursive processing of elements.
|
||||
*/
|
||||
private static Object handleCollection(Collection<?> collection, IdentityHashMap<Object, Object> seen,
|
||||
int depth, String path) {
|
||||
List<Object> result = new ArrayList<>();
|
||||
int count = 0;
|
||||
|
||||
for (Object item : collection) {
|
||||
if (count >= MAX_COLLECTION_SIZE) {
|
||||
result.add(KryoPlaceholder.create(null,
|
||||
collection.size() - count + " more elements truncated", path + "[truncated]"));
|
||||
break;
|
||||
}
|
||||
|
||||
String itemPath = path.isEmpty() ? "[" + count + "]" : path + "[" + count + "]";
|
||||
|
||||
try {
|
||||
result.add(recursiveProcess(item, seen, depth + 1, itemPath));
|
||||
} catch (Exception e) {
|
||||
result.add(KryoPlaceholder.create(item, e.getMessage(), itemPath));
|
||||
}
|
||||
count++;
|
||||
}
|
||||
|
||||
// Try to preserve original collection type
|
||||
if (collection instanceof Set) {
|
||||
return new LinkedHashSet<>(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Array serialization with recursive processing of elements.
|
||||
*/
|
||||
private static Object handleArray(Object array, IdentityHashMap<Object, Object> seen,
|
||||
int depth, String path) {
|
||||
int length = java.lang.reflect.Array.getLength(array);
|
||||
int limit = Math.min(length, MAX_COLLECTION_SIZE);
|
||||
|
||||
List<Object> result = new ArrayList<>();
|
||||
for (int i = 0; i < limit; i++) {
|
||||
String itemPath = path.isEmpty() ? "[" + i + "]" : path + "[" + i + "]";
|
||||
Object element = java.lang.reflect.Array.get(array, i);
|
||||
|
||||
try {
|
||||
result.add(recursiveProcess(element, seen, depth + 1, itemPath));
|
||||
} catch (Exception e) {
|
||||
result.add(KryoPlaceholder.create(element, e.getMessage(), itemPath));
|
||||
}
|
||||
}
|
||||
|
||||
if (length > limit) {
|
||||
result.add(KryoPlaceholder.create(null,
|
||||
length - limit + " more elements truncated", path + "[truncated]"));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle custom object serialization with recursive processing of fields.
|
||||
*/
|
||||
private static Object handleObject(Object obj, IdentityHashMap<Object, Object> seen,
|
||||
int depth, String path) {
|
||||
Class<?> clazz = obj.getClass();
|
||||
|
||||
// Try to create a copy with processed fields
|
||||
try {
|
||||
Object newObj = createInstance(clazz);
|
||||
if (newObj == null) {
|
||||
return KryoPlaceholder.create(obj, "Cannot instantiate class: " + clazz.getName(), path);
|
||||
}
|
||||
|
||||
// Copy and process all fields
|
||||
Class<?> currentClass = clazz;
|
||||
while (currentClass != null && currentClass != Object.class) {
|
||||
for (Field field : currentClass.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(field.getModifiers()) ||
|
||||
Modifier.isTransient(field.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(obj);
|
||||
String fieldPath = path.isEmpty() ? field.getName() : path + "." + field.getName();
|
||||
|
||||
Object processedValue = recursiveProcess(value, seen, depth + 1, fieldPath);
|
||||
field.set(newObj, processedValue);
|
||||
} catch (Exception e) {
|
||||
// Field couldn't be processed - leave as default
|
||||
}
|
||||
}
|
||||
currentClass = currentClass.getSuperclass();
|
||||
}
|
||||
|
||||
// Verify the new object can be serialized
|
||||
byte[] testSerialize = tryDirectSerialize(newObj);
|
||||
if (testSerialize != null) {
|
||||
return newObj;
|
||||
}
|
||||
|
||||
// Still can't serialize - return as map representation
|
||||
return objectToMap(obj, seen, depth, path);
|
||||
|
||||
} catch (Exception e) {
|
||||
// Fall back to map representation
|
||||
return objectToMap(obj, seen, depth, path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an object to a Map representation for serialization.
|
||||
*/
|
||||
private static Map<String, Object> objectToMap(Object obj, IdentityHashMap<Object, Object> seen,
|
||||
int depth, String path) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("__type__", obj.getClass().getName());
|
||||
|
||||
Class<?> currentClass = obj.getClass();
|
||||
while (currentClass != null && currentClass != Object.class) {
|
||||
for (Field field : currentClass.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(field.getModifiers()) ||
|
||||
Modifier.isTransient(field.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(obj);
|
||||
String fieldPath = path.isEmpty() ? field.getName() : path + "." + field.getName();
|
||||
|
||||
Object processedValue = recursiveProcess(value, seen, depth + 1, fieldPath);
|
||||
result.put(field.getName(), processedValue);
|
||||
} catch (Exception e) {
|
||||
result.put(field.getName(),
|
||||
KryoPlaceholder.create(null, "Field access error: " + e.getMessage(),
|
||||
path + "." + field.getName()));
|
||||
}
|
||||
}
|
||||
currentClass = currentClass.getSuperclass();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to create an instance of a class.
|
||||
*/
|
||||
private static Object createInstance(Class<?> clazz) {
|
||||
try {
|
||||
return clazz.getDeclaredConstructor().newInstance();
|
||||
} catch (Exception e) {
|
||||
// Try Objenesis via Kryo's instantiator
|
||||
try {
|
||||
Kryo kryo = KRYO.get();
|
||||
return kryo.newInstance(clazz);
|
||||
} catch (Exception e2) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a type to the known unserializable types cache.
|
||||
*/
|
||||
public static void registerUnserializableType(Class<?> clazz) {
|
||||
UNSERIALIZABLE_TYPES.add(clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the unserializable types cache to default state.
|
||||
* Clears any dynamically discovered types but keeps the built-in defaults.
|
||||
*/
|
||||
public static void clearUnserializableTypesCache() {
|
||||
UNSERIALIZABLE_TYPES.clear();
|
||||
// Re-add default unserializable types
|
||||
UNSERIALIZABLE_TYPES.add(Socket.class);
|
||||
UNSERIALIZABLE_TYPES.add(ServerSocket.class);
|
||||
UNSERIALIZABLE_TYPES.add(InputStream.class);
|
||||
UNSERIALIZABLE_TYPES.add(OutputStream.class);
|
||||
UNSERIALIZABLE_TYPES.add(Connection.class);
|
||||
UNSERIALIZABLE_TYPES.add(Statement.class);
|
||||
UNSERIALIZABLE_TYPES.add(ResultSet.class);
|
||||
UNSERIALIZABLE_TYPES.add(Thread.class);
|
||||
UNSERIALIZABLE_TYPES.add(ThreadGroup.class);
|
||||
UNSERIALIZABLE_TYPES.add(ClassLoader.class);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,430 +0,0 @@
|
|||
package com.codeflash;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Deep object comparison for verifying serialization/deserialization correctness.
|
||||
*
|
||||
* This comparator is used to verify that objects survive the serialize-deserialize
|
||||
* cycle correctly. It handles:
|
||||
* - Primitives and wrappers with epsilon tolerance for floats
|
||||
* - Collections, Maps, and Arrays
|
||||
* - Custom objects via reflection
|
||||
* - NaN and Infinity special cases
|
||||
* - Exception comparison
|
||||
* - KryoPlaceholder rejection
|
||||
*/
|
||||
public final class ObjectComparator {
|
||||
|
||||
private static final double EPSILON = 1e-9;
|
||||
|
||||
private ObjectComparator() {
|
||||
// Utility class
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two objects for deep equality.
|
||||
*
|
||||
* @param orig The original object
|
||||
* @param newObj The object to compare against
|
||||
* @return true if objects are equivalent
|
||||
* @throws KryoPlaceholderAccessException if comparison involves a placeholder
|
||||
*/
|
||||
public static boolean compare(Object orig, Object newObj) {
|
||||
return compareInternal(orig, newObj, new IdentityHashMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two objects, returning a detailed result.
|
||||
*
|
||||
* @param orig The original object
|
||||
* @param newObj The object to compare against
|
||||
* @return ComparisonResult with details about the comparison
|
||||
*/
|
||||
public static ComparisonResult compareWithDetails(Object orig, Object newObj) {
|
||||
try {
|
||||
boolean equal = compareInternal(orig, newObj, new IdentityHashMap<>());
|
||||
return new ComparisonResult(equal, null);
|
||||
} catch (KryoPlaceholderAccessException e) {
|
||||
return new ComparisonResult(false, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean compareInternal(Object orig, Object newObj,
|
||||
IdentityHashMap<Object, Object> seen) {
|
||||
// Handle nulls
|
||||
if (orig == null && newObj == null) {
|
||||
return true;
|
||||
}
|
||||
if (orig == null || newObj == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Detect and reject KryoPlaceholder
|
||||
if (orig instanceof KryoPlaceholder) {
|
||||
KryoPlaceholder p = (KryoPlaceholder) orig;
|
||||
throw new KryoPlaceholderAccessException(
|
||||
"Cannot compare: original contains placeholder for unserializable object",
|
||||
p.getObjType(), p.getPath());
|
||||
}
|
||||
if (newObj instanceof KryoPlaceholder) {
|
||||
KryoPlaceholder p = (KryoPlaceholder) newObj;
|
||||
throw new KryoPlaceholderAccessException(
|
||||
"Cannot compare: new object contains placeholder for unserializable object",
|
||||
p.getObjType(), p.getPath());
|
||||
}
|
||||
|
||||
// Handle exceptions specially
|
||||
if (orig instanceof Throwable && newObj instanceof Throwable) {
|
||||
return compareExceptions((Throwable) orig, (Throwable) newObj);
|
||||
}
|
||||
|
||||
Class<?> origClass = orig.getClass();
|
||||
Class<?> newClass = newObj.getClass();
|
||||
|
||||
// Check type compatibility
|
||||
if (!origClass.equals(newClass)) {
|
||||
if (!areTypesCompatible(origClass, newClass)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle primitives and wrappers
|
||||
if (orig instanceof Boolean) {
|
||||
return orig.equals(newObj);
|
||||
}
|
||||
if (orig instanceof Character) {
|
||||
return orig.equals(newObj);
|
||||
}
|
||||
if (orig instanceof String) {
|
||||
return orig.equals(newObj);
|
||||
}
|
||||
if (orig instanceof Number) {
|
||||
return compareNumbers((Number) orig, (Number) newObj);
|
||||
}
|
||||
|
||||
// Handle enums
|
||||
if (origClass.isEnum()) {
|
||||
return orig.equals(newObj);
|
||||
}
|
||||
|
||||
// Handle Class objects
|
||||
if (orig instanceof Class) {
|
||||
return orig.equals(newObj);
|
||||
}
|
||||
|
||||
// Handle date/time types
|
||||
if (orig instanceof Date || orig instanceof LocalDateTime ||
|
||||
orig instanceof LocalDate || orig instanceof LocalTime) {
|
||||
return orig.equals(newObj);
|
||||
}
|
||||
|
||||
// Handle Optional
|
||||
if (orig instanceof Optional && newObj instanceof Optional) {
|
||||
return compareOptionals((Optional<?>) orig, (Optional<?>) newObj, seen);
|
||||
}
|
||||
|
||||
// Check for circular reference to prevent infinite recursion
|
||||
if (seen.containsKey(orig)) {
|
||||
// If we've seen this object before, just check identity
|
||||
return seen.get(orig) == newObj;
|
||||
}
|
||||
seen.put(orig, newObj);
|
||||
|
||||
try {
|
||||
// Handle arrays
|
||||
if (origClass.isArray()) {
|
||||
return compareArrays(orig, newObj, seen);
|
||||
}
|
||||
|
||||
// Handle collections
|
||||
if (orig instanceof Collection && newObj instanceof Collection) {
|
||||
return compareCollections((Collection<?>) orig, (Collection<?>) newObj, seen);
|
||||
}
|
||||
|
||||
// Handle maps
|
||||
if (orig instanceof Map && newObj instanceof Map) {
|
||||
return compareMaps((Map<?, ?>) orig, (Map<?, ?>) newObj, seen);
|
||||
}
|
||||
|
||||
// Handle general objects via reflection
|
||||
return compareObjects(orig, newObj, seen);
|
||||
|
||||
} finally {
|
||||
seen.remove(orig);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two types are compatible for comparison.
|
||||
*/
|
||||
private static boolean areTypesCompatible(Class<?> type1, Class<?> type2) {
|
||||
// Allow comparing different Collection implementations
|
||||
if (Collection.class.isAssignableFrom(type1) && Collection.class.isAssignableFrom(type2)) {
|
||||
return true;
|
||||
}
|
||||
// Allow comparing different Map implementations
|
||||
if (Map.class.isAssignableFrom(type1) && Map.class.isAssignableFrom(type2)) {
|
||||
return true;
|
||||
}
|
||||
// Allow comparing different Number types
|
||||
if (Number.class.isAssignableFrom(type1) && Number.class.isAssignableFrom(type2)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two numbers with epsilon tolerance for floating point.
|
||||
*/
|
||||
private static boolean compareNumbers(Number n1, Number n2) {
|
||||
// Handle floating point with epsilon
|
||||
if (n1 instanceof Double || n1 instanceof Float ||
|
||||
n2 instanceof Double || n2 instanceof Float) {
|
||||
|
||||
double d1 = n1.doubleValue();
|
||||
double d2 = n2.doubleValue();
|
||||
|
||||
// Handle NaN
|
||||
if (Double.isNaN(d1) && Double.isNaN(d2)) {
|
||||
return true;
|
||||
}
|
||||
if (Double.isNaN(d1) || Double.isNaN(d2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle Infinity
|
||||
if (Double.isInfinite(d1) && Double.isInfinite(d2)) {
|
||||
return (d1 > 0) == (d2 > 0); // Same sign
|
||||
}
|
||||
if (Double.isInfinite(d1) || Double.isInfinite(d2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare with epsilon
|
||||
return Math.abs(d1 - d2) < EPSILON;
|
||||
}
|
||||
|
||||
// Integer types - exact comparison
|
||||
return n1.longValue() == n2.longValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two exceptions.
|
||||
*/
|
||||
private static boolean compareExceptions(Throwable orig, Throwable newEx) {
|
||||
// Must be same type
|
||||
if (!orig.getClass().equals(newEx.getClass())) {
|
||||
return false;
|
||||
}
|
||||
// Compare message (both may be null)
|
||||
return Objects.equals(orig.getMessage(), newEx.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two Optional values.
|
||||
*/
|
||||
private static boolean compareOptionals(Optional<?> orig, Optional<?> newOpt,
|
||||
IdentityHashMap<Object, Object> seen) {
|
||||
if (orig.isPresent() != newOpt.isPresent()) {
|
||||
return false;
|
||||
}
|
||||
if (!orig.isPresent()) {
|
||||
return true; // Both empty
|
||||
}
|
||||
return compareInternal(orig.get(), newOpt.get(), seen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two arrays.
|
||||
*/
|
||||
private static boolean compareArrays(Object orig, Object newObj,
|
||||
IdentityHashMap<Object, Object> seen) {
|
||||
int length1 = Array.getLength(orig);
|
||||
int length2 = Array.getLength(newObj);
|
||||
|
||||
if (length1 != length2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < length1; i++) {
|
||||
Object elem1 = Array.get(orig, i);
|
||||
Object elem2 = Array.get(newObj, i);
|
||||
if (!compareInternal(elem1, elem2, seen)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two collections.
|
||||
*/
|
||||
private static boolean compareCollections(Collection<?> orig, Collection<?> newColl,
|
||||
IdentityHashMap<Object, Object> seen) {
|
||||
if (orig.size() != newColl.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For Sets, compare element-by-element (order doesn't matter)
|
||||
if (orig instanceof Set && newColl instanceof Set) {
|
||||
return compareSets((Set<?>) orig, (Set<?>) newColl, seen);
|
||||
}
|
||||
|
||||
// For ordered collections (List, etc.), compare in order
|
||||
Iterator<?> iter1 = orig.iterator();
|
||||
Iterator<?> iter2 = newColl.iterator();
|
||||
|
||||
while (iter1.hasNext() && iter2.hasNext()) {
|
||||
if (!compareInternal(iter1.next(), iter2.next(), seen)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return !iter1.hasNext() && !iter2.hasNext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two sets (order-independent).
|
||||
*/
|
||||
private static boolean compareSets(Set<?> orig, Set<?> newSet,
|
||||
IdentityHashMap<Object, Object> seen) {
|
||||
if (orig.size() != newSet.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For each element in orig, find a matching element in newSet
|
||||
for (Object elem1 : orig) {
|
||||
boolean found = false;
|
||||
for (Object elem2 : newSet) {
|
||||
try {
|
||||
if (compareInternal(elem1, elem2, new IdentityHashMap<>(seen))) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
} catch (KryoPlaceholderAccessException e) {
|
||||
// Propagate placeholder exceptions
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two maps.
|
||||
*/
|
||||
private static boolean compareMaps(Map<?, ?> orig, Map<?, ?> newMap,
|
||||
IdentityHashMap<Object, Object> seen) {
|
||||
if (orig.size() != newMap.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Map.Entry<?, ?> entry : orig.entrySet()) {
|
||||
Object key = entry.getKey();
|
||||
Object value1 = entry.getValue();
|
||||
|
||||
if (!newMap.containsKey(key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Object value2 = newMap.get(key);
|
||||
if (!compareInternal(value1, value2, seen)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two objects via reflection.
|
||||
*/
|
||||
private static boolean compareObjects(Object orig, Object newObj,
|
||||
IdentityHashMap<Object, Object> seen) {
|
||||
Class<?> clazz = orig.getClass();
|
||||
|
||||
// If class has a custom equals method, use it
|
||||
try {
|
||||
if (hasCustomEquals(clazz)) {
|
||||
return orig.equals(newObj);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Fall through to field comparison
|
||||
}
|
||||
|
||||
// Compare all fields via reflection
|
||||
Class<?> currentClass = clazz;
|
||||
while (currentClass != null && currentClass != Object.class) {
|
||||
for (Field field : currentClass.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(field.getModifiers()) ||
|
||||
Modifier.isTransient(field.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
Object value1 = field.get(orig);
|
||||
Object value2 = field.get(newObj);
|
||||
|
||||
if (!compareInternal(value1, value2, seen)) {
|
||||
return false;
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
// Can't access field - assume not equal
|
||||
return false;
|
||||
}
|
||||
}
|
||||
currentClass = currentClass.getSuperclass();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a class has a custom equals method (not from Object).
|
||||
*/
|
||||
private static boolean hasCustomEquals(Class<?> clazz) {
|
||||
try {
|
||||
java.lang.reflect.Method equalsMethod = clazz.getMethod("equals", Object.class);
|
||||
return equalsMethod.getDeclaringClass() != Object.class;
|
||||
} catch (NoSuchMethodException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a comparison with optional error details.
|
||||
*/
|
||||
public static class ComparisonResult {
|
||||
private final boolean equal;
|
||||
private final String errorMessage;
|
||||
|
||||
public ComparisonResult(boolean equal, String errorMessage) {
|
||||
this.equal = equal;
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public boolean isEqual() {
|
||||
return equal;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public boolean hasError() {
|
||||
return errorMessage != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
* impact on benchmark measurements.
|
||||
*
|
||||
* Database schema:
|
||||
* - invocations: call_id, method_id, args_json, result_json, error_json, start_time, end_time
|
||||
* - invocations: call_id, method_id, args_blob, result_blob, error_blob, start_time, end_time
|
||||
* - benchmarks: method_id, duration_ns, timestamp
|
||||
* - benchmark_results: method_id, mean_ns, stddev_ns, min_ns, max_ns, p50_ns, p90_ns, p99_ns, iterations
|
||||
*/
|
||||
|
|
@ -65,14 +65,14 @@ public final class ResultWriter {
|
|||
|
||||
private void initializeSchema() throws SQLException {
|
||||
try (Statement stmt = connection.createStatement()) {
|
||||
// Invocations table - stores input/output/error for each function call
|
||||
// Invocations table - stores input/output/error for each function call as BLOBs
|
||||
stmt.execute(
|
||||
"CREATE TABLE IF NOT EXISTS invocations (" +
|
||||
"call_id INTEGER PRIMARY KEY, " +
|
||||
"method_id TEXT NOT NULL, " +
|
||||
"args_json TEXT, " +
|
||||
"result_json TEXT, " +
|
||||
"error_json TEXT, " +
|
||||
"args_blob BLOB, " +
|
||||
"result_blob BLOB, " +
|
||||
"error_blob BLOB, " +
|
||||
"start_time INTEGER, " +
|
||||
"end_time INTEGER)"
|
||||
);
|
||||
|
|
@ -109,13 +109,13 @@ public final class ResultWriter {
|
|||
|
||||
private void prepareStatements() throws SQLException {
|
||||
insertInvocationInput = connection.prepareStatement(
|
||||
"INSERT INTO invocations (call_id, method_id, args_json, start_time) VALUES (?, ?, ?, ?)"
|
||||
"INSERT INTO invocations (call_id, method_id, args_blob, start_time) VALUES (?, ?, ?, ?)"
|
||||
);
|
||||
updateInvocationOutput = connection.prepareStatement(
|
||||
"UPDATE invocations SET result_json = ?, end_time = ? WHERE call_id = ?"
|
||||
"UPDATE invocations SET result_blob = ?, end_time = ? WHERE call_id = ?"
|
||||
);
|
||||
updateInvocationError = connection.prepareStatement(
|
||||
"UPDATE invocations SET error_json = ?, end_time = ? WHERE call_id = ?"
|
||||
"UPDATE invocations SET error_blob = ?, end_time = ? WHERE call_id = ?"
|
||||
);
|
||||
insertBenchmark = connection.prepareStatement(
|
||||
"INSERT INTO benchmarks (method_id, duration_ns, timestamp) VALUES (?, ?, ?)"
|
||||
|
|
@ -130,22 +130,22 @@ public final class ResultWriter {
|
|||
/**
|
||||
* Record function input (beginning of invocation).
|
||||
*/
|
||||
public void recordInput(long callId, String methodId, String argsJson, long startTime) {
|
||||
writeQueue.offer(new WriteTask(WriteType.INPUT, callId, methodId, argsJson, null, null, startTime, 0, null));
|
||||
public void recordInput(long callId, String methodId, byte[] argsBlob, long startTime) {
|
||||
writeQueue.offer(new WriteTask(WriteType.INPUT, callId, methodId, argsBlob, null, null, startTime, 0, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Record function output (successful completion).
|
||||
*/
|
||||
public void recordOutput(long callId, String methodId, String resultJson, long endTime) {
|
||||
writeQueue.offer(new WriteTask(WriteType.OUTPUT, callId, methodId, null, resultJson, null, 0, endTime, null));
|
||||
public void recordOutput(long callId, String methodId, byte[] resultBlob, long endTime) {
|
||||
writeQueue.offer(new WriteTask(WriteType.OUTPUT, callId, methodId, null, resultBlob, null, 0, endTime, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Record function error (exception thrown).
|
||||
*/
|
||||
public void recordError(long callId, String methodId, String errorJson, long endTime) {
|
||||
writeQueue.offer(new WriteTask(WriteType.ERROR, callId, methodId, null, null, errorJson, 0, endTime, null));
|
||||
public void recordError(long callId, String methodId, byte[] errorBlob, long endTime) {
|
||||
writeQueue.offer(new WriteTask(WriteType.ERROR, callId, methodId, null, null, errorBlob, 0, endTime, null));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -196,20 +196,20 @@ public final class ResultWriter {
|
|||
case INPUT:
|
||||
insertInvocationInput.setLong(1, task.callId);
|
||||
insertInvocationInput.setString(2, task.methodId);
|
||||
insertInvocationInput.setString(3, task.argsJson);
|
||||
insertInvocationInput.setBytes(3, task.argsBlob);
|
||||
insertInvocationInput.setLong(4, task.startTime);
|
||||
insertInvocationInput.executeUpdate();
|
||||
break;
|
||||
|
||||
case OUTPUT:
|
||||
updateInvocationOutput.setString(1, task.resultJson);
|
||||
updateInvocationOutput.setBytes(1, task.resultBlob);
|
||||
updateInvocationOutput.setLong(2, task.endTime);
|
||||
updateInvocationOutput.setLong(3, task.callId);
|
||||
updateInvocationOutput.executeUpdate();
|
||||
break;
|
||||
|
||||
case ERROR:
|
||||
updateInvocationError.setString(1, task.errorJson);
|
||||
updateInvocationError.setBytes(1, task.errorBlob);
|
||||
updateInvocationError.setLong(2, task.endTime);
|
||||
updateInvocationError.setLong(3, task.callId);
|
||||
updateInvocationError.executeUpdate();
|
||||
|
|
@ -294,22 +294,22 @@ public final class ResultWriter {
|
|||
final WriteType type;
|
||||
final long callId;
|
||||
final String methodId;
|
||||
final String argsJson;
|
||||
final String resultJson;
|
||||
final String errorJson;
|
||||
final byte[] argsBlob;
|
||||
final byte[] resultBlob;
|
||||
final byte[] errorBlob;
|
||||
final long startTime;
|
||||
final long endTime;
|
||||
final BenchmarkResult benchmarkResult;
|
||||
|
||||
WriteTask(WriteType type, long callId, String methodId, String argsJson,
|
||||
String resultJson, String errorJson, long startTime, long endTime,
|
||||
WriteTask(WriteType type, long callId, String methodId, byte[] argsBlob,
|
||||
byte[] resultBlob, byte[] errorBlob, long startTime, long endTime,
|
||||
BenchmarkResult benchmarkResult) {
|
||||
this.type = type;
|
||||
this.callId = callId;
|
||||
this.methodId = methodId;
|
||||
this.argsJson = argsJson;
|
||||
this.resultJson = resultJson;
|
||||
this.errorJson = errorJson;
|
||||
this.argsBlob = argsBlob;
|
||||
this.resultBlob = resultBlob;
|
||||
this.errorBlob = errorBlob;
|
||||
this.startTime = startTime;
|
||||
this.endTime = endTime;
|
||||
this.benchmarkResult = benchmarkResult;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,842 @@
|
|||
package com.codeflash;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Edge case tests for Comparator to catch subtle bugs.
|
||||
*/
|
||||
@DisplayName("Comparator Edge Case Tests")
|
||||
class ComparatorEdgeCaseTest {
|
||||
|
||||
// ============================================================
|
||||
// NUMBER EDGE CASES
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Number Edge Cases")
|
||||
class NumberEdgeCases {
|
||||
|
||||
@Test
|
||||
@DisplayName("BigDecimal comparison should work correctly")
|
||||
void testBigDecimalComparison() {
|
||||
BigDecimal bd1 = new BigDecimal("123456789.123456789");
|
||||
BigDecimal bd2 = new BigDecimal("123456789.123456789");
|
||||
BigDecimal bd3 = new BigDecimal("123456789.123456788");
|
||||
|
||||
assertTrue(Comparator.compare(bd1, bd2), "Same BigDecimals should be equal");
|
||||
assertFalse(Comparator.compare(bd1, bd3), "Different BigDecimals should not be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("BigDecimal with different scale should compare by value")
|
||||
void testBigDecimalDifferentScale() {
|
||||
BigDecimal bd1 = new BigDecimal("1.0");
|
||||
BigDecimal bd2 = new BigDecimal("1.00");
|
||||
|
||||
// Note: BigDecimal.equals considers scale, but compareTo doesn't
|
||||
// Our comparator should handle this
|
||||
assertTrue(Comparator.compare(bd1, bd2), "1.0 and 1.00 should be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("BigInteger comparison should work correctly")
|
||||
void testBigIntegerComparison() {
|
||||
BigInteger bi1 = new BigInteger("123456789012345678901234567890");
|
||||
BigInteger bi2 = new BigInteger("123456789012345678901234567890");
|
||||
BigInteger bi3 = new BigInteger("123456789012345678901234567891");
|
||||
|
||||
assertTrue(Comparator.compare(bi1, bi2), "Same BigIntegers should be equal");
|
||||
assertFalse(Comparator.compare(bi1, bi3), "Different BigIntegers should not be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("BigInteger larger than Long.MAX_VALUE")
|
||||
void testBigIntegerLargerThanLong() {
|
||||
BigInteger bi1 = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE);
|
||||
BigInteger bi2 = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE);
|
||||
BigInteger bi3 = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.TWO);
|
||||
|
||||
assertTrue(Comparator.compare(bi1, bi2), "Same large BigIntegers should be equal");
|
||||
assertFalse(Comparator.compare(bi1, bi3), "Different large BigIntegers should not be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Byte comparison")
|
||||
void testByteComparison() {
|
||||
Byte b1 = (byte) 127;
|
||||
Byte b2 = (byte) 127;
|
||||
Byte b3 = (byte) -128;
|
||||
|
||||
assertTrue(Comparator.compare(b1, b2));
|
||||
assertFalse(Comparator.compare(b1, b3));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Short comparison")
|
||||
void testShortComparison() {
|
||||
Short s1 = (short) 32767;
|
||||
Short s2 = (short) 32767;
|
||||
Short s3 = (short) -32768;
|
||||
|
||||
assertTrue(Comparator.compare(s1, s2));
|
||||
assertFalse(Comparator.compare(s1, s3));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Large double comparison with relative tolerance")
|
||||
void testLargeDoubleComparison() {
|
||||
// For large numbers, absolute epsilon may be too small
|
||||
double large1 = 1e15;
|
||||
double large2 = 1e15 + 1; // Difference of 1 in 1e15
|
||||
|
||||
// With relative tolerance, these should be equal (difference is 1e-15 relative)
|
||||
assertTrue(Comparator.compare(large1, large2),
|
||||
"Large numbers with tiny relative difference should be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Large doubles that are actually different")
|
||||
void testLargeDoublesActuallyDifferent() {
|
||||
double large1 = 1e15;
|
||||
double large2 = 1.001e15; // 0.1% difference
|
||||
|
||||
assertFalse(Comparator.compare(large1, large2),
|
||||
"Large numbers with significant relative difference should NOT be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Float vs Double comparison")
|
||||
void testFloatVsDouble() {
|
||||
Float f = 3.14f;
|
||||
Double d = 3.14;
|
||||
|
||||
// These may differ slightly due to precision
|
||||
// Testing current behavior
|
||||
boolean result = Comparator.compare(f, d);
|
||||
// Document: Float 3.14f != Double 3.14 due to precision differences
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Integer overflow edge case")
|
||||
void testIntegerOverflow() {
|
||||
Integer maxInt = Integer.MAX_VALUE;
|
||||
Long maxIntAsLong = (long) Integer.MAX_VALUE;
|
||||
|
||||
assertTrue(Comparator.compare(maxInt, maxIntAsLong),
|
||||
"Integer.MAX_VALUE should equal same value as Long");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Long overflow to BigInteger")
|
||||
void testLongOverflowToBigInteger() {
|
||||
Long maxLong = Long.MAX_VALUE;
|
||||
BigInteger maxLongAsBigInt = BigInteger.valueOf(Long.MAX_VALUE);
|
||||
|
||||
assertTrue(Comparator.compare(maxLong, maxLongAsBigInt),
|
||||
"Long.MAX_VALUE should equal same value as BigInteger");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Very small double comparison")
|
||||
void testVerySmallDoubleComparison() {
|
||||
double small1 = 1e-15;
|
||||
double small2 = 1e-15 + 1e-25;
|
||||
|
||||
assertTrue(Comparator.compare(small1, small2),
|
||||
"Very close small numbers should be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Negative zero equals positive zero")
|
||||
void testNegativeZero() {
|
||||
double negZero = -0.0;
|
||||
double posZero = 0.0;
|
||||
|
||||
assertTrue(Comparator.compare(negZero, posZero),
|
||||
"-0.0 should equal 0.0");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Mixed integer types comparison")
|
||||
void testMixedIntegerTypes() {
|
||||
Integer i = 42;
|
||||
Long l = 42L;
|
||||
|
||||
assertTrue(Comparator.compare(i, l), "Integer 42 should equal Long 42");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ARRAY EDGE CASES
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Array Edge Cases")
|
||||
class ArrayEdgeCases {
|
||||
|
||||
@Test
|
||||
@DisplayName("Empty arrays of same type")
|
||||
void testEmptyArrays() {
|
||||
int[] arr1 = new int[0];
|
||||
int[] arr2 = new int[0];
|
||||
|
||||
assertTrue(Comparator.compare(arr1, arr2));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Empty arrays of different types")
|
||||
void testEmptyArraysDifferentTypes() {
|
||||
int[] intArr = new int[0];
|
||||
long[] longArr = new long[0];
|
||||
|
||||
// Different array types should not be equal even if empty
|
||||
assertFalse(Comparator.compare(intArr, longArr));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Primitive array vs wrapper array")
|
||||
void testPrimitiveVsWrapperArray() {
|
||||
int[] primitiveArr = {1, 2, 3};
|
||||
Integer[] wrapperArr = {1, 2, 3};
|
||||
|
||||
// These are different types
|
||||
assertFalse(Comparator.compare(primitiveArr, wrapperArr));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Nested arrays")
|
||||
void testNestedArrays() {
|
||||
int[][] arr1 = {{1, 2}, {3, 4}};
|
||||
int[][] arr2 = {{1, 2}, {3, 4}};
|
||||
int[][] arr3 = {{1, 2}, {3, 5}};
|
||||
|
||||
assertTrue(Comparator.compare(arr1, arr2));
|
||||
assertFalse(Comparator.compare(arr1, arr3));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Array with null elements")
|
||||
void testArrayWithNulls() {
|
||||
String[] arr1 = {"a", null, "c"};
|
||||
String[] arr2 = {"a", null, "c"};
|
||||
String[] arr3 = {"a", "b", "c"};
|
||||
|
||||
assertTrue(Comparator.compare(arr1, arr2));
|
||||
assertFalse(Comparator.compare(arr1, arr3));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LIST VS SET ORDER BEHAVIOR
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("List vs Set Order Behavior")
|
||||
class ListVsSetOrderBehavior {
|
||||
|
||||
@Test
|
||||
@DisplayName("List comparison is ORDER SENSITIVE - [1,2,3] vs [2,3,1] should be FALSE")
|
||||
void testListOrderMatters() {
|
||||
List<Integer> list1 = Arrays.asList(1, 2, 3);
|
||||
List<Integer> list2 = Arrays.asList(2, 3, 1);
|
||||
|
||||
assertFalse(Comparator.compare(list1, list2),
|
||||
"Lists with same elements but different order should NOT be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("List comparison with same order should be TRUE")
|
||||
void testListSameOrder() {
|
||||
List<Integer> list1 = Arrays.asList(1, 2, 3);
|
||||
List<Integer> list2 = Arrays.asList(1, 2, 3);
|
||||
|
||||
assertTrue(Comparator.compare(list1, list2),
|
||||
"Lists with same elements in same order should be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Set comparison is ORDER INDEPENDENT - {1,2,3} vs {3,2,1} should be TRUE")
|
||||
void testSetOrderDoesNotMatter() {
|
||||
Set<Integer> set1 = new LinkedHashSet<>(Arrays.asList(1, 2, 3));
|
||||
Set<Integer> set2 = new LinkedHashSet<>(Arrays.asList(3, 2, 1));
|
||||
|
||||
assertTrue(Comparator.compare(set1, set2),
|
||||
"Sets with same elements in different order should be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Set comparison with different elements should be FALSE")
|
||||
void testSetDifferentElements() {
|
||||
Set<Integer> set1 = new HashSet<>(Arrays.asList(1, 2, 3));
|
||||
Set<Integer> set2 = new HashSet<>(Arrays.asList(1, 2, 4));
|
||||
|
||||
assertFalse(Comparator.compare(set1, set2),
|
||||
"Sets with different elements should NOT be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ArrayList vs LinkedList with same elements same order should be TRUE")
|
||||
void testDifferentListImplementationsSameOrder() {
|
||||
List<Integer> arrayList = new ArrayList<>(Arrays.asList(1, 2, 3));
|
||||
List<Integer> linkedList = new LinkedList<>(Arrays.asList(1, 2, 3));
|
||||
|
||||
assertTrue(Comparator.compare(arrayList, linkedList),
|
||||
"Different List implementations with same elements in same order should be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ArrayList vs LinkedList with different order should be FALSE")
|
||||
void testDifferentListImplementationsDifferentOrder() {
|
||||
List<Integer> arrayList = new ArrayList<>(Arrays.asList(1, 2, 3));
|
||||
List<Integer> linkedList = new LinkedList<>(Arrays.asList(3, 2, 1));
|
||||
|
||||
assertFalse(Comparator.compare(arrayList, linkedList),
|
||||
"Different List implementations with different order should NOT be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HashSet vs TreeSet with same elements should be TRUE")
|
||||
void testDifferentSetImplementations() {
|
||||
Set<Integer> hashSet = new HashSet<>(Arrays.asList(3, 1, 2));
|
||||
Set<Integer> treeSet = new TreeSet<>(Arrays.asList(1, 2, 3));
|
||||
|
||||
assertTrue(Comparator.compare(hashSet, treeSet),
|
||||
"Different Set implementations with same elements should be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("List with nested lists - order matters at all levels")
|
||||
void testNestedListOrder() {
|
||||
List<List<Integer>> list1 = Arrays.asList(
|
||||
Arrays.asList(1, 2),
|
||||
Arrays.asList(3, 4)
|
||||
);
|
||||
List<List<Integer>> list2 = Arrays.asList(
|
||||
Arrays.asList(3, 4),
|
||||
Arrays.asList(1, 2)
|
||||
);
|
||||
List<List<Integer>> list3 = Arrays.asList(
|
||||
Arrays.asList(1, 2),
|
||||
Arrays.asList(3, 4)
|
||||
);
|
||||
|
||||
assertFalse(Comparator.compare(list1, list2),
|
||||
"Nested lists with different outer order should NOT be equal");
|
||||
assertTrue(Comparator.compare(list1, list3),
|
||||
"Nested lists with same order should be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Set with nested sets - order independent")
|
||||
void testNestedSetOrder() {
|
||||
Set<Set<Integer>> set1 = new HashSet<>();
|
||||
set1.add(new HashSet<>(Arrays.asList(1, 2)));
|
||||
set1.add(new HashSet<>(Arrays.asList(3, 4)));
|
||||
|
||||
Set<Set<Integer>> set2 = new HashSet<>();
|
||||
set2.add(new HashSet<>(Arrays.asList(4, 3))); // Different internal order
|
||||
set2.add(new HashSet<>(Arrays.asList(2, 1))); // Different internal order
|
||||
|
||||
assertTrue(Comparator.compare(set1, set2),
|
||||
"Nested sets should be equal regardless of order at any level");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// COLLECTION EDGE CASES
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Collection Edge Cases")
|
||||
class CollectionEdgeCases {
|
||||
|
||||
@Test
|
||||
@DisplayName("Set with custom objects without equals")
|
||||
void testSetWithCustomObjectsNoEquals() {
|
||||
Set<CustomNoEquals> set1 = new HashSet<>();
|
||||
set1.add(new CustomNoEquals("a"));
|
||||
|
||||
Set<CustomNoEquals> set2 = new HashSet<>();
|
||||
set2.add(new CustomNoEquals("a"));
|
||||
|
||||
// Should use deep comparison, not equals()
|
||||
assertTrue(Comparator.compare(set1, set2),
|
||||
"Sets with equivalent custom objects should be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Empty Set equals empty Set")
|
||||
void testEmptySets() {
|
||||
Set<Integer> set1 = new HashSet<>();
|
||||
Set<Integer> set2 = new TreeSet<>();
|
||||
|
||||
assertTrue(Comparator.compare(set1, set2));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("List vs Set with same elements")
|
||||
void testListVsSet() {
|
||||
List<Integer> list = Arrays.asList(1, 2, 3);
|
||||
Set<Integer> set = new LinkedHashSet<>(Arrays.asList(1, 2, 3));
|
||||
|
||||
// Different collection types should not be equal
|
||||
// Actually, our comparator allows this - testing current behavior
|
||||
boolean result = Comparator.compare(list, set);
|
||||
// Document: List and Set comparison depends on areTypesCompatible
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("List with duplicates vs Set")
|
||||
void testListWithDuplicatesVsSet() {
|
||||
List<Integer> list = Arrays.asList(1, 1, 2);
|
||||
Set<Integer> set = new LinkedHashSet<>(Arrays.asList(1, 2));
|
||||
|
||||
assertFalse(Comparator.compare(list, set), "Different sizes should not be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ConcurrentHashMap comparison")
|
||||
void testConcurrentHashMap() {
|
||||
ConcurrentHashMap<String, Integer> map1 = new ConcurrentHashMap<>();
|
||||
map1.put("a", 1);
|
||||
map1.put("b", 2);
|
||||
|
||||
ConcurrentHashMap<String, Integer> map2 = new ConcurrentHashMap<>();
|
||||
map2.put("a", 1);
|
||||
map2.put("b", 2);
|
||||
|
||||
assertTrue(Comparator.compare(map1, map2));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MAP EDGE CASES
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Map Edge Cases")
|
||||
class MapEdgeCases {
|
||||
|
||||
@Test
|
||||
@DisplayName("Map with null key")
|
||||
void testMapWithNullKey() {
|
||||
Map<String, Integer> map1 = new HashMap<>();
|
||||
map1.put(null, 1);
|
||||
map1.put("b", 2);
|
||||
|
||||
Map<String, Integer> map2 = new HashMap<>();
|
||||
map2.put(null, 1);
|
||||
map2.put("b", 2);
|
||||
|
||||
assertTrue(Comparator.compare(map1, map2));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Map with null value")
|
||||
void testMapWithNullValue() {
|
||||
Map<String, Integer> map1 = new HashMap<>();
|
||||
map1.put("a", null);
|
||||
map1.put("b", 2);
|
||||
|
||||
Map<String, Integer> map2 = new HashMap<>();
|
||||
map2.put("a", null);
|
||||
map2.put("b", 2);
|
||||
|
||||
assertTrue(Comparator.compare(map1, map2));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Map with complex keys")
|
||||
void testMapWithComplexKeys() {
|
||||
Map<List<Integer>, String> map1 = new HashMap<>();
|
||||
map1.put(Arrays.asList(1, 2, 3), "value1");
|
||||
|
||||
Map<List<Integer>, String> map2 = new HashMap<>();
|
||||
map2.put(Arrays.asList(1, 2, 3), "value1");
|
||||
|
||||
assertTrue(Comparator.compare(map1, map2),
|
||||
"Maps with complex keys should compare using deep key comparison");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Map comparison should not double-match entries")
|
||||
void testMapNoDoubleMatching() {
|
||||
// This tests that we don't match the same entry twice
|
||||
Map<String, Integer> map1 = new HashMap<>();
|
||||
map1.put("a", 1);
|
||||
map1.put("b", 1); // Same value as "a"
|
||||
|
||||
Map<String, Integer> map2 = new HashMap<>();
|
||||
map2.put("a", 1);
|
||||
map2.put("c", 1); // Different key but same value
|
||||
|
||||
assertFalse(Comparator.compare(map1, map2),
|
||||
"Maps with different keys should not be equal");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// OBJECT EDGE CASES
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Object Edge Cases")
|
||||
class ObjectEdgeCases {
|
||||
|
||||
@Test
|
||||
@DisplayName("Objects with inherited fields")
|
||||
void testInheritedFields() {
|
||||
Child child1 = new Child("parent", "child");
|
||||
Child child2 = new Child("parent", "child");
|
||||
Child child3 = new Child("different", "child");
|
||||
|
||||
assertTrue(Comparator.compare(child1, child2));
|
||||
assertFalse(Comparator.compare(child1, child3));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Different classes with same fields should not be equal")
|
||||
void testDifferentClassesSameFields() {
|
||||
ClassA objA = new ClassA("value");
|
||||
ClassB objB = new ClassB("value");
|
||||
|
||||
assertFalse(Comparator.compare(objA, objB),
|
||||
"Different classes should not be equal even with same field values");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Object with transient field")
|
||||
void testTransientField() {
|
||||
ObjectWithTransient obj1 = new ObjectWithTransient("name", "transientValue1");
|
||||
ObjectWithTransient obj2 = new ObjectWithTransient("name", "transientValue2");
|
||||
|
||||
// Transient fields should be skipped
|
||||
assertTrue(Comparator.compare(obj1, obj2),
|
||||
"Objects differing only in transient fields should be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Object with static field")
|
||||
void testStaticField() {
|
||||
ObjectWithStatic.staticField = "static1";
|
||||
ObjectWithStatic obj1 = new ObjectWithStatic("instance1");
|
||||
|
||||
ObjectWithStatic.staticField = "static2";
|
||||
ObjectWithStatic obj2 = new ObjectWithStatic("instance1");
|
||||
|
||||
// Static fields should be skipped
|
||||
assertTrue(Comparator.compare(obj1, obj2),
|
||||
"Static fields should not affect comparison");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Circular reference in object")
|
||||
void testCircularReferenceInObject() {
|
||||
CircularRef ref1 = new CircularRef("a");
|
||||
CircularRef ref2 = new CircularRef("b");
|
||||
ref1.other = ref2;
|
||||
ref2.other = ref1;
|
||||
|
||||
CircularRef ref3 = new CircularRef("a");
|
||||
CircularRef ref4 = new CircularRef("b");
|
||||
ref3.other = ref4;
|
||||
ref4.other = ref3;
|
||||
|
||||
assertTrue(Comparator.compare(ref1, ref3),
|
||||
"Equivalent circular structures should be equal");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SPECIAL TYPES
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Special Types")
|
||||
class SpecialTypes {
|
||||
|
||||
@Test
|
||||
@DisplayName("UUID comparison")
|
||||
void testUUIDComparison() {
|
||||
UUID uuid1 = UUID.fromString("550e8400-e29b-41d4-a716-446655440000");
|
||||
UUID uuid2 = UUID.fromString("550e8400-e29b-41d4-a716-446655440000");
|
||||
UUID uuid3 = UUID.fromString("550e8400-e29b-41d4-a716-446655440001");
|
||||
|
||||
assertTrue(Comparator.compare(uuid1, uuid2));
|
||||
assertFalse(Comparator.compare(uuid1, uuid3));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("URI comparison")
|
||||
void testURIComparison() throws Exception {
|
||||
URI uri1 = new URI("https://example.com/path");
|
||||
URI uri2 = new URI("https://example.com/path");
|
||||
URI uri3 = new URI("https://example.com/other");
|
||||
|
||||
assertTrue(Comparator.compare(uri1, uri2));
|
||||
assertFalse(Comparator.compare(uri1, uri3));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("URL comparison")
|
||||
void testURLComparison() throws Exception {
|
||||
URL url1 = new URL("https://example.com/path");
|
||||
URL url2 = new URL("https://example.com/path");
|
||||
|
||||
assertTrue(Comparator.compare(url1, url2));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Class object comparison")
|
||||
void testClassObjectComparison() {
|
||||
Class<?> class1 = String.class;
|
||||
Class<?> class2 = String.class;
|
||||
Class<?> class3 = Integer.class;
|
||||
|
||||
assertTrue(Comparator.compare(class1, class2));
|
||||
assertFalse(Comparator.compare(class1, class3));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CUSTOM OBJECT (PERSON) EDGE CASES
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Custom Object (Person) Edge Cases")
|
||||
class PersonObjectEdgeCases {
|
||||
|
||||
@Test
|
||||
@DisplayName("Person with same name, age, date should be equal")
|
||||
void testPersonSameFields() {
|
||||
Person p1 = new Person("John", 25, java.time.LocalDate.of(2000, 1, 15));
|
||||
Person p2 = new Person("John", 25, java.time.LocalDate.of(2000, 1, 15));
|
||||
|
||||
assertTrue(Comparator.compare(p1, p2),
|
||||
"Persons with same fields should be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Person with different name should NOT be equal")
|
||||
void testPersonDifferentName() {
|
||||
Person p1 = new Person("John", 25, java.time.LocalDate.of(2000, 1, 15));
|
||||
Person p2 = new Person("Jane", 25, java.time.LocalDate.of(2000, 1, 15));
|
||||
|
||||
assertFalse(Comparator.compare(p1, p2),
|
||||
"Persons with different names should NOT be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Person with different age should NOT be equal")
|
||||
void testPersonDifferentAge() {
|
||||
Person p1 = new Person("John", 25, java.time.LocalDate.of(2000, 1, 15));
|
||||
Person p2 = new Person("John", 26, java.time.LocalDate.of(2000, 1, 15));
|
||||
|
||||
assertFalse(Comparator.compare(p1, p2),
|
||||
"Persons with different ages should NOT be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Person with different date should NOT be equal")
|
||||
void testPersonDifferentDate() {
|
||||
Person p1 = new Person("John", 25, java.time.LocalDate.of(2000, 1, 15));
|
||||
Person p2 = new Person("John", 25, java.time.LocalDate.of(2000, 1, 16));
|
||||
|
||||
assertFalse(Comparator.compare(p1, p2),
|
||||
"Persons with different dates should NOT be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Person with null name vs non-null name")
|
||||
void testPersonNullVsNonNullName() {
|
||||
Person p1 = new Person(null, 25, java.time.LocalDate.of(2000, 1, 15));
|
||||
Person p2 = new Person("John", 25, java.time.LocalDate.of(2000, 1, 15));
|
||||
|
||||
assertFalse(Comparator.compare(p1, p2),
|
||||
"Person with null name vs non-null name should NOT be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Person with both null names should be equal")
|
||||
void testPersonBothNullNames() {
|
||||
Person p1 = new Person(null, 25, java.time.LocalDate.of(2000, 1, 15));
|
||||
Person p2 = new Person(null, 25, java.time.LocalDate.of(2000, 1, 15));
|
||||
|
||||
assertTrue(Comparator.compare(p1, p2),
|
||||
"Persons with both null names and same other fields should be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Person with null date vs non-null date")
|
||||
void testPersonNullVsNonNullDate() {
|
||||
Person p1 = new Person("John", 25, null);
|
||||
Person p2 = new Person("John", 25, java.time.LocalDate.of(2000, 1, 15));
|
||||
|
||||
assertFalse(Comparator.compare(p1, p2),
|
||||
"Person with null date vs non-null date should NOT be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("List of Persons with same content same order")
|
||||
void testListOfPersonsSameOrder() {
|
||||
List<Person> list1 = Arrays.asList(
|
||||
new Person("John", 25, java.time.LocalDate.of(2000, 1, 15)),
|
||||
new Person("Jane", 30, java.time.LocalDate.of(1995, 6, 20))
|
||||
);
|
||||
List<Person> list2 = Arrays.asList(
|
||||
new Person("John", 25, java.time.LocalDate.of(2000, 1, 15)),
|
||||
new Person("Jane", 30, java.time.LocalDate.of(1995, 6, 20))
|
||||
);
|
||||
|
||||
assertTrue(Comparator.compare(list1, list2),
|
||||
"Lists of Persons with same content in same order should be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("List of Persons with same content different order should NOT be equal")
|
||||
void testListOfPersonsDifferentOrder() {
|
||||
List<Person> list1 = Arrays.asList(
|
||||
new Person("John", 25, java.time.LocalDate.of(2000, 1, 15)),
|
||||
new Person("Jane", 30, java.time.LocalDate.of(1995, 6, 20))
|
||||
);
|
||||
List<Person> list2 = Arrays.asList(
|
||||
new Person("Jane", 30, java.time.LocalDate.of(1995, 6, 20)),
|
||||
new Person("John", 25, java.time.LocalDate.of(2000, 1, 15))
|
||||
);
|
||||
|
||||
assertFalse(Comparator.compare(list1, list2),
|
||||
"Lists of Persons with different order should NOT be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Map with Person values")
|
||||
void testMapWithPersonValues() {
|
||||
Map<String, Person> map1 = new HashMap<>();
|
||||
map1.put("employee1", new Person("John", 25, java.time.LocalDate.of(2000, 1, 15)));
|
||||
|
||||
Map<String, Person> map2 = new HashMap<>();
|
||||
map2.put("employee1", new Person("John", 25, java.time.LocalDate.of(2000, 1, 15)));
|
||||
|
||||
assertTrue(Comparator.compare(map1, map2),
|
||||
"Maps with same Person values should be equal");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Person with floating point age (simulated)")
|
||||
void testPersonWithFloatingPointField() {
|
||||
PersonWithDouble p1 = new PersonWithDouble("John", 25.0000000001);
|
||||
PersonWithDouble p2 = new PersonWithDouble("John", 25.0);
|
||||
|
||||
assertTrue(Comparator.compare(p1, p2),
|
||||
"Persons with nearly equal floating point ages should be equal");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HELPER CLASSES
|
||||
// ============================================================
|
||||
|
||||
static class Person {
|
||||
String name;
|
||||
int age;
|
||||
java.time.LocalDate birthDate;
|
||||
|
||||
Person(String name, int age, java.time.LocalDate birthDate) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
this.birthDate = birthDate;
|
||||
}
|
||||
// Intentionally NO equals/hashCode - uses reflection comparison
|
||||
}
|
||||
|
||||
static class PersonWithDouble {
|
||||
String name;
|
||||
double age;
|
||||
|
||||
PersonWithDouble(String name, double age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
}
|
||||
|
||||
static class CustomNoEquals {
|
||||
String value;
|
||||
|
||||
CustomNoEquals(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
// No equals/hashCode override
|
||||
}
|
||||
|
||||
static class Parent {
|
||||
String parentField;
|
||||
|
||||
Parent(String parentField) {
|
||||
this.parentField = parentField;
|
||||
}
|
||||
}
|
||||
|
||||
static class Child extends Parent {
|
||||
String childField;
|
||||
|
||||
Child(String parentField, String childField) {
|
||||
super(parentField);
|
||||
this.childField = childField;
|
||||
}
|
||||
}
|
||||
|
||||
static class ClassA {
|
||||
String field;
|
||||
|
||||
ClassA(String field) {
|
||||
this.field = field;
|
||||
}
|
||||
}
|
||||
|
||||
static class ClassB {
|
||||
String field;
|
||||
|
||||
ClassB(String field) {
|
||||
this.field = field;
|
||||
}
|
||||
}
|
||||
|
||||
static class ObjectWithTransient {
|
||||
String name;
|
||||
transient String transientField;
|
||||
|
||||
ObjectWithTransient(String name, String transientField) {
|
||||
this.name = name;
|
||||
this.transientField = transientField;
|
||||
}
|
||||
}
|
||||
|
||||
static class ObjectWithStatic {
|
||||
static String staticField;
|
||||
String instanceField;
|
||||
|
||||
ObjectWithStatic(String instanceField) {
|
||||
this.instanceField = instanceField;
|
||||
}
|
||||
}
|
||||
|
||||
static class CircularRef {
|
||||
String name;
|
||||
CircularRef other;
|
||||
|
||||
CircularRef(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,10 +9,10 @@ import java.util.*;
|
|||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Tests for ObjectComparator.
|
||||
* Tests for Comparator.
|
||||
*/
|
||||
@DisplayName("ObjectComparator Tests")
|
||||
class ObjectComparatorTest {
|
||||
@DisplayName("Comparator Tests")
|
||||
class ComparatorTest {
|
||||
|
||||
@Nested
|
||||
@DisplayName("Primitive Comparison")
|
||||
|
|
@ -21,72 +21,72 @@ class ObjectComparatorTest {
|
|||
@Test
|
||||
@DisplayName("integers: exact match")
|
||||
void testIntegers() {
|
||||
assertTrue(ObjectComparator.compare(42, 42));
|
||||
assertFalse(ObjectComparator.compare(42, 43));
|
||||
assertTrue(Comparator.compare(42, 42));
|
||||
assertFalse(Comparator.compare(42, 43));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("longs: exact match")
|
||||
void testLongs() {
|
||||
assertTrue(ObjectComparator.compare(Long.MAX_VALUE, Long.MAX_VALUE));
|
||||
assertFalse(ObjectComparator.compare(1L, 2L));
|
||||
assertTrue(Comparator.compare(Long.MAX_VALUE, Long.MAX_VALUE));
|
||||
assertFalse(Comparator.compare(1L, 2L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("doubles: epsilon tolerance")
|
||||
void testDoubleEpsilon() {
|
||||
// Within epsilon - should be equal
|
||||
assertTrue(ObjectComparator.compare(1.0, 1.0 + 1e-10));
|
||||
assertTrue(ObjectComparator.compare(3.14159, 3.14159 + 1e-12));
|
||||
assertTrue(Comparator.compare(1.0, 1.0 + 1e-10));
|
||||
assertTrue(Comparator.compare(3.14159, 3.14159 + 1e-12));
|
||||
|
||||
// Outside epsilon - should not be equal
|
||||
assertFalse(ObjectComparator.compare(1.0, 1.1));
|
||||
assertFalse(ObjectComparator.compare(1.0, 1.0 + 1e-8));
|
||||
assertFalse(Comparator.compare(1.0, 1.1));
|
||||
assertFalse(Comparator.compare(1.0, 1.0 + 1e-8));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("floats: epsilon tolerance")
|
||||
void testFloatEpsilon() {
|
||||
assertTrue(ObjectComparator.compare(1.0f, 1.0f + 1e-10f));
|
||||
assertFalse(ObjectComparator.compare(1.0f, 1.1f));
|
||||
assertTrue(Comparator.compare(1.0f, 1.0f + 1e-10f));
|
||||
assertFalse(Comparator.compare(1.0f, 1.1f));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("NaN: should equal NaN")
|
||||
void testNaN() {
|
||||
assertTrue(ObjectComparator.compare(Double.NaN, Double.NaN));
|
||||
assertTrue(ObjectComparator.compare(Float.NaN, Float.NaN));
|
||||
assertTrue(Comparator.compare(Double.NaN, Double.NaN));
|
||||
assertTrue(Comparator.compare(Float.NaN, Float.NaN));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Infinity: same sign should be equal")
|
||||
void testInfinity() {
|
||||
assertTrue(ObjectComparator.compare(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY));
|
||||
assertTrue(ObjectComparator.compare(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY));
|
||||
assertFalse(ObjectComparator.compare(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY));
|
||||
assertTrue(Comparator.compare(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY));
|
||||
assertTrue(Comparator.compare(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY));
|
||||
assertFalse(Comparator.compare(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("booleans: exact match")
|
||||
void testBooleans() {
|
||||
assertTrue(ObjectComparator.compare(true, true));
|
||||
assertTrue(ObjectComparator.compare(false, false));
|
||||
assertFalse(ObjectComparator.compare(true, false));
|
||||
assertTrue(Comparator.compare(true, true));
|
||||
assertTrue(Comparator.compare(false, false));
|
||||
assertFalse(Comparator.compare(true, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("strings: exact match")
|
||||
void testStrings() {
|
||||
assertTrue(ObjectComparator.compare("hello", "hello"));
|
||||
assertTrue(ObjectComparator.compare("", ""));
|
||||
assertFalse(ObjectComparator.compare("hello", "world"));
|
||||
assertTrue(Comparator.compare("hello", "hello"));
|
||||
assertTrue(Comparator.compare("", ""));
|
||||
assertFalse(Comparator.compare("hello", "world"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("characters: exact match")
|
||||
void testCharacters() {
|
||||
assertTrue(ObjectComparator.compare('a', 'a'));
|
||||
assertFalse(ObjectComparator.compare('a', 'b'));
|
||||
assertTrue(Comparator.compare('a', 'a'));
|
||||
assertFalse(Comparator.compare('a', 'b'));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -97,14 +97,14 @@ class ObjectComparatorTest {
|
|||
@Test
|
||||
@DisplayName("both null: should be equal")
|
||||
void testBothNull() {
|
||||
assertTrue(ObjectComparator.compare(null, null));
|
||||
assertTrue(Comparator.compare(null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("one null: should not be equal")
|
||||
void testOneNull() {
|
||||
assertFalse(ObjectComparator.compare(null, "value"));
|
||||
assertFalse(ObjectComparator.compare("value", null));
|
||||
assertFalse(Comparator.compare(null, "value"));
|
||||
assertFalse(Comparator.compare("value", null));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -119,8 +119,8 @@ class ObjectComparatorTest {
|
|||
List<Integer> list2 = Arrays.asList(1, 2, 3);
|
||||
List<Integer> list3 = Arrays.asList(3, 2, 1);
|
||||
|
||||
assertTrue(ObjectComparator.compare(list1, list2));
|
||||
assertFalse(ObjectComparator.compare(list1, list3));
|
||||
assertTrue(Comparator.compare(list1, list2));
|
||||
assertFalse(Comparator.compare(list1, list3));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -129,7 +129,7 @@ class ObjectComparatorTest {
|
|||
List<Integer> list1 = Arrays.asList(1, 2, 3);
|
||||
List<Integer> list2 = Arrays.asList(1, 2);
|
||||
|
||||
assertFalse(ObjectComparator.compare(list1, list2));
|
||||
assertFalse(Comparator.compare(list1, list2));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -138,7 +138,7 @@ class ObjectComparatorTest {
|
|||
Set<Integer> set1 = new HashSet<>(Arrays.asList(1, 2, 3));
|
||||
Set<Integer> set2 = new HashSet<>(Arrays.asList(3, 2, 1));
|
||||
|
||||
assertTrue(ObjectComparator.compare(set1, set2));
|
||||
assertTrue(Comparator.compare(set1, set2));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -147,14 +147,14 @@ class ObjectComparatorTest {
|
|||
Set<Integer> set1 = new HashSet<>(Arrays.asList(1, 2, 3));
|
||||
Set<Integer> set2 = new HashSet<>(Arrays.asList(1, 2, 4));
|
||||
|
||||
assertFalse(ObjectComparator.compare(set1, set2));
|
||||
assertFalse(Comparator.compare(set1, set2));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("empty collections: should be equal")
|
||||
void testEmptyCollections() {
|
||||
assertTrue(ObjectComparator.compare(new ArrayList<>(), new ArrayList<>()));
|
||||
assertTrue(ObjectComparator.compare(new HashSet<>(), new HashSet<>()));
|
||||
assertTrue(Comparator.compare(new ArrayList<>(), new ArrayList<>()));
|
||||
assertTrue(Comparator.compare(new HashSet<>(), new HashSet<>()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -169,7 +169,7 @@ class ObjectComparatorTest {
|
|||
Arrays.asList(3, 4)
|
||||
);
|
||||
|
||||
assertTrue(ObjectComparator.compare(nested1, nested2));
|
||||
assertTrue(Comparator.compare(nested1, nested2));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -188,7 +188,7 @@ class ObjectComparatorTest {
|
|||
map2.put("two", 2);
|
||||
map2.put("one", 1);
|
||||
|
||||
assertTrue(ObjectComparator.compare(map1, map2));
|
||||
assertTrue(Comparator.compare(map1, map2));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -197,7 +197,7 @@ class ObjectComparatorTest {
|
|||
Map<String, Integer> map1 = Map.of("key", 1);
|
||||
Map<String, Integer> map2 = Map.of("key", 2);
|
||||
|
||||
assertFalse(ObjectComparator.compare(map1, map2));
|
||||
assertFalse(Comparator.compare(map1, map2));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -206,7 +206,7 @@ class ObjectComparatorTest {
|
|||
Map<String, Integer> map1 = Map.of("key1", 1);
|
||||
Map<String, Integer> map2 = Map.of("key2", 1);
|
||||
|
||||
assertFalse(ObjectComparator.compare(map1, map2));
|
||||
assertFalse(Comparator.compare(map1, map2));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -215,7 +215,7 @@ class ObjectComparatorTest {
|
|||
Map<String, Integer> map1 = Map.of("one", 1, "two", 2);
|
||||
Map<String, Integer> map2 = Map.of("one", 1);
|
||||
|
||||
assertFalse(ObjectComparator.compare(map1, map2));
|
||||
assertFalse(Comparator.compare(map1, map2));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -227,7 +227,7 @@ class ObjectComparatorTest {
|
|||
Map<String, Object> map2 = new HashMap<>();
|
||||
map2.put("inner", Map.of("key", "value"));
|
||||
|
||||
assertTrue(ObjectComparator.compare(map1, map2));
|
||||
assertTrue(Comparator.compare(map1, map2));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -242,8 +242,8 @@ class ObjectComparatorTest {
|
|||
int[] arr2 = {1, 2, 3};
|
||||
int[] arr3 = {1, 2, 4};
|
||||
|
||||
assertTrue(ObjectComparator.compare(arr1, arr2));
|
||||
assertFalse(ObjectComparator.compare(arr1, arr3));
|
||||
assertTrue(Comparator.compare(arr1, arr2));
|
||||
assertFalse(Comparator.compare(arr1, arr3));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -252,7 +252,7 @@ class ObjectComparatorTest {
|
|||
String[] arr1 = {"a", "b", "c"};
|
||||
String[] arr2 = {"a", "b", "c"};
|
||||
|
||||
assertTrue(ObjectComparator.compare(arr1, arr2));
|
||||
assertTrue(Comparator.compare(arr1, arr2));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -261,7 +261,7 @@ class ObjectComparatorTest {
|
|||
int[] arr1 = {1, 2, 3};
|
||||
int[] arr2 = {1, 2};
|
||||
|
||||
assertFalse(ObjectComparator.compare(arr1, arr2));
|
||||
assertFalse(Comparator.compare(arr1, arr2));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -275,7 +275,7 @@ class ObjectComparatorTest {
|
|||
Exception e1 = new IllegalArgumentException("test");
|
||||
Exception e2 = new IllegalArgumentException("test");
|
||||
|
||||
assertTrue(ObjectComparator.compare(e1, e2));
|
||||
assertTrue(Comparator.compare(e1, e2));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -284,7 +284,7 @@ class ObjectComparatorTest {
|
|||
Exception e1 = new IllegalArgumentException("test");
|
||||
Exception e2 = new IllegalStateException("test");
|
||||
|
||||
assertFalse(ObjectComparator.compare(e1, e2));
|
||||
assertFalse(Comparator.compare(e1, e2));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -293,7 +293,7 @@ class ObjectComparatorTest {
|
|||
Exception e1 = new RuntimeException("message 1");
|
||||
Exception e2 = new RuntimeException("message 2");
|
||||
|
||||
assertFalse(ObjectComparator.compare(e1, e2));
|
||||
assertFalse(Comparator.compare(e1, e2));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -302,7 +302,7 @@ class ObjectComparatorTest {
|
|||
Exception e1 = new RuntimeException((String) null);
|
||||
Exception e2 = new RuntimeException((String) null);
|
||||
|
||||
assertTrue(ObjectComparator.compare(e1, e2));
|
||||
assertTrue(Comparator.compare(e1, e2));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -318,7 +318,7 @@ class ObjectComparatorTest {
|
|||
);
|
||||
|
||||
assertThrows(KryoPlaceholderAccessException.class, () -> {
|
||||
ObjectComparator.compare(placeholder, "anything");
|
||||
Comparator.compare(placeholder, "anything");
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -330,7 +330,7 @@ class ObjectComparatorTest {
|
|||
);
|
||||
|
||||
assertThrows(KryoPlaceholderAccessException.class, () -> {
|
||||
ObjectComparator.compare("anything", placeholder);
|
||||
Comparator.compare("anything", placeholder);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -348,7 +348,7 @@ class ObjectComparatorTest {
|
|||
map2.put("socket", "different");
|
||||
|
||||
assertThrows(KryoPlaceholderAccessException.class, () -> {
|
||||
ObjectComparator.compare(map1, map2);
|
||||
Comparator.compare(map1, map2);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -359,8 +359,8 @@ class ObjectComparatorTest {
|
|||
"java.net.Socket", "<socket>", "error", "path"
|
||||
);
|
||||
|
||||
ObjectComparator.ComparisonResult result =
|
||||
ObjectComparator.compareWithDetails(placeholder, "anything");
|
||||
Comparator.ComparisonResult result =
|
||||
Comparator.compareWithDetails(placeholder, "anything");
|
||||
|
||||
assertFalse(result.isEqual());
|
||||
assertTrue(result.hasError());
|
||||
|
|
@ -378,7 +378,7 @@ class ObjectComparatorTest {
|
|||
TestObj obj1 = new TestObj("name", 42);
|
||||
TestObj obj2 = new TestObj("name", 42);
|
||||
|
||||
assertTrue(ObjectComparator.compare(obj1, obj2));
|
||||
assertTrue(Comparator.compare(obj1, obj2));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -387,7 +387,7 @@ class ObjectComparatorTest {
|
|||
TestObj obj1 = new TestObj("name", 42);
|
||||
TestObj obj2 = new TestObj("name", 43);
|
||||
|
||||
assertFalse(ObjectComparator.compare(obj1, obj2));
|
||||
assertFalse(Comparator.compare(obj1, obj2));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -396,7 +396,7 @@ class ObjectComparatorTest {
|
|||
TestNested nested1 = new TestNested(new TestObj("inner", 1));
|
||||
TestNested nested2 = new TestNested(new TestObj("inner", 1));
|
||||
|
||||
assertTrue(ObjectComparator.compare(nested1, nested2));
|
||||
assertTrue(Comparator.compare(nested1, nested2));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -410,7 +410,7 @@ class ObjectComparatorTest {
|
|||
List<Integer> arrayList = new ArrayList<>(Arrays.asList(1, 2, 3));
|
||||
List<Integer> linkedList = new LinkedList<>(Arrays.asList(1, 2, 3));
|
||||
|
||||
assertTrue(ObjectComparator.compare(arrayList, linkedList));
|
||||
assertTrue(Comparator.compare(arrayList, linkedList));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -422,14 +422,14 @@ class ObjectComparatorTest {
|
|||
Map<String, Integer> linkedHashMap = new LinkedHashMap<>();
|
||||
linkedHashMap.put("key", 1);
|
||||
|
||||
assertTrue(ObjectComparator.compare(hashMap, linkedHashMap));
|
||||
assertTrue(Comparator.compare(hashMap, linkedHashMap));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("incompatible types: not equal")
|
||||
void testIncompatibleTypes() {
|
||||
assertFalse(ObjectComparator.compare("string", 42));
|
||||
assertFalse(ObjectComparator.compare(new ArrayList<>(), new HashMap<>()));
|
||||
assertFalse(Comparator.compare("string", 42));
|
||||
assertFalse(Comparator.compare(new ArrayList<>(), new HashMap<>()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -440,26 +440,26 @@ class ObjectComparatorTest {
|
|||
@Test
|
||||
@DisplayName("both empty: equal")
|
||||
void testBothEmpty() {
|
||||
assertTrue(ObjectComparator.compare(Optional.empty(), Optional.empty()));
|
||||
assertTrue(Comparator.compare(Optional.empty(), Optional.empty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("both present with same value: equal")
|
||||
void testBothPresentSame() {
|
||||
assertTrue(ObjectComparator.compare(Optional.of("value"), Optional.of("value")));
|
||||
assertTrue(Comparator.compare(Optional.of("value"), Optional.of("value")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("one empty, one present: not equal")
|
||||
void testOneEmpty() {
|
||||
assertFalse(ObjectComparator.compare(Optional.empty(), Optional.of("value")));
|
||||
assertFalse(ObjectComparator.compare(Optional.of("value"), Optional.empty()));
|
||||
assertFalse(Comparator.compare(Optional.empty(), Optional.of("value")));
|
||||
assertFalse(Comparator.compare(Optional.of("value"), Optional.empty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("both present with different values: not equal")
|
||||
void testDifferentValues() {
|
||||
assertFalse(ObjectComparator.compare(Optional.of("a"), Optional.of("b")));
|
||||
assertFalse(Comparator.compare(Optional.of("a"), Optional.of("b")));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -470,13 +470,13 @@ class ObjectComparatorTest {
|
|||
@Test
|
||||
@DisplayName("same enum values: equal")
|
||||
void testSameEnum() {
|
||||
assertTrue(ObjectComparator.compare(TestEnum.A, TestEnum.A));
|
||||
assertTrue(Comparator.compare(TestEnum.A, TestEnum.A));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("different enum values: not equal")
|
||||
void testDifferentEnum() {
|
||||
assertFalse(ObjectComparator.compare(TestEnum.A, TestEnum.B));
|
||||
assertFalse(Comparator.compare(TestEnum.A, TestEnum.B));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -117,11 +117,11 @@ class KryoPlaceholderTest {
|
|||
);
|
||||
|
||||
// Serialize and deserialize the placeholder
|
||||
byte[] serialized = KryoSerializer.serialize(original);
|
||||
byte[] serialized = Serializer.serialize(original);
|
||||
assertNotNull(serialized);
|
||||
assertTrue(serialized.length > 0);
|
||||
|
||||
Object deserialized = KryoSerializer.deserialize(serialized);
|
||||
Object deserialized = Serializer.deserialize(serialized);
|
||||
assertInstanceOf(KryoPlaceholder.class, deserialized);
|
||||
|
||||
KryoPlaceholder restored = (KryoPlaceholder) deserialized;
|
||||
|
|
|
|||
|
|
@ -1,567 +0,0 @@
|
|||
package com.codeflash;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Tests for KryoSerializer following Python's dill/patcher test patterns.
|
||||
*
|
||||
* Test pattern: Create object -> Serialize -> Deserialize -> Compare with original
|
||||
*/
|
||||
@DisplayName("KryoSerializer Tests")
|
||||
class KryoSerializerTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
KryoSerializer.clearUnserializableTypesCache();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ROUNDTRIP TESTS - Following Python's test patterns
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Roundtrip Tests - Simple Nested Structures")
|
||||
class RoundtripSimpleNestedTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("simple nested data structure serializes and deserializes correctly")
|
||||
void testSimpleNested() {
|
||||
Map<String, Object> originalData = new LinkedHashMap<>();
|
||||
originalData.put("numbers", Arrays.asList(1, 2, 3));
|
||||
Map<String, Object> nestedDict = new LinkedHashMap<>();
|
||||
nestedDict.put("key", "value");
|
||||
nestedDict.put("another", 42);
|
||||
originalData.put("nested_dict", nestedDict);
|
||||
|
||||
byte[] dumped = KryoSerializer.serialize(originalData);
|
||||
Object reloaded = KryoSerializer.deserialize(dumped);
|
||||
|
||||
assertTrue(ObjectComparator.compare(originalData, reloaded),
|
||||
"Reloaded data should equal original data");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("integers roundtrip correctly")
|
||||
void testIntegers() {
|
||||
int[] testCases = {5, 0, -1, Integer.MAX_VALUE, Integer.MIN_VALUE};
|
||||
for (int original : testCases) {
|
||||
byte[] dumped = KryoSerializer.serialize(original);
|
||||
Object reloaded = KryoSerializer.deserialize(dumped);
|
||||
assertTrue(ObjectComparator.compare(original, reloaded),
|
||||
"Failed for: " + original);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("floats roundtrip correctly with epsilon tolerance")
|
||||
void testFloats() {
|
||||
double[] testCases = {5.0, 0.0, -1.0, 3.14159, Double.MAX_VALUE};
|
||||
for (double original : testCases) {
|
||||
byte[] dumped = KryoSerializer.serialize(original);
|
||||
Object reloaded = KryoSerializer.deserialize(dumped);
|
||||
assertTrue(ObjectComparator.compare(original, reloaded),
|
||||
"Failed for: " + original);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("strings roundtrip correctly")
|
||||
void testStrings() {
|
||||
String[] testCases = {"Hello", "", "World", "unicode: \u00e9\u00e8"};
|
||||
for (String original : testCases) {
|
||||
byte[] dumped = KryoSerializer.serialize(original);
|
||||
Object reloaded = KryoSerializer.deserialize(dumped);
|
||||
assertTrue(ObjectComparator.compare(original, reloaded),
|
||||
"Failed for: " + original);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("lists roundtrip correctly")
|
||||
void testLists() {
|
||||
List<Integer> original = Arrays.asList(1, 2, 3);
|
||||
byte[] dumped = KryoSerializer.serialize(original);
|
||||
Object reloaded = KryoSerializer.deserialize(dumped);
|
||||
assertTrue(ObjectComparator.compare(original, reloaded));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("maps roundtrip correctly")
|
||||
void testMaps() {
|
||||
Map<String, Integer> original = new LinkedHashMap<>();
|
||||
original.put("a", 1);
|
||||
original.put("b", 2);
|
||||
|
||||
byte[] dumped = KryoSerializer.serialize(original);
|
||||
Object reloaded = KryoSerializer.deserialize(dumped);
|
||||
assertTrue(ObjectComparator.compare(original, reloaded));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("sets roundtrip correctly")
|
||||
void testSets() {
|
||||
Set<Integer> original = new LinkedHashSet<>(Arrays.asList(1, 2, 3));
|
||||
byte[] dumped = KryoSerializer.serialize(original);
|
||||
Object reloaded = KryoSerializer.deserialize(dumped);
|
||||
assertTrue(ObjectComparator.compare(original, reloaded));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("null roundtrips correctly")
|
||||
void testNull() {
|
||||
byte[] dumped = KryoSerializer.serialize(null);
|
||||
Object reloaded = KryoSerializer.deserialize(dumped);
|
||||
assertNull(reloaded);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// UNSERIALIZABLE OBJECT TESTS
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Unserializable Object Tests")
|
||||
class UnserializableObjectTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("socket replaced by KryoPlaceholder")
|
||||
void testSocketReplacedByPlaceholder() throws Exception {
|
||||
try (Socket socket = new Socket()) {
|
||||
Map<String, Object> dataWithSocket = new LinkedHashMap<>();
|
||||
dataWithSocket.put("safe_value", 123);
|
||||
dataWithSocket.put("raw_socket", socket);
|
||||
|
||||
byte[] dumped = KryoSerializer.serialize(dataWithSocket);
|
||||
Map<?, ?> reloaded = (Map<?, ?>) KryoSerializer.deserialize(dumped);
|
||||
|
||||
assertInstanceOf(Map.class, reloaded);
|
||||
assertEquals(123, reloaded.get("safe_value"));
|
||||
assertInstanceOf(KryoPlaceholder.class, reloaded.get("raw_socket"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("database connection replaced by KryoPlaceholder")
|
||||
void testDatabaseConnectionReplacedByPlaceholder() throws Exception {
|
||||
try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) {
|
||||
Map<String, Object> dataWithDb = new LinkedHashMap<>();
|
||||
dataWithDb.put("description", "Database connection");
|
||||
dataWithDb.put("connection", conn);
|
||||
|
||||
byte[] dumped = KryoSerializer.serialize(dataWithDb);
|
||||
Map<?, ?> reloaded = (Map<?, ?>) KryoSerializer.deserialize(dumped);
|
||||
|
||||
assertInstanceOf(Map.class, reloaded);
|
||||
assertEquals("Database connection", reloaded.get("description"));
|
||||
assertInstanceOf(KryoPlaceholder.class, reloaded.get("connection"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("InputStream replaced by KryoPlaceholder")
|
||||
void testInputStreamReplacedByPlaceholder() {
|
||||
InputStream stream = new ByteArrayInputStream("test".getBytes());
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("description", "Contains stream");
|
||||
data.put("stream", stream);
|
||||
|
||||
byte[] dumped = KryoSerializer.serialize(data);
|
||||
Map<?, ?> reloaded = (Map<?, ?>) KryoSerializer.deserialize(dumped);
|
||||
|
||||
assertEquals("Contains stream", reloaded.get("description"));
|
||||
assertInstanceOf(KryoPlaceholder.class, reloaded.get("stream"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("OutputStream replaced by KryoPlaceholder")
|
||||
void testOutputStreamReplacedByPlaceholder() {
|
||||
OutputStream stream = new ByteArrayOutputStream();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("stream", stream);
|
||||
|
||||
byte[] dumped = KryoSerializer.serialize(data);
|
||||
Map<?, ?> reloaded = (Map<?, ?>) KryoSerializer.deserialize(dumped);
|
||||
|
||||
assertInstanceOf(KryoPlaceholder.class, reloaded.get("stream"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("deeply nested unserializable object")
|
||||
void testDeeplyNestedUnserializable() throws Exception {
|
||||
try (Socket socket = new Socket()) {
|
||||
Map<String, Object> level3 = new LinkedHashMap<>();
|
||||
level3.put("normal", "value");
|
||||
level3.put("socket", socket);
|
||||
|
||||
Map<String, Object> level2 = new LinkedHashMap<>();
|
||||
level2.put("level3", level3);
|
||||
|
||||
Map<String, Object> level1 = new LinkedHashMap<>();
|
||||
level1.put("level2", level2);
|
||||
|
||||
Map<String, Object> deepNested = new LinkedHashMap<>();
|
||||
deepNested.put("level1", level1);
|
||||
|
||||
byte[] dumped = KryoSerializer.serialize(deepNested);
|
||||
Map<?, ?> reloaded = (Map<?, ?>) KryoSerializer.deserialize(dumped);
|
||||
|
||||
Map<?, ?> l1 = (Map<?, ?>) reloaded.get("level1");
|
||||
Map<?, ?> l2 = (Map<?, ?>) l1.get("level2");
|
||||
Map<?, ?> l3 = (Map<?, ?>) l2.get("level3");
|
||||
|
||||
assertEquals("value", l3.get("normal"));
|
||||
assertInstanceOf(KryoPlaceholder.class, l3.get("socket"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("class with unserializable attribute - field becomes placeholder")
|
||||
void testClassWithUnserializableAttribute() throws Exception {
|
||||
Socket socket = new Socket();
|
||||
try {
|
||||
TestClassWithSocket obj = new TestClassWithSocket();
|
||||
obj.normal = "normal value";
|
||||
obj.unserializable = socket;
|
||||
|
||||
byte[] dumped = KryoSerializer.serialize(obj);
|
||||
Object reloaded = KryoSerializer.deserialize(dumped);
|
||||
|
||||
// The object itself is serializable - only the socket field becomes a placeholder
|
||||
// This matches Python's pickle_patcher behavior which preserves object structure
|
||||
assertInstanceOf(TestClassWithSocket.class, reloaded);
|
||||
TestClassWithSocket reloadedObj = (TestClassWithSocket) reloaded;
|
||||
|
||||
assertEquals("normal value", reloadedObj.normal);
|
||||
assertInstanceOf(KryoPlaceholder.class, reloadedObj.unserializable);
|
||||
} finally {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PLACEHOLDER ACCESS TESTS
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Placeholder Access Tests")
|
||||
class PlaceholderAccessTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("comparing objects with placeholder throws KryoPlaceholderAccessException")
|
||||
void testPlaceholderComparisonThrowsException() throws Exception {
|
||||
try (Socket socket = new Socket()) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("socket", socket);
|
||||
|
||||
byte[] dumped = KryoSerializer.serialize(data);
|
||||
Map<?, ?> reloaded = (Map<?, ?>) KryoSerializer.deserialize(dumped);
|
||||
|
||||
KryoPlaceholder placeholder = (KryoPlaceholder) reloaded.get("socket");
|
||||
|
||||
assertThrows(KryoPlaceholderAccessException.class, () -> {
|
||||
ObjectComparator.compare(placeholder, "anything");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// EXCEPTION SERIALIZATION TESTS
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Exception Serialization Tests")
|
||||
class ExceptionSerializationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("exception serializes with type and message")
|
||||
void testExceptionSerialization() {
|
||||
Exception original = new IllegalArgumentException("test error");
|
||||
|
||||
byte[] dumped = KryoSerializer.serializeException(original);
|
||||
Map<?, ?> reloaded = (Map<?, ?>) KryoSerializer.deserialize(dumped);
|
||||
|
||||
assertEquals(true, reloaded.get("__exception__"));
|
||||
assertEquals("java.lang.IllegalArgumentException", reloaded.get("type"));
|
||||
assertEquals("test error", reloaded.get("message"));
|
||||
assertNotNull(reloaded.get("stackTrace"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("exception with cause includes cause info")
|
||||
void testExceptionWithCause() {
|
||||
Exception cause = new NullPointerException("root cause");
|
||||
Exception original = new RuntimeException("wrapper", cause);
|
||||
|
||||
byte[] dumped = KryoSerializer.serializeException(original);
|
||||
Map<?, ?> reloaded = (Map<?, ?>) KryoSerializer.deserialize(dumped);
|
||||
|
||||
assertEquals("java.lang.NullPointerException", reloaded.get("causeType"));
|
||||
assertEquals("root cause", reloaded.get("causeMessage"));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CIRCULAR REFERENCE TESTS
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Circular Reference Tests")
|
||||
class CircularReferenceTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("circular reference handled without stack overflow")
|
||||
void testCircularReference() {
|
||||
Node a = new Node("A");
|
||||
Node b = new Node("B");
|
||||
a.next = b;
|
||||
b.next = a;
|
||||
|
||||
byte[] dumped = KryoSerializer.serialize(a);
|
||||
assertNotNull(dumped);
|
||||
|
||||
Object reloaded = KryoSerializer.deserialize(dumped);
|
||||
assertNotNull(reloaded);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("self-referencing object handled gracefully")
|
||||
void testSelfReference() {
|
||||
SelfReferencing obj = new SelfReferencing();
|
||||
obj.self = obj;
|
||||
|
||||
byte[] dumped = KryoSerializer.serialize(obj);
|
||||
assertNotNull(dumped);
|
||||
|
||||
Object reloaded = KryoSerializer.deserialize(dumped);
|
||||
assertNotNull(reloaded);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("deeply nested structure respects max depth")
|
||||
void testDeeplyNested() {
|
||||
Map<String, Object> current = new HashMap<>();
|
||||
Map<String, Object> root = current;
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
Map<String, Object> next = new HashMap<>();
|
||||
current.put("nested", next);
|
||||
current = next;
|
||||
}
|
||||
current.put("value", "deep");
|
||||
|
||||
byte[] dumped = KryoSerializer.serialize(root);
|
||||
assertNotNull(dumped);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FULL FLOW TESTS - SQLite Integration
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Full Flow Tests - SQLite Integration")
|
||||
class FullFlowTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("serialize -> store in SQLite BLOB -> read -> deserialize -> compare")
|
||||
void testFullFlowWithSQLite() throws Exception {
|
||||
Path dbPath = Files.createTempFile("kryo_test_", ".db");
|
||||
|
||||
try {
|
||||
Map<String, Object> inputArgs = new LinkedHashMap<>();
|
||||
inputArgs.put("numbers", Arrays.asList(3, 1, 4, 1, 5));
|
||||
inputArgs.put("name", "test");
|
||||
|
||||
List<Integer> result = Arrays.asList(1, 1, 3, 4, 5);
|
||||
|
||||
byte[] argsBlob = KryoSerializer.serialize(inputArgs);
|
||||
byte[] resultBlob = KryoSerializer.serialize(result);
|
||||
|
||||
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + dbPath)) {
|
||||
conn.createStatement().execute(
|
||||
"CREATE TABLE test_results (id INTEGER PRIMARY KEY, args BLOB, result BLOB)"
|
||||
);
|
||||
|
||||
try (PreparedStatement ps = conn.prepareStatement(
|
||||
"INSERT INTO test_results (id, args, result) VALUES (?, ?, ?)")) {
|
||||
ps.setInt(1, 1);
|
||||
ps.setBytes(2, argsBlob);
|
||||
ps.setBytes(3, resultBlob);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
|
||||
try (PreparedStatement ps = conn.prepareStatement(
|
||||
"SELECT args, result FROM test_results WHERE id = ?")) {
|
||||
ps.setInt(1, 1);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
assertTrue(rs.next());
|
||||
|
||||
byte[] storedArgs = rs.getBytes("args");
|
||||
byte[] storedResult = rs.getBytes("result");
|
||||
|
||||
Object deserializedArgs = KryoSerializer.deserialize(storedArgs);
|
||||
Object deserializedResult = KryoSerializer.deserialize(storedResult);
|
||||
|
||||
assertTrue(ObjectComparator.compare(inputArgs, deserializedArgs),
|
||||
"Args should match after full SQLite round-trip");
|
||||
assertTrue(ObjectComparator.compare(result, deserializedResult),
|
||||
"Result should match after full SQLite round-trip");
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Files.deleteIfExists(dbPath);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("full flow with custom objects")
|
||||
void testFullFlowWithCustomObjects() throws Exception {
|
||||
Path dbPath = Files.createTempFile("kryo_custom_", ".db");
|
||||
|
||||
try {
|
||||
TestPerson original = new TestPerson("Alice", 25);
|
||||
|
||||
byte[] blob = KryoSerializer.serialize(original);
|
||||
|
||||
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + dbPath)) {
|
||||
conn.createStatement().execute(
|
||||
"CREATE TABLE objects (id INTEGER PRIMARY KEY, data BLOB)"
|
||||
);
|
||||
|
||||
try (PreparedStatement ps = conn.prepareStatement(
|
||||
"INSERT INTO objects (id, data) VALUES (?, ?)")) {
|
||||
ps.setInt(1, 1);
|
||||
ps.setBytes(2, blob);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
|
||||
try (PreparedStatement ps = conn.prepareStatement(
|
||||
"SELECT data FROM objects WHERE id = ?")) {
|
||||
ps.setInt(1, 1);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
assertTrue(rs.next());
|
||||
byte[] stored = rs.getBytes("data");
|
||||
Object deserialized = KryoSerializer.deserialize(stored);
|
||||
|
||||
assertTrue(ObjectComparator.compare(original, deserialized));
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Files.deleteIfExists(dbPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// DATE/TIME AND ENUM TESTS
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Date/Time and Enum Tests")
|
||||
class DateTimeEnumTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("LocalDate roundtrips correctly")
|
||||
void testLocalDate() {
|
||||
LocalDate original = LocalDate.of(2024, 1, 15);
|
||||
byte[] dumped = KryoSerializer.serialize(original);
|
||||
Object reloaded = KryoSerializer.deserialize(dumped);
|
||||
assertTrue(ObjectComparator.compare(original, reloaded));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("LocalDateTime roundtrips correctly")
|
||||
void testLocalDateTime() {
|
||||
LocalDateTime original = LocalDateTime.of(2024, 1, 15, 10, 30, 45);
|
||||
byte[] dumped = KryoSerializer.serialize(original);
|
||||
Object reloaded = KryoSerializer.deserialize(dumped);
|
||||
assertTrue(ObjectComparator.compare(original, reloaded));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Date roundtrips correctly")
|
||||
void testDate() {
|
||||
Date original = new Date();
|
||||
byte[] dumped = KryoSerializer.serialize(original);
|
||||
Object reloaded = KryoSerializer.deserialize(dumped);
|
||||
assertTrue(ObjectComparator.compare(original, reloaded));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("enum roundtrips correctly")
|
||||
void testEnum() {
|
||||
TestEnum original = TestEnum.VALUE_B;
|
||||
byte[] dumped = KryoSerializer.serialize(original);
|
||||
Object reloaded = KryoSerializer.deserialize(dumped);
|
||||
assertTrue(ObjectComparator.compare(original, reloaded));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// TEST HELPER CLASSES
|
||||
// ============================================================
|
||||
|
||||
static class TestPerson {
|
||||
String name;
|
||||
int age;
|
||||
|
||||
TestPerson() {}
|
||||
|
||||
TestPerson(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
}
|
||||
|
||||
static class TestClassWithSocket {
|
||||
String normal;
|
||||
Object unserializable; // Using Object to allow placeholder substitution
|
||||
|
||||
TestClassWithSocket() {}
|
||||
}
|
||||
|
||||
static class Node {
|
||||
String value;
|
||||
Node next;
|
||||
|
||||
Node() {}
|
||||
|
||||
Node(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
static class SelfReferencing {
|
||||
SelfReferencing self;
|
||||
|
||||
SelfReferencing() {}
|
||||
}
|
||||
|
||||
enum TestEnum {
|
||||
VALUE_A, VALUE_B, VALUE_C
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,804 @@
|
|||
package com.codeflash;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Edge case tests for Serializer to ensure robust serialization.
|
||||
*/
|
||||
@DisplayName("Serializer Edge Case Tests")
|
||||
class SerializerEdgeCaseTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
Serializer.clearUnserializableTypesCache();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// NUMBER EDGE CASES
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Number Serialization")
|
||||
class NumberSerialization {
|
||||
|
||||
@Test
|
||||
@DisplayName("BigDecimal roundtrip")
|
||||
void testBigDecimalRoundtrip() {
|
||||
BigDecimal original = new BigDecimal("123456789.123456789012345678901234567890");
|
||||
|
||||
byte[] serialized = Serializer.serialize(original);
|
||||
Object deserialized = Serializer.deserialize(serialized);
|
||||
|
||||
assertTrue(Comparator.compare(original, deserialized),
|
||||
"BigDecimal should survive roundtrip");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("BigInteger roundtrip")
|
||||
void testBigIntegerRoundtrip() {
|
||||
BigInteger original = new BigInteger("123456789012345678901234567890123456789012345678901234567890");
|
||||
|
||||
byte[] serialized = Serializer.serialize(original);
|
||||
Object deserialized = Serializer.deserialize(serialized);
|
||||
|
||||
assertTrue(Comparator.compare(original, deserialized),
|
||||
"BigInteger should survive roundtrip");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("AtomicInteger - known limitation, becomes Map")
|
||||
void testAtomicIntegerLimitation() {
|
||||
// AtomicInteger uses Unsafe internally, which causes issues with reflection-based serialization
|
||||
// This documents the limitation - atomic types may not roundtrip perfectly
|
||||
AtomicInteger original = new AtomicInteger(42);
|
||||
|
||||
byte[] serialized = Serializer.serialize(original);
|
||||
Object deserialized = Serializer.deserialize(serialized);
|
||||
|
||||
// Currently becomes a Map due to internal Unsafe usage
|
||||
// This is a known limitation for JDK atomic types
|
||||
assertNotNull(deserialized);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Special double values")
|
||||
void testSpecialDoubleValues() {
|
||||
double[] values = {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, -0.0, Double.MIN_VALUE, Double.MAX_VALUE};
|
||||
|
||||
for (double value : values) {
|
||||
byte[] serialized = Serializer.serialize(value);
|
||||
Object deserialized = Serializer.deserialize(serialized);
|
||||
|
||||
assertTrue(Comparator.compare(value, deserialized),
|
||||
"Failed for value: " + value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// DATE/TIME EDGE CASES
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Date/Time Serialization")
|
||||
class DateTimeSerialization {
|
||||
|
||||
@Test
|
||||
@DisplayName("All Java 8 time types")
|
||||
void testJava8TimeTypes() {
|
||||
Object[] timeObjects = {
|
||||
LocalDate.of(2024, 1, 15),
|
||||
LocalTime.of(10, 30, 45),
|
||||
LocalDateTime.of(2024, 1, 15, 10, 30, 45),
|
||||
Instant.now(),
|
||||
Duration.ofHours(5),
|
||||
Period.ofMonths(3),
|
||||
ZonedDateTime.now(),
|
||||
OffsetDateTime.now(),
|
||||
OffsetTime.now(),
|
||||
Year.of(2024),
|
||||
YearMonth.of(2024, 1),
|
||||
MonthDay.of(1, 15)
|
||||
};
|
||||
|
||||
for (Object original : timeObjects) {
|
||||
byte[] serialized = Serializer.serialize(original);
|
||||
Object deserialized = Serializer.deserialize(serialized);
|
||||
|
||||
assertTrue(Comparator.compare(original, deserialized),
|
||||
"Failed for type: " + original.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Legacy Date types")
|
||||
void testLegacyDateTypes() {
|
||||
Date date = new Date();
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
|
||||
byte[] serializedDate = Serializer.serialize(date);
|
||||
Object deserializedDate = Serializer.deserialize(serializedDate);
|
||||
assertTrue(Comparator.compare(date, deserializedDate));
|
||||
|
||||
byte[] serializedCal = Serializer.serialize(calendar);
|
||||
Object deserializedCal = Serializer.deserialize(serializedCal);
|
||||
assertInstanceOf(Calendar.class, deserializedCal);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// COLLECTION EDGE CASES
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Collection Edge Cases")
|
||||
class CollectionEdgeCases {
|
||||
|
||||
@Test
|
||||
@DisplayName("Empty collections")
|
||||
void testEmptyCollections() {
|
||||
Collection<?>[] empties = {
|
||||
new ArrayList<>(),
|
||||
new LinkedList<>(),
|
||||
new HashSet<>(),
|
||||
new TreeSet<>(),
|
||||
new LinkedHashSet<>()
|
||||
};
|
||||
|
||||
for (Collection<?> original : empties) {
|
||||
byte[] serialized = Serializer.serialize(original);
|
||||
Object deserialized = Serializer.deserialize(serialized);
|
||||
|
||||
assertEquals(original.getClass(), deserialized.getClass(),
|
||||
"Type should be preserved for: " + original.getClass().getSimpleName());
|
||||
assertTrue(((Collection<?>) deserialized).isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Empty maps")
|
||||
void testEmptyMaps() {
|
||||
Map<?, ?>[] empties = {
|
||||
new HashMap<>(),
|
||||
new LinkedHashMap<>(),
|
||||
new TreeMap<>()
|
||||
};
|
||||
|
||||
for (Map<?, ?> original : empties) {
|
||||
byte[] serialized = Serializer.serialize(original);
|
||||
Object deserialized = Serializer.deserialize(serialized);
|
||||
|
||||
assertEquals(original.getClass(), deserialized.getClass());
|
||||
assertTrue(((Map<?, ?>) deserialized).isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Collections with null elements")
|
||||
void testCollectionsWithNulls() {
|
||||
List<String> list = new ArrayList<>();
|
||||
list.add("a");
|
||||
list.add(null);
|
||||
list.add("c");
|
||||
|
||||
byte[] serialized = Serializer.serialize(list);
|
||||
List<?> deserialized = (List<?>) Serializer.deserialize(serialized);
|
||||
|
||||
assertEquals(3, deserialized.size());
|
||||
assertEquals("a", deserialized.get(0));
|
||||
assertNull(deserialized.get(1));
|
||||
assertEquals("c", deserialized.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Map with null key and value")
|
||||
void testMapWithNulls() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put(null, "nullKey");
|
||||
map.put("nullValue", null);
|
||||
map.put("normal", "value");
|
||||
|
||||
byte[] serialized = Serializer.serialize(map);
|
||||
Map<?, ?> deserialized = (Map<?, ?>) Serializer.deserialize(serialized);
|
||||
|
||||
assertEquals(3, deserialized.size());
|
||||
assertEquals("nullKey", deserialized.get(null));
|
||||
assertNull(deserialized.get("nullValue"));
|
||||
assertEquals("value", deserialized.get("normal"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ConcurrentHashMap roundtrip")
|
||||
void testConcurrentHashMap() {
|
||||
ConcurrentHashMap<String, Integer> original = new ConcurrentHashMap<>();
|
||||
original.put("a", 1);
|
||||
original.put("b", 2);
|
||||
|
||||
byte[] serialized = Serializer.serialize(original);
|
||||
Object deserialized = Serializer.deserialize(serialized);
|
||||
|
||||
assertInstanceOf(ConcurrentHashMap.class, deserialized);
|
||||
assertTrue(Comparator.compare(original, deserialized));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("EnumSet and EnumMap")
|
||||
void testEnumCollections() {
|
||||
EnumSet<DayOfWeek> enumSet = EnumSet.of(DayOfWeek.MONDAY, DayOfWeek.FRIDAY);
|
||||
EnumMap<DayOfWeek, String> enumMap = new EnumMap<>(DayOfWeek.class);
|
||||
enumMap.put(DayOfWeek.MONDAY, "Start");
|
||||
enumMap.put(DayOfWeek.FRIDAY, "End");
|
||||
|
||||
byte[] serializedSet = Serializer.serialize(enumSet);
|
||||
Object deserializedSet = Serializer.deserialize(serializedSet);
|
||||
assertTrue(Comparator.compare(enumSet, deserializedSet));
|
||||
|
||||
byte[] serializedMap = Serializer.serialize(enumMap);
|
||||
Object deserializedMap = Serializer.deserialize(serializedMap);
|
||||
assertTrue(Comparator.compare(enumMap, deserializedMap));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ARRAY EDGE CASES
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Array Edge Cases")
|
||||
class ArrayEdgeCases {
|
||||
|
||||
@Test
|
||||
@DisplayName("Empty arrays of various types")
|
||||
void testEmptyArrays() {
|
||||
Object[] empties = {
|
||||
new int[0],
|
||||
new String[0],
|
||||
new Object[0],
|
||||
new double[0]
|
||||
};
|
||||
|
||||
for (Object original : empties) {
|
||||
byte[] serialized = Serializer.serialize(original);
|
||||
Object deserialized = Serializer.deserialize(serialized);
|
||||
|
||||
assertEquals(original.getClass(), deserialized.getClass());
|
||||
assertEquals(0, java.lang.reflect.Array.getLength(deserialized));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Multi-dimensional arrays")
|
||||
void testMultiDimensionalArrays() {
|
||||
int[][][] original = {{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}};
|
||||
|
||||
byte[] serialized = Serializer.serialize(original);
|
||||
Object deserialized = Serializer.deserialize(serialized);
|
||||
|
||||
assertTrue(Comparator.compare(original, deserialized));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Array with all nulls")
|
||||
void testArrayWithAllNulls() {
|
||||
String[] original = new String[3]; // All null
|
||||
|
||||
byte[] serialized = Serializer.serialize(original);
|
||||
String[] deserialized = (String[]) Serializer.deserialize(serialized);
|
||||
|
||||
assertEquals(3, deserialized.length);
|
||||
assertNull(deserialized[0]);
|
||||
assertNull(deserialized[1]);
|
||||
assertNull(deserialized[2]);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SPECIAL TYPES
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Special Types")
|
||||
class SpecialTypes {
|
||||
|
||||
@Test
|
||||
@DisplayName("UUID roundtrip")
|
||||
void testUUIDRoundtrip() {
|
||||
UUID original = UUID.randomUUID();
|
||||
|
||||
byte[] serialized = Serializer.serialize(original);
|
||||
Object deserialized = Serializer.deserialize(serialized);
|
||||
|
||||
assertEquals(original, deserialized);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Currency roundtrip")
|
||||
void testCurrencyRoundtrip() {
|
||||
Currency original = Currency.getInstance("USD");
|
||||
|
||||
byte[] serialized = Serializer.serialize(original);
|
||||
Object deserialized = Serializer.deserialize(serialized);
|
||||
|
||||
assertEquals(original, deserialized);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Locale roundtrip")
|
||||
void testLocaleRoundtrip() {
|
||||
Locale original = Locale.US;
|
||||
|
||||
byte[] serialized = Serializer.serialize(original);
|
||||
Object deserialized = Serializer.deserialize(serialized);
|
||||
|
||||
assertEquals(original, deserialized);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Optional roundtrip")
|
||||
void testOptionalRoundtrip() {
|
||||
Optional<String> present = Optional.of("value");
|
||||
Optional<String> empty = Optional.empty();
|
||||
|
||||
byte[] serializedPresent = Serializer.serialize(present);
|
||||
Object deserializedPresent = Serializer.deserialize(serializedPresent);
|
||||
assertTrue(Comparator.compare(present, deserializedPresent));
|
||||
|
||||
byte[] serializedEmpty = Serializer.serialize(empty);
|
||||
Object deserializedEmpty = Serializer.deserialize(serializedEmpty);
|
||||
assertTrue(Comparator.compare(empty, deserializedEmpty));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// COMPLEX NESTED STRUCTURES
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Complex Nested Structures")
|
||||
class ComplexNested {
|
||||
|
||||
@Test
|
||||
@DisplayName("Deeply nested maps and lists")
|
||||
void testDeeplyNestedStructure() {
|
||||
Map<String, Object> root = new LinkedHashMap<>();
|
||||
root.put("level1", createNestedStructure(8));
|
||||
|
||||
byte[] serialized = Serializer.serialize(root);
|
||||
Object deserialized = Serializer.deserialize(serialized);
|
||||
|
||||
assertTrue(Comparator.compare(root, deserialized));
|
||||
}
|
||||
|
||||
private Map<String, Object> createNestedStructure(int depth) {
|
||||
if (depth == 0) {
|
||||
Map<String, Object> leaf = new LinkedHashMap<>();
|
||||
leaf.put("value", "leaf");
|
||||
return leaf;
|
||||
}
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("nested", createNestedStructure(depth - 1));
|
||||
map.put("list", Arrays.asList(1, 2, 3));
|
||||
return map;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Mixed collection types")
|
||||
void testMixedCollectionTypes() {
|
||||
Map<String, Object> mixed = new LinkedHashMap<>();
|
||||
mixed.put("list", Arrays.asList(1, 2, 3));
|
||||
mixed.put("set", new LinkedHashSet<>(Arrays.asList("a", "b", "c")));
|
||||
mixed.put("map", Map.of("key", "value"));
|
||||
mixed.put("array", new int[]{1, 2, 3});
|
||||
|
||||
byte[] serialized = Serializer.serialize(mixed);
|
||||
Object deserialized = Serializer.deserialize(serialized);
|
||||
|
||||
assertTrue(Comparator.compare(mixed, deserialized));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SERIALIZER LIMITS AND BOUNDARIES
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Serializer Limits and Boundaries")
|
||||
class SerializerLimitsTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Collection with exactly MAX_COLLECTION_SIZE (1000) elements")
|
||||
void testCollectionAtMaxSize() {
|
||||
List<Integer> list = new ArrayList<>();
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
list.add(i);
|
||||
}
|
||||
|
||||
byte[] serialized = Serializer.serialize(list);
|
||||
List<?> deserialized = (List<?>) Serializer.deserialize(serialized);
|
||||
|
||||
assertEquals(1000, deserialized.size(),
|
||||
"Collection at exactly MAX_COLLECTION_SIZE should not be truncated");
|
||||
assertTrue(Comparator.compare(list, deserialized));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Collection exceeding MAX_COLLECTION_SIZE gets truncated with placeholder")
|
||||
void testCollectionExceedsMaxSize() {
|
||||
// Create list with unserializable object to trigger recursive processing
|
||||
List<Object> list = new ArrayList<>();
|
||||
for (int i = 0; i < 1001; i++) {
|
||||
list.add(i);
|
||||
}
|
||||
// Add socket to force recursive processing which applies truncation
|
||||
list.add(0, new Object() {
|
||||
// Anonymous class to trigger recursive processing
|
||||
String field = "test";
|
||||
});
|
||||
|
||||
byte[] serialized = Serializer.serialize(list);
|
||||
Object deserialized = Serializer.deserialize(serialized);
|
||||
|
||||
assertNotNull(deserialized, "Should serialize without error");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Map with exactly MAX_COLLECTION_SIZE (1000) entries")
|
||||
void testMapAtMaxSize() {
|
||||
Map<String, Integer> map = new LinkedHashMap<>();
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
map.put("key" + i, i);
|
||||
}
|
||||
|
||||
byte[] serialized = Serializer.serialize(map);
|
||||
Map<?, ?> deserialized = (Map<?, ?>) Serializer.deserialize(serialized);
|
||||
|
||||
assertEquals(1000, deserialized.size(),
|
||||
"Map at exactly MAX_COLLECTION_SIZE should not be truncated");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Nested structure at MAX_DEPTH (10) creates placeholder")
|
||||
void testMaxDepthExceeded() {
|
||||
// Create structure deeper than MAX_DEPTH (10)
|
||||
Map<String, Object> root = new LinkedHashMap<>();
|
||||
Map<String, Object> current = root;
|
||||
|
||||
for (int i = 0; i < 15; i++) {
|
||||
Map<String, Object> next = new LinkedHashMap<>();
|
||||
current.put("level" + i, next);
|
||||
current = next;
|
||||
}
|
||||
current.put("deepValue", "should be placeholder or truncated");
|
||||
|
||||
byte[] serialized = Serializer.serialize(root);
|
||||
Object deserialized = Serializer.deserialize(serialized);
|
||||
|
||||
assertNotNull(deserialized, "Should serialize without stack overflow");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Array at MAX_COLLECTION_SIZE boundary")
|
||||
void testArrayAtMaxSize() {
|
||||
int[] array = new int[1000];
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
array[i] = i;
|
||||
}
|
||||
|
||||
byte[] serialized = Serializer.serialize(array);
|
||||
int[] deserialized = (int[]) Serializer.deserialize(serialized);
|
||||
|
||||
assertEquals(1000, deserialized.length);
|
||||
assertTrue(Comparator.compare(array, deserialized));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// UNSERIALIZABLE TYPE HANDLING
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Unserializable Type Handling")
|
||||
class UnserializableTypeHandlingTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Thread object becomes placeholder")
|
||||
void testThreadBecomesPlaceholder() {
|
||||
Thread thread = new Thread(() -> {});
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("normal", "value");
|
||||
data.put("thread", thread);
|
||||
|
||||
byte[] serialized = Serializer.serialize(data);
|
||||
Map<?, ?> deserialized = (Map<?, ?>) Serializer.deserialize(serialized);
|
||||
|
||||
assertEquals("value", deserialized.get("normal"));
|
||||
assertInstanceOf(KryoPlaceholder.class, deserialized.get("thread"),
|
||||
"Thread should be replaced with KryoPlaceholder");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ThreadGroup object becomes placeholder")
|
||||
void testThreadGroupBecomesPlaceholder() {
|
||||
ThreadGroup group = new ThreadGroup("test-group");
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("group", group);
|
||||
|
||||
byte[] serialized = Serializer.serialize(data);
|
||||
Map<?, ?> deserialized = (Map<?, ?>) Serializer.deserialize(serialized);
|
||||
|
||||
assertInstanceOf(KryoPlaceholder.class, deserialized.get("group"),
|
||||
"ThreadGroup should be replaced with KryoPlaceholder");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ClassLoader becomes placeholder")
|
||||
void testClassLoaderBecomesPlaceholder() {
|
||||
ClassLoader loader = this.getClass().getClassLoader();
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("loader", loader);
|
||||
|
||||
byte[] serialized = Serializer.serialize(data);
|
||||
Map<?, ?> deserialized = (Map<?, ?>) Serializer.deserialize(serialized);
|
||||
|
||||
assertInstanceOf(KryoPlaceholder.class, deserialized.get("loader"),
|
||||
"ClassLoader should be replaced with KryoPlaceholder");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Nested unserializable in List")
|
||||
void testNestedUnserializableInList() {
|
||||
Thread thread = new Thread(() -> {});
|
||||
|
||||
List<Object> list = new ArrayList<>();
|
||||
list.add("before");
|
||||
list.add(thread);
|
||||
list.add("after");
|
||||
|
||||
byte[] serialized = Serializer.serialize(list);
|
||||
List<?> deserialized = (List<?>) Serializer.deserialize(serialized);
|
||||
|
||||
assertEquals(3, deserialized.size());
|
||||
assertEquals("before", deserialized.get(0));
|
||||
assertInstanceOf(KryoPlaceholder.class, deserialized.get(1));
|
||||
assertEquals("after", deserialized.get(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Nested unserializable in Map value")
|
||||
void testNestedUnserializableInMapValue() {
|
||||
Thread thread = new Thread(() -> {});
|
||||
|
||||
Map<String, Object> innerMap = new LinkedHashMap<>();
|
||||
innerMap.put("thread", thread);
|
||||
innerMap.put("normal", "value");
|
||||
|
||||
Map<String, Object> outerMap = new LinkedHashMap<>();
|
||||
outerMap.put("inner", innerMap);
|
||||
|
||||
byte[] serialized = Serializer.serialize(outerMap);
|
||||
Map<?, ?> deserialized = (Map<?, ?>) Serializer.deserialize(serialized);
|
||||
|
||||
Map<?, ?> innerDeserialized = (Map<?, ?>) deserialized.get("inner");
|
||||
assertInstanceOf(KryoPlaceholder.class, innerDeserialized.get("thread"));
|
||||
assertEquals("value", innerDeserialized.get("normal"));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CIRCULAR REFERENCE EDGE CASES
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Circular Reference Edge Cases")
|
||||
class CircularReferenceEdgeCaseTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Self-referencing List")
|
||||
void testSelfReferencingList() {
|
||||
List<Object> list = new ArrayList<>();
|
||||
list.add("item1");
|
||||
list.add(list); // Self-reference
|
||||
list.add("item2");
|
||||
|
||||
byte[] serialized = Serializer.serialize(list);
|
||||
Object deserialized = Serializer.deserialize(serialized);
|
||||
|
||||
assertNotNull(deserialized, "Should handle self-referencing list");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Self-referencing Map")
|
||||
void testSelfReferencingMap() {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("key1", "value1");
|
||||
map.put("self", map); // Self-reference
|
||||
map.put("key2", "value2");
|
||||
|
||||
byte[] serialized = Serializer.serialize(map);
|
||||
Object deserialized = Serializer.deserialize(serialized);
|
||||
|
||||
assertNotNull(deserialized, "Should handle self-referencing map");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Circular reference between two Lists - known limitation")
|
||||
void testCircularReferenceBetweenLists() {
|
||||
// Known limitation: circular references between collections cause StackOverflow
|
||||
// because Kryo's direct serialization is attempted first, which doesn't handle
|
||||
// this case well. This test documents the limitation.
|
||||
List<Object> list1 = new ArrayList<>();
|
||||
List<Object> list2 = new ArrayList<>();
|
||||
|
||||
list1.add("in list1");
|
||||
list1.add(list2);
|
||||
|
||||
list2.add("in list2");
|
||||
list2.add(list1);
|
||||
|
||||
// This will cause StackOverflowError - documenting as known limitation
|
||||
assertThrows(StackOverflowError.class, () -> {
|
||||
Serializer.serialize(list1);
|
||||
}, "Circular references between collections cause StackOverflow - known limitation");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Diamond reference pattern")
|
||||
void testDiamondReferencePattern() {
|
||||
Map<String, Object> shared = new LinkedHashMap<>();
|
||||
shared.put("sharedValue", "shared");
|
||||
|
||||
Map<String, Object> left = new LinkedHashMap<>();
|
||||
left.put("name", "left");
|
||||
left.put("shared", shared);
|
||||
|
||||
Map<String, Object> right = new LinkedHashMap<>();
|
||||
right.put("name", "right");
|
||||
right.put("shared", shared); // Same reference
|
||||
|
||||
Map<String, Object> root = new LinkedHashMap<>();
|
||||
root.put("left", left);
|
||||
root.put("right", right);
|
||||
|
||||
byte[] serialized = Serializer.serialize(root);
|
||||
Map<?, ?> deserialized = (Map<?, ?>) Serializer.deserialize(serialized);
|
||||
|
||||
assertNotNull(deserialized);
|
||||
// Both left and right should reference the same shared object
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LIST ORDER PRESERVATION
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("List Order Preservation")
|
||||
class ListOrderPreservationTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("List order preserved after serialization [1,2,3]")
|
||||
void testListOrderPreserved() {
|
||||
List<Integer> original = Arrays.asList(1, 2, 3);
|
||||
|
||||
byte[] serialized = Serializer.serialize(original);
|
||||
List<?> deserialized = (List<?>) Serializer.deserialize(serialized);
|
||||
|
||||
assertEquals(1, deserialized.get(0));
|
||||
assertEquals(2, deserialized.get(1));
|
||||
assertEquals(3, deserialized.get(2));
|
||||
assertTrue(Comparator.compare(original, deserialized));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Comparison of [1,2,3] vs [2,3,1] after roundtrip should be FALSE")
|
||||
void testDifferentOrderListsNotEqual() {
|
||||
List<Integer> list1 = Arrays.asList(1, 2, 3);
|
||||
List<Integer> list2 = Arrays.asList(2, 3, 1);
|
||||
|
||||
byte[] serialized1 = Serializer.serialize(list1);
|
||||
byte[] serialized2 = Serializer.serialize(list2);
|
||||
|
||||
Object deserialized1 = Serializer.deserialize(serialized1);
|
||||
Object deserialized2 = Serializer.deserialize(serialized2);
|
||||
|
||||
assertFalse(Comparator.compare(deserialized1, deserialized2),
|
||||
"[1,2,3] and [2,3,1] should NOT be equal - order matters for Lists");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Set order does not matter - {1,2,3} vs {3,2,1} should be TRUE")
|
||||
void testSetOrderDoesNotMatter() {
|
||||
Set<Integer> set1 = new LinkedHashSet<>(Arrays.asList(1, 2, 3));
|
||||
Set<Integer> set2 = new LinkedHashSet<>(Arrays.asList(3, 2, 1));
|
||||
|
||||
byte[] serialized1 = Serializer.serialize(set1);
|
||||
byte[] serialized2 = Serializer.serialize(set2);
|
||||
|
||||
Object deserialized1 = Serializer.deserialize(serialized1);
|
||||
Object deserialized2 = Serializer.deserialize(serialized2);
|
||||
|
||||
assertTrue(Comparator.compare(deserialized1, deserialized2),
|
||||
"{1,2,3} and {3,2,1} should be equal - order doesn't matter for Sets");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("LinkedHashMap preserves insertion order")
|
||||
void testLinkedHashMapOrderPreserved() {
|
||||
Map<String, Integer> original = new LinkedHashMap<>();
|
||||
original.put("first", 1);
|
||||
original.put("second", 2);
|
||||
original.put("third", 3);
|
||||
|
||||
byte[] serialized = Serializer.serialize(original);
|
||||
Map<?, ?> deserialized = (Map<?, ?>) Serializer.deserialize(serialized);
|
||||
|
||||
List<String> keys = new ArrayList<>(((Map<String, ?>) deserialized).keySet());
|
||||
assertEquals("first", keys.get(0));
|
||||
assertEquals("second", keys.get(1));
|
||||
assertEquals("third", keys.get(2));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// REGRESSION TESTS
|
||||
// ============================================================
|
||||
|
||||
@Nested
|
||||
@DisplayName("Regression Tests")
|
||||
class RegressionTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Boolean wrapper roundtrip")
|
||||
void testBooleanWrapper() {
|
||||
Boolean trueVal = Boolean.TRUE;
|
||||
Boolean falseVal = Boolean.FALSE;
|
||||
|
||||
assertTrue(Comparator.compare(trueVal,
|
||||
Serializer.deserialize(Serializer.serialize(trueVal))));
|
||||
assertTrue(Comparator.compare(falseVal,
|
||||
Serializer.deserialize(Serializer.serialize(falseVal))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Character wrapper roundtrip")
|
||||
void testCharacterWrapper() {
|
||||
Character ch = 'X';
|
||||
|
||||
Object result = Serializer.deserialize(Serializer.serialize(ch));
|
||||
assertTrue(Comparator.compare(ch, result));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Empty string roundtrip")
|
||||
void testEmptyString() {
|
||||
String empty = "";
|
||||
|
||||
Object result = Serializer.deserialize(Serializer.serialize(empty));
|
||||
assertEquals("", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Unicode string roundtrip")
|
||||
void testUnicodeString() {
|
||||
String unicode = "Hello 世界 🌍 مرحبا";
|
||||
|
||||
Object result = Serializer.deserialize(Serializer.serialize(unicode));
|
||||
assertEquals(unicode, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue