WIP in kryo

This commit is contained in:
HeshamHM28 2026-02-05 02:39:29 +02:00
parent 5c61000cec
commit 0c079494af
7 changed files with 2330 additions and 0 deletions

View file

@ -0,0 +1,118 @@
package com.codeflash;
import java.io.Serializable;
import java.util.Objects;
/**
* Placeholder for objects that could not be serialized.
*
* When KryoSerializer 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.
*
* This allows the rest of the object graph to be serialized while preserving
* information about what was lost. If code attempts to use the placeholder
* during replay tests, an error can be detected.
*/
public final class KryoPlaceholder implements Serializable {
private static final long serialVersionUID = 1L;
private static final int MAX_STR_LENGTH = 100;
private final String objType;
private final String objStr;
private final String errorMsg;
private final String path;
/**
* Create a placeholder for an unserializable object.
*
* @param objType The fully qualified class name of the original object
* @param objStr String representation of the object (may be truncated)
* @param errorMsg The error message explaining why serialization failed
* @param path The path in the object graph (e.g., "data.nested[0].socket")
*/
public KryoPlaceholder(String objType, String objStr, String errorMsg, String path) {
this.objType = objType;
this.objStr = truncate(objStr, MAX_STR_LENGTH);
this.errorMsg = errorMsg;
this.path = path;
}
/**
* Create a placeholder from an object and error.
*/
public static KryoPlaceholder create(Object obj, String errorMsg, String path) {
String objType = obj != null ? obj.getClass().getName() : "null";
String objStr = safeToString(obj);
return new KryoPlaceholder(objType, objStr, errorMsg, path);
}
private static String safeToString(Object obj) {
if (obj == null) {
return "null";
}
try {
return obj.toString();
} catch (Exception e) {
return "<toString failed: " + e.getMessage() + ">";
}
}
private static String truncate(String s, int maxLength) {
if (s == null) {
return null;
}
if (s.length() <= maxLength) {
return s;
}
return s.substring(0, maxLength) + "...";
}
/**
* Get the original type name of the unserializable object.
*/
public String getObjType() {
return objType;
}
/**
* Get the string representation of the original object (may be truncated).
*/
public String getObjStr() {
return objStr;
}
/**
* Get the error message explaining why serialization failed.
*/
public String getErrorMsg() {
return errorMsg;
}
/**
* Get the path in the object graph where this placeholder was created.
*/
public String getPath() {
return path;
}
@Override
public String toString() {
return String.format("<KryoPlaceholder[%s] at '%s': %s>", objType, path, objStr);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
KryoPlaceholder that = (KryoPlaceholder) o;
return Objects.equals(objType, that.objType) &&
Objects.equals(path, that.path);
}
@Override
public int hashCode() {
return Objects.hash(objType, path);
}
}

View file

@ -0,0 +1,40 @@
package com.codeflash;
/**
* Exception thrown when attempting to access or use a KryoPlaceholder.
*
* This exception indicates that code attempted to interact with an object
* that could not be serialized and was replaced with a placeholder. This
* typically means the test behavior cannot be verified for this code path.
*/
public class KryoPlaceholderAccessException extends RuntimeException {
private final String objType;
private final String path;
public KryoPlaceholderAccessException(String message, String objType, String path) {
super(message);
this.objType = objType;
this.path = path;
}
/**
* Get the original type name of the unserializable object.
*/
public String getObjType() {
return objType;
}
/**
* Get the path in the object graph where the placeholder was created.
*/
public String getPath() {
return path;
}
@Override
public String toString() {
return String.format("KryoPlaceholderAccessException[type=%s, path=%s]: %s",
objType, path, getMessage());
}
}

View file

@ -0,0 +1,490 @@
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

@ -0,0 +1,430 @@
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

@ -0,0 +1,179 @@
package com.codeflash;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests for KryoPlaceholder class.
*/
@DisplayName("KryoPlaceholder Tests")
class KryoPlaceholderTest {
@Nested
@DisplayName("Metadata Storage")
class MetadataTests {
@Test
@DisplayName("should store all metadata correctly")
void testMetadataStorage() {
KryoPlaceholder placeholder = new KryoPlaceholder(
"java.net.Socket",
"<socket instance>",
"Cannot serialize socket",
"data.connection.socket"
);
assertEquals("java.net.Socket", placeholder.getObjType());
assertEquals("<socket instance>", placeholder.getObjStr());
assertEquals("Cannot serialize socket", placeholder.getErrorMsg());
assertEquals("data.connection.socket", placeholder.getPath());
}
@Test
@DisplayName("should truncate long string representations")
void testStringTruncation() {
String longStr = "x".repeat(200);
KryoPlaceholder placeholder = new KryoPlaceholder(
"SomeType", longStr, "error", "path"
);
assertTrue(placeholder.getObjStr().length() <= 103); // 100 + "..."
assertTrue(placeholder.getObjStr().endsWith("..."));
}
@Test
@DisplayName("should handle null string representation")
void testNullStringRepresentation() {
KryoPlaceholder placeholder = new KryoPlaceholder(
"SomeType", null, "error", "path"
);
assertNull(placeholder.getObjStr());
}
}
@Nested
@DisplayName("Factory Method")
class FactoryTests {
@Test
@DisplayName("should create placeholder from object")
void testCreateFromObject() {
Object obj = new StringBuilder("test");
KryoPlaceholder placeholder = KryoPlaceholder.create(
obj, "Cannot serialize", "root"
);
assertEquals("java.lang.StringBuilder", placeholder.getObjType());
assertEquals("test", placeholder.getObjStr());
assertEquals("Cannot serialize", placeholder.getErrorMsg());
assertEquals("root", placeholder.getPath());
}
@Test
@DisplayName("should handle null object")
void testCreateFromNull() {
KryoPlaceholder placeholder = KryoPlaceholder.create(
null, "Null object", "path"
);
assertEquals("null", placeholder.getObjType());
assertEquals("null", placeholder.getObjStr());
}
@Test
@DisplayName("should handle object with failing toString")
void testCreateFromObjectWithBadToString() {
Object badObj = new Object() {
@Override
public String toString() {
throw new RuntimeException("toString failed!");
}
};
KryoPlaceholder placeholder = KryoPlaceholder.create(
badObj, "error", "path"
);
assertTrue(placeholder.getObjStr().contains("toString failed"));
}
}
@Nested
@DisplayName("Serialization")
class SerializationTests {
@Test
@DisplayName("placeholder should be serializable itself")
void testPlaceholderSerializable() {
KryoPlaceholder original = new KryoPlaceholder(
"java.net.Socket",
"<socket>",
"Cannot serialize socket",
"data.socket"
);
// Serialize and deserialize the placeholder
byte[] serialized = KryoSerializer.serialize(original);
assertNotNull(serialized);
assertTrue(serialized.length > 0);
Object deserialized = KryoSerializer.deserialize(serialized);
assertInstanceOf(KryoPlaceholder.class, deserialized);
KryoPlaceholder restored = (KryoPlaceholder) deserialized;
assertEquals(original.getObjType(), restored.getObjType());
assertEquals(original.getObjStr(), restored.getObjStr());
assertEquals(original.getErrorMsg(), restored.getErrorMsg());
assertEquals(original.getPath(), restored.getPath());
}
}
@Nested
@DisplayName("toString")
class ToStringTests {
@Test
@DisplayName("should produce readable toString")
void testToString() {
KryoPlaceholder placeholder = new KryoPlaceholder(
"java.net.Socket",
"<socket instance>",
"error",
"data.socket"
);
String str = placeholder.toString();
assertTrue(str.contains("KryoPlaceholder"));
assertTrue(str.contains("java.net.Socket"));
assertTrue(str.contains("data.socket"));
}
}
@Nested
@DisplayName("Equality")
class EqualityTests {
@Test
@DisplayName("placeholders with same type and path should be equal")
void testEquality() {
KryoPlaceholder p1 = new KryoPlaceholder("Type", "str1", "error1", "path");
KryoPlaceholder p2 = new KryoPlaceholder("Type", "str2", "error2", "path");
assertEquals(p1, p2);
assertEquals(p1.hashCode(), p2.hashCode());
}
@Test
@DisplayName("placeholders with different paths should not be equal")
void testInequality() {
KryoPlaceholder p1 = new KryoPlaceholder("Type", "str", "error", "path1");
KryoPlaceholder p2 = new KryoPlaceholder("Type", "str", "error", "path2");
assertNotEquals(p1, p2);
}
}
}

View file

@ -0,0 +1,567 @@
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,506 @@
package com.codeflash;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests for ObjectComparator.
*/
@DisplayName("ObjectComparator Tests")
class ObjectComparatorTest {
@Nested
@DisplayName("Primitive Comparison")
class PrimitiveTests {
@Test
@DisplayName("integers: exact match")
void testIntegers() {
assertTrue(ObjectComparator.compare(42, 42));
assertFalse(ObjectComparator.compare(42, 43));
}
@Test
@DisplayName("longs: exact match")
void testLongs() {
assertTrue(ObjectComparator.compare(Long.MAX_VALUE, Long.MAX_VALUE));
assertFalse(ObjectComparator.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));
// Outside epsilon - should not be equal
assertFalse(ObjectComparator.compare(1.0, 1.1));
assertFalse(ObjectComparator.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));
}
@Test
@DisplayName("NaN: should equal NaN")
void testNaN() {
assertTrue(ObjectComparator.compare(Double.NaN, Double.NaN));
assertTrue(ObjectComparator.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));
}
@Test
@DisplayName("booleans: exact match")
void testBooleans() {
assertTrue(ObjectComparator.compare(true, true));
assertTrue(ObjectComparator.compare(false, false));
assertFalse(ObjectComparator.compare(true, false));
}
@Test
@DisplayName("strings: exact match")
void testStrings() {
assertTrue(ObjectComparator.compare("hello", "hello"));
assertTrue(ObjectComparator.compare("", ""));
assertFalse(ObjectComparator.compare("hello", "world"));
}
@Test
@DisplayName("characters: exact match")
void testCharacters() {
assertTrue(ObjectComparator.compare('a', 'a'));
assertFalse(ObjectComparator.compare('a', 'b'));
}
}
@Nested
@DisplayName("Null Handling")
class NullTests {
@Test
@DisplayName("both null: should be equal")
void testBothNull() {
assertTrue(ObjectComparator.compare(null, null));
}
@Test
@DisplayName("one null: should not be equal")
void testOneNull() {
assertFalse(ObjectComparator.compare(null, "value"));
assertFalse(ObjectComparator.compare("value", null));
}
}
@Nested
@DisplayName("Collection Comparison")
class CollectionTests {
@Test
@DisplayName("lists: order matters")
void testLists() {
List<Integer> list1 = Arrays.asList(1, 2, 3);
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));
}
@Test
@DisplayName("lists: different sizes")
void testListsDifferentSizes() {
List<Integer> list1 = Arrays.asList(1, 2, 3);
List<Integer> list2 = Arrays.asList(1, 2);
assertFalse(ObjectComparator.compare(list1, list2));
}
@Test
@DisplayName("sets: order doesn't matter")
void testSets() {
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));
}
@Test
@DisplayName("sets: different contents")
void testSetsDifferentContents() {
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));
}
@Test
@DisplayName("empty collections: should be equal")
void testEmptyCollections() {
assertTrue(ObjectComparator.compare(new ArrayList<>(), new ArrayList<>()));
assertTrue(ObjectComparator.compare(new HashSet<>(), new HashSet<>()));
}
@Test
@DisplayName("nested collections")
void testNestedCollections() {
List<List<Integer>> nested1 = Arrays.asList(
Arrays.asList(1, 2),
Arrays.asList(3, 4)
);
List<List<Integer>> nested2 = Arrays.asList(
Arrays.asList(1, 2),
Arrays.asList(3, 4)
);
assertTrue(ObjectComparator.compare(nested1, nested2));
}
}
@Nested
@DisplayName("Map Comparison")
class MapTests {
@Test
@DisplayName("maps: same contents")
void testMaps() {
Map<String, Integer> map1 = new HashMap<>();
map1.put("one", 1);
map1.put("two", 2);
Map<String, Integer> map2 = new HashMap<>();
map2.put("two", 2);
map2.put("one", 1);
assertTrue(ObjectComparator.compare(map1, map2));
}
@Test
@DisplayName("maps: different values")
void testMapsDifferentValues() {
Map<String, Integer> map1 = Map.of("key", 1);
Map<String, Integer> map2 = Map.of("key", 2);
assertFalse(ObjectComparator.compare(map1, map2));
}
@Test
@DisplayName("maps: different keys")
void testMapsDifferentKeys() {
Map<String, Integer> map1 = Map.of("key1", 1);
Map<String, Integer> map2 = Map.of("key2", 1);
assertFalse(ObjectComparator.compare(map1, map2));
}
@Test
@DisplayName("maps: different sizes")
void testMapsDifferentSizes() {
Map<String, Integer> map1 = Map.of("one", 1, "two", 2);
Map<String, Integer> map2 = Map.of("one", 1);
assertFalse(ObjectComparator.compare(map1, map2));
}
@Test
@DisplayName("nested maps")
void testNestedMaps() {
Map<String, Object> map1 = new HashMap<>();
map1.put("inner", Map.of("key", "value"));
Map<String, Object> map2 = new HashMap<>();
map2.put("inner", Map.of("key", "value"));
assertTrue(ObjectComparator.compare(map1, map2));
}
}
@Nested
@DisplayName("Array Comparison")
class ArrayTests {
@Test
@DisplayName("int arrays: element-wise comparison")
void testIntArrays() {
int[] arr1 = {1, 2, 3};
int[] arr2 = {1, 2, 3};
int[] arr3 = {1, 2, 4};
assertTrue(ObjectComparator.compare(arr1, arr2));
assertFalse(ObjectComparator.compare(arr1, arr3));
}
@Test
@DisplayName("object arrays: element-wise comparison")
void testObjectArrays() {
String[] arr1 = {"a", "b", "c"};
String[] arr2 = {"a", "b", "c"};
assertTrue(ObjectComparator.compare(arr1, arr2));
}
@Test
@DisplayName("arrays: different lengths")
void testArraysDifferentLengths() {
int[] arr1 = {1, 2, 3};
int[] arr2 = {1, 2};
assertFalse(ObjectComparator.compare(arr1, arr2));
}
}
@Nested
@DisplayName("Exception Comparison")
class ExceptionTests {
@Test
@DisplayName("same exception type and message: equal")
void testSameException() {
Exception e1 = new IllegalArgumentException("test");
Exception e2 = new IllegalArgumentException("test");
assertTrue(ObjectComparator.compare(e1, e2));
}
@Test
@DisplayName("different exception types: not equal")
void testDifferentExceptionTypes() {
Exception e1 = new IllegalArgumentException("test");
Exception e2 = new IllegalStateException("test");
assertFalse(ObjectComparator.compare(e1, e2));
}
@Test
@DisplayName("different messages: not equal")
void testDifferentMessages() {
Exception e1 = new RuntimeException("message 1");
Exception e2 = new RuntimeException("message 2");
assertFalse(ObjectComparator.compare(e1, e2));
}
@Test
@DisplayName("both null messages: equal")
void testBothNullMessages() {
Exception e1 = new RuntimeException((String) null);
Exception e2 = new RuntimeException((String) null);
assertTrue(ObjectComparator.compare(e1, e2));
}
}
@Nested
@DisplayName("Placeholder Rejection")
class PlaceholderTests {
@Test
@DisplayName("original contains placeholder: throws exception")
void testOriginalPlaceholder() {
KryoPlaceholder placeholder = new KryoPlaceholder(
"java.net.Socket", "<socket>", "error", "path"
);
assertThrows(KryoPlaceholderAccessException.class, () -> {
ObjectComparator.compare(placeholder, "anything");
});
}
@Test
@DisplayName("new contains placeholder: throws exception")
void testNewPlaceholder() {
KryoPlaceholder placeholder = new KryoPlaceholder(
"java.net.Socket", "<socket>", "error", "path"
);
assertThrows(KryoPlaceholderAccessException.class, () -> {
ObjectComparator.compare("anything", placeholder);
});
}
@Test
@DisplayName("placeholder in nested structure: throws exception")
void testNestedPlaceholder() {
KryoPlaceholder placeholder = new KryoPlaceholder(
"java.net.Socket", "<socket>", "error", "data.socket"
);
Map<String, Object> map1 = new HashMap<>();
map1.put("socket", placeholder);
Map<String, Object> map2 = new HashMap<>();
map2.put("socket", "different");
assertThrows(KryoPlaceholderAccessException.class, () -> {
ObjectComparator.compare(map1, map2);
});
}
@Test
@DisplayName("compareWithDetails captures error message")
void testCompareWithDetails() {
KryoPlaceholder placeholder = new KryoPlaceholder(
"java.net.Socket", "<socket>", "error", "path"
);
ObjectComparator.ComparisonResult result =
ObjectComparator.compareWithDetails(placeholder, "anything");
assertFalse(result.isEqual());
assertTrue(result.hasError());
assertNotNull(result.getErrorMessage());
}
}
@Nested
@DisplayName("Custom Objects")
class CustomObjectTests {
@Test
@DisplayName("objects with same field values: equal")
void testSameFields() {
TestObj obj1 = new TestObj("name", 42);
TestObj obj2 = new TestObj("name", 42);
assertTrue(ObjectComparator.compare(obj1, obj2));
}
@Test
@DisplayName("objects with different field values: not equal")
void testDifferentFields() {
TestObj obj1 = new TestObj("name", 42);
TestObj obj2 = new TestObj("name", 43);
assertFalse(ObjectComparator.compare(obj1, obj2));
}
@Test
@DisplayName("nested objects")
void testNestedObjects() {
TestNested nested1 = new TestNested(new TestObj("inner", 1));
TestNested nested2 = new TestNested(new TestObj("inner", 1));
assertTrue(ObjectComparator.compare(nested1, nested2));
}
}
@Nested
@DisplayName("Type Compatibility")
class TypeCompatibilityTests {
@Test
@DisplayName("different list implementations: compatible")
void testDifferentListTypes() {
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));
}
@Test
@DisplayName("different map implementations: compatible")
void testDifferentMapTypes() {
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("key", 1);
Map<String, Integer> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("key", 1);
assertTrue(ObjectComparator.compare(hashMap, linkedHashMap));
}
@Test
@DisplayName("incompatible types: not equal")
void testIncompatibleTypes() {
assertFalse(ObjectComparator.compare("string", 42));
assertFalse(ObjectComparator.compare(new ArrayList<>(), new HashMap<>()));
}
}
@Nested
@DisplayName("Optional Comparison")
class OptionalTests {
@Test
@DisplayName("both empty: equal")
void testBothEmpty() {
assertTrue(ObjectComparator.compare(Optional.empty(), Optional.empty()));
}
@Test
@DisplayName("both present with same value: equal")
void testBothPresentSame() {
assertTrue(ObjectComparator.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()));
}
@Test
@DisplayName("both present with different values: not equal")
void testDifferentValues() {
assertFalse(ObjectComparator.compare(Optional.of("a"), Optional.of("b")));
}
}
@Nested
@DisplayName("Enum Comparison")
class EnumTests {
@Test
@DisplayName("same enum values: equal")
void testSameEnum() {
assertTrue(ObjectComparator.compare(TestEnum.A, TestEnum.A));
}
@Test
@DisplayName("different enum values: not equal")
void testDifferentEnum() {
assertFalse(ObjectComparator.compare(TestEnum.A, TestEnum.B));
}
}
// Test helper classes
static class TestObj {
String name;
int value;
TestObj(String name, int value) {
this.name = name;
this.value = value;
}
}
static class TestNested {
TestObj inner;
TestNested(TestObj inner) {
this.inner = inner;
}
}
enum TestEnum {
A, B, C
}
}