This commit is contained in:
HeshamHM28 2026-02-05 21:53:28 +02:00
parent 0c079494af
commit f681e221f5
13 changed files with 3877 additions and 2403 deletions

View file

@ -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());
}
/**

View file

@ -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;
}
}
}

View file

@ -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.
*

View file

@ -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);
}
}

View file

@ -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;
}
}
}

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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));
}
}

View file

@ -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;

View file

@ -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
}
}

View file

@ -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);
}
}
}