WIP in kryo
This commit is contained in:
parent
5c61000cec
commit
0c079494af
7 changed files with 2330 additions and 0 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue