Merge pull request #1302 from corda/feature/kat/evolvability

Add evolvability to the deserializer
This commit is contained in:
Katelyn Baker 2017-08-25 10:38:52 +01:00 committed by GitHub
commit 2b5e3e39a2
25 changed files with 654 additions and 47 deletions

View File

@ -0,0 +1,14 @@
package net.corda.core.serialization
/**
* This annotation is a marker to indicate which secondary constructors should be considered, and in which
* order, for evolving objects during their deserialisation.
*
* Versions will be considered in descending order, currently duplicate versions will result in
* non deterministic behaviour when deserialising objects
*/
@Target(AnnotationTarget.CONSTRUCTOR)
@Retention(AnnotationRetention.RUNTIME)
annotation class DeprecatedConstructorForDeserialization(val version: Int)

View File

@ -0,0 +1,130 @@
package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import net.corda.nodeapi.internal.serialization.carpenter.getTypeAsClass
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type
import java.io.NotSerializableException
import kotlin.reflect.KFunction
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.jvm.javaType
/**
* Serializer for deserialising objects whose definition has changed since they
* were serialised
*/
class EvolutionSerializer(
clazz: Type,
factory: SerializerFactory,
val readers: List<OldParam?>,
override val kotlinConstructor: KFunction<Any>?) : ObjectSerializer(clazz, factory) {
// explicitly set as empty to indicate it's unused by this type of serializer
override val propertySerializers: Collection<PropertySerializer> = emptyList()
/**
* Represents a parameter as would be passed to the constructor of the class as it was
* when it was serialised and NOT how that class appears now
*
* @param type The jvm type of the parameter
* @param idx where in the parameter list this parameter falls. Required as the parameter
* order may have been changed and we need to know where into the list to look
* @param property object to read the actual property value
*/
data class OldParam(val type: Type, val idx: Int, val property: PropertySerializer) {
fun readProperty(paramValues: List<*>, schema: Schema, input: DeserializationInput) =
property.readProperty(paramValues[idx], schema, input)
}
companion object {
/**
* Unlike the generic deserialisation case where we need to locate the primary constructor
* for the object (or our best guess) in the case of an object whose structure has changed
* since serialisation we need to attempt to locate a constructor that we can use. I.e.
* it's parameters match the serialised members and it will initialise any newly added
* elements
*
* TODO: Type evolution
* TODO: rename annotation
*/
internal fun getEvolverConstructor(type: Type, oldArgs: Map<String?, Type>): KFunction<Any>? {
val clazz: Class<*> = type.asClass()!!
if (!isConcrete(clazz)) return null
val oldArgumentSet = oldArgs.map { Pair(it.key, it.value) }
var maxConstructorVersion = Integer.MIN_VALUE
var constructor: KFunction<Any>? = null
clazz.kotlin.constructors.forEach {
val version = it.findAnnotation<DeprecatedConstructorForDeserialization>()?.version ?: Integer.MIN_VALUE
if (oldArgumentSet.containsAll(it.parameters.map { v -> Pair(v.name, v.type.javaType) }) &&
version > maxConstructorVersion) {
constructor = it
maxConstructorVersion = version
}
}
// if we didn't get an exact match revert to existing behaviour, if the new parameters
// are not mandatory (i.e. nullable) things are fine
return constructor ?: constructorForDeserialization(type)
}
/**
* Build a serialization object for deserialisation only of objects serialised
* as different versions of a class
*
* @param old is an object holding the schema that represents the object
* as it was serialised and the type descriptor of that type
* @param new is the Serializer built for the Class as it exists now, not
* how it was serialised and persisted.
*/
fun make(old: CompositeType, new: ObjectSerializer,
factory: SerializerFactory): AMQPSerializer<Any> {
val oldFieldToType = old.fields.map {
it.name as String? to it.getTypeAsClass(factory.classloader) as Type
}.toMap()
val constructor = getEvolverConstructor(new.type, oldFieldToType) ?:
throw NotSerializableException(
"Attempt to deserialize an interface: ${new.type}. Serialized form is invalid.")
val oldArgs = mutableMapOf<String, OldParam>()
var idx = 0
old.fields.forEach {
val returnType = it.getTypeAsClass(factory.classloader)
oldArgs[it.name] = OldParam(
returnType, idx++, PropertySerializer.make(it.name, null, returnType, factory))
}
val readers = constructor.parameters.map {
oldArgs[it.name!!] ?: if (!it.type.isMarkedNullable) {
throw NotSerializableException(
"New parameter ${it.name} is mandatory, should be nullable for evolution to worK")
} else null
}
return EvolutionSerializer(new.type, factory, readers, constructor)
}
}
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
throw IllegalAccessException("It should be impossible to write an evolution serializer")
}
/**
* Unlike a normal [readObject] call where we simply apply the parameter deserialisers
* to the object list of values we need to map that list, which is ordered per the
* constructor of the original state of the object, we need to map the new parameter order
* of the current constructor onto that list inserting nulls where new parameters are
* encountered
*
* TODO: Object references
*/
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
if (obj !is List<*>) throw NotSerializableException("Body of described type is unexpected $obj")
return construct(readers.map { it?.readProperty(obj, schema, input) })
}
}

View File

@ -4,23 +4,19 @@ import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion
import org.apache.qpid.proton.amqp.UnsignedInteger
import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException
import java.lang.reflect.Constructor
import java.lang.reflect.Type
import kotlin.reflect.jvm.javaConstructor
/**
* Responsible for serializing and deserializing a regular object instance via a series of properties (matched with a constructor).
*/
class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerializer<Any> {
open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerializer<Any> {
override val type: Type get() = clazz
private val javaConstructor: Constructor<Any>?
internal val propertySerializers: Collection<PropertySerializer>
open val kotlinConstructor = constructorForDeserialization(clazz)
val javaConstructor by lazy { kotlinConstructor?.javaConstructor }
init {
val kotlinConstructor = constructorForDeserialization(clazz)
javaConstructor = kotlinConstructor?.javaConstructor
javaConstructor?.isAccessible = true
propertySerializers = propertiesForSerialization(kotlinConstructor, clazz, factory)
open internal val propertySerializers: Collection<PropertySerializer> by lazy {
propertiesForSerialization(kotlinConstructor, clazz, factory)
}
private val typeName = nameForType(clazz)
@ -28,7 +24,7 @@ class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerial
override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}"
private val interfaces = interfacesForSerialization(clazz, factory) // We restrict to only those annotated or whitelisted
internal val typeNotation: TypeNotation = CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor, null), generateFields())
open internal val typeNotation : TypeNotation by lazy {CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor, null), generateFields()) }
override fun writeClassInfo(output: SerializationOutput) {
if (output.writeTypeNotations(typeNotation)) {
@ -68,15 +64,8 @@ class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerial
return propertySerializers.map { Field(it.name, it.type, it.requires, it.default, null, it.mandatory, false) }
}
private fun generateProvides(): List<String> {
return interfaces.map { nameForType(it) }
}
private fun generateProvides(): List<String> = interfaces.map { nameForType(it) }
fun construct(properties: List<Any?>): Any {
if (javaConstructor == null) {
fun construct(properties: List<Any?>) = javaConstructor?.newInstance(*properties.toTypedArray()) ?:
throw NotSerializableException("Attempt to deserialize an interface: $clazz. Serialized form is invalid.")
}
return javaConstructor.newInstance(*properties.toTypedArray())
}
}

View File

@ -10,7 +10,7 @@ import kotlin.reflect.jvm.javaGetter
/**
* Base class for serialization of a property of an object.
*/
sealed class PropertySerializer(val name: String, val readMethod: Method, val resolvedType: Type) {
sealed class PropertySerializer(val name: String, val readMethod: Method?, val resolvedType: Type) {
abstract fun writeClassInfo(output: SerializationOutput)
abstract fun writeProperty(obj: Any?, data: Data, output: SerializationOutput)
abstract fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any?
@ -44,7 +44,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re
}
private fun generateMandatory(): Boolean {
return isJVMPrimitive || !readMethod.returnsNullable()
return isJVMPrimitive || !(readMethod?.returnsNullable() ?: true)
}
private fun Method.returnsNullable(): Boolean {
@ -53,8 +53,8 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re
}
companion object {
fun make(name: String, readMethod: Method, resolvedType: Type, factory: SerializerFactory): PropertySerializer {
readMethod.isAccessible = true
fun make(name: String, readMethod: Method?, resolvedType: Type, factory: SerializerFactory): PropertySerializer {
readMethod?.isAccessible = true
if (SerializerFactory.isPrimitive(resolvedType)) {
return when(resolvedType) {
Char::class.java, Character::class.java -> AMQPCharPropertySerializer(name, readMethod)
@ -69,7 +69,10 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re
/**
* A property serializer for a complex type (another object).
*/
class DescribedTypePropertySerializer(name: String, readMethod: Method, resolvedType: Type, private val lazyTypeSerializer: () -> AMQPSerializer<*>) : PropertySerializer(name, readMethod, resolvedType) {
class DescribedTypePropertySerializer(
name: String, readMethod: Method?,
resolvedType: Type,
private val lazyTypeSerializer: () -> AMQPSerializer<*>) : PropertySerializer(name, readMethod, resolvedType) {
// This is lazy so we don't get an infinite loop when a method returns an instance of the class.
private val typeSerializer: AMQPSerializer<*> by lazy { lazyTypeSerializer() }
@ -84,14 +87,14 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re
}
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) {
output.writeObjectOrNull(readMethod.invoke(obj), data, resolvedType)
output.writeObjectOrNull(readMethod!!.invoke(obj), data, resolvedType)
}
}
/**
* A property serializer for most AMQP primitive type (Int, String, etc).
*/
class AMQPPrimitivePropertySerializer(name: String, readMethod: Method, resolvedType: Type) : PropertySerializer(name, readMethod, resolvedType) {
class AMQPPrimitivePropertySerializer(name: String, readMethod: Method?, resolvedType: Type) : PropertySerializer(name, readMethod, resolvedType) {
override fun writeClassInfo(output: SerializationOutput) {}
override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? {
@ -99,7 +102,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re
}
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) {
val value = readMethod.invoke(obj)
val value = readMethod!!.invoke(obj)
if (value is ByteArray) {
data.putObject(Binary(value))
} else {
@ -113,7 +116,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re
* value of the character is stored in numeric UTF-16 form and on deserialisation requires explicit
* casting back to a char otherwise it's treated as an Integer and a TypeMismatch occurs
*/
class AMQPCharPropertySerializer(name: String, readMethod: Method) :
class AMQPCharPropertySerializer(name: String, readMethod: Method?) :
PropertySerializer(name, readMethod, Character::class.java) {
override fun writeClassInfo(output: SerializationOutput) {}
@ -122,7 +125,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re
}
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) {
val input = readMethod.invoke(obj)
val input = readMethod!!.invoke(obj)
if (input != null) data.putShort((input as Char).toShort()) else data.putNull()
}
}

View File

@ -65,7 +65,7 @@ internal fun <T : Any> propertiesForSerialization(kotlinConstructor: KFunction<T
return if (kotlinConstructor != null) propertiesForSerializationFromConstructor(kotlinConstructor, type, factory) else propertiesForSerializationFromAbstract(clazz, type, factory)
}
private fun isConcrete(clazz: Class<*>): Boolean = !(clazz.isInterface || Modifier.isAbstract(clazz.modifiers))
fun isConcrete(clazz: Class<*>): Boolean = !(clazz.isInterface || Modifier.isAbstract(clazz.modifiers))
private fun <T : Any> propertiesForSerializationFromConstructor(kotlinConstructor: KFunction<T>, type: Type, factory: SerializerFactory): Collection<PropertySerializer> {
val clazz = (kotlinConstructor.returnType.classifier as KClass<*>).javaObjectType

View File

@ -16,6 +16,8 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import javax.annotation.concurrent.ThreadSafe
data class schemaAndDescriptor (val schema: Schema, val typeDescriptor: Any)
/**
* Factory of serializers designed to be shared across threads and invocations.
*/
@ -28,9 +30,6 @@ import javax.annotation.concurrent.ThreadSafe
// TODO: profile for performance in general
// TODO: use guava caches etc so not unbounded
// TODO: do we need to support a transient annotation to exclude certain properties?
// TODO: incorporate the class carpenter for classes not on the classpath.
// TODO: apply class loader logic and an "app context" throughout this code.
// TODO: schema evolution solution when the fingerprints do not line up.
// TODO: allow definition of well known types that are left out of the schema.
// TODO: generally map Object to '*' all over the place in the schema and make sure use of '*' amd '?' is consistent and documented in generics.
// TODO: found a document that states textual descriptors are Symbols. Adjust schema class appropriately.
@ -48,6 +47,12 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
val classloader : ClassLoader
get() = classCarpenter.classloader
fun getEvolutionSerializer(typeNotation: TypeNotation, newSerializer: ObjectSerializer) : AMQPSerializer<Any> {
return serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) {
EvolutionSerializer.make(typeNotation as CompositeType, newSerializer, this)
}
}
/**
* Look up, and manufacture if necessary, a serializer for the given type.
*
@ -155,9 +160,9 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
@Throws(NotSerializableException::class)
fun get(typeDescriptor: Any, schema: Schema): AMQPSerializer<Any> {
return serializersByDescriptor[typeDescriptor] ?: {
processSchema(schema)
serializersByDescriptor[typeDescriptor] ?:
throw NotSerializableException("Could not find type matching descriptor $typeDescriptor.")
processSchema(schemaAndDescriptor(schema, typeDescriptor))
serializersByDescriptor[typeDescriptor] ?: throw NotSerializableException(
"Could not find type matching descriptor $typeDescriptor.")
}()
}
@ -179,11 +184,18 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
* Iterate over an AMQP schema, for each type ascertain weather it's on ClassPath of [classloader] amd
* if not use the [ClassCarpenter] to generate a class to use in it's place
*/
private fun processSchema(schema: Schema, sentinel: Boolean = false) {
private fun processSchema(schema: schemaAndDescriptor, sentinel: Boolean = false) {
val carpenterSchemas = CarpenterSchemas.newInstance()
for (typeNotation in schema.types) {
for (typeNotation in schema.schema.types) {
try {
processSchemaEntry(typeNotation)
val serialiser = processSchemaEntry(typeNotation)
// if we just successfully built a serialiser for the type but the type fingerprint
// doesn't match that of the serialised object then we are dealing with different
// instance of the class, as such we need to build an EvolutionSerialiser
if (serialiser.typeDescriptor != typeNotation.descriptor.name) {
getEvolutionSerializer(typeNotation, serialiser as ObjectSerializer)
}
} catch (e: ClassNotFoundException) {
if (sentinel || (typeNotation !is CompositeType)) throw e
typeNotation.carpenterSchema(classloader, carpenterSchemas = carpenterSchemas)
@ -197,23 +209,21 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) {
}
}
private fun processSchemaEntry(typeNotation: TypeNotation) {
when (typeNotation) {
private fun processSchemaEntry(typeNotation: TypeNotation) = when (typeNotation) {
is CompositeType -> processCompositeType(typeNotation) // java.lang.Class (whether a class or interface)
is RestrictedType -> processRestrictedType(typeNotation) // Collection / Map, possibly with generics
}
}
private fun processRestrictedType(typeNotation: RestrictedType) {
private fun processRestrictedType(typeNotation: RestrictedType): AMQPSerializer<Any> {
// TODO: class loader logic, and compare the schema.
val type = typeForName(typeNotation.name, classloader)
get(null, type)
return get(null, type)
}
private fun processCompositeType(typeNotation: CompositeType) {
private fun processCompositeType(typeNotation: CompositeType): AMQPSerializer<Any> {
// TODO: class loader logic, and compare the schema.
val type = typeForName(typeNotation.name, classloader)
get(type.asClass() ?: throw NotSerializableException("Unable to build composite type for $type"), type)
return get(type.asClass() ?: throw NotSerializableException("Unable to build composite type for $type"), type)
}
private fun makeClassSerializer(clazz: Class<*>, type: Type, declaredType: Type): AMQPSerializer<Any> = serializersByType.computeIfAbsent(type) {

View File

@ -19,7 +19,7 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<T
val constructor = constructorForDeserialization(obj.javaClass)
val props = propertiesForSerialization(constructor, obj.javaClass, factory)
for (prop in props) {
extraProperties[prop.name] = prop.readMethod.invoke(obj)
extraProperties[prop.name] = prop.readMethod!!.invoke(obj)
}
} catch(e: NotSerializableException) {
}

View File

@ -0,0 +1,461 @@
package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import org.junit.Test
import java.io.File
import java.io.NotSerializableException
import kotlin.test.assertEquals
// To regenerate any of the binary test files do the following
//
// 1. Uncomment the code where the original form of the class is defined in the test
// 2. Comment out the rest of the test
// 3. Run the test
// 4. Using the printed path copy that file to the resources directory
// 5. Comment back out the generation code and uncomment the actual test
class EvolvabilityTests {
@Test
fun simpleOrderSwapSameType() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.simpleOrderSwapSameType")
val f = File(path.toURI())
val A = 1
val B = 2
// Original version of the class for the serialised version of this class
//
// data class C (val a: Int, val b: Int)
// val sc = SerializationOutput(sf).serialize(C(A, B))
// f.writeBytes(sc.bytes)
// new version of the class, in this case the order of the parameters has been swapped
data class C (val b: Int, val a: Int)
val sc2 = f.readBytes()
val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes<C>(sc2))
assertEquals(A, deserializedC.a)
assertEquals(B, deserializedC.b)
}
@Test
fun simpleOrderSwapDifferentType() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.simpleOrderSwapDifferentType")
val f = File(path.toURI())
val A = 1
val B = "two"
// Original version of the class as it was serialised
//
// data class C (val a: Int, val b: String)
// val sc = SerializationOutput(sf).serialize(C(A, B))
// f.writeBytes(sc.bytes)
// new version of the class, in this case the order of the parameters has been swapped
data class C (val b: String, val a: Int)
val sc2 = f.readBytes()
val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes<C>(sc2))
assertEquals(A, deserializedC.a)
assertEquals(B, deserializedC.b)
}
@Test
fun addAdditionalParamNotMandatory() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.addAdditionalParamNotMandatory")
val f = File(path.toURI())
val A = 1
// Original version of the class as it was serialised
//
// data class C(val a: Int)
// val sc = SerializationOutput(sf).serialize(C(A))
// f.writeBytes(sc.bytes)
// println ("Path = $path")
data class C (val a: Int, val b: Int?)
val sc2 = f.readBytes()
val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes<C>(sc2))
assertEquals (A, deserializedC.a)
assertEquals (null, deserializedC.b)
}
@Test(expected = NotSerializableException::class)
fun addAdditionalParam() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.addAdditionalParam")
val f = File(path.toURI())
@Suppress("UNUSED_VARIABLE")
val A = 1
// Original version of the class as it was serialised
//
// data class C(val a: Int)
// val sc = SerializationOutput(sf).serialize(C(A))
// f.writeBytes(sc.bytes)
// println ("Path = $path")
// new version of the class, in this case a new parameter has been added (b)
data class C (val a: Int, val b: Int)
val sc2 = f.readBytes()
// Expected to throw as we can't construct the new type as it contains a newly
// added parameter that isn't optional, i.e. not nullable and there isn't
// a constructor that takes the old parameters
DeserializationInput(sf).deserialize(SerializedBytes<C>(sc2))
}
@Suppress("UNUSED_VARIABLE")
@Test
fun removeParameters() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.removeParameters")
val f = File(path.toURI())
val A = 1
val B = "two"
val C = "three"
val D = 4
// Original version of the class as it was serialised
//
// data class CC(val a: Int, val b: String, val c: String, val d: Int)
// val scc = SerializationOutput(sf).serialize(CC(A, B, C, D))
// f.writeBytes(scc.bytes)
// println ("Path = $path")
data class CC (val b: String, val d: Int)
val sc2 = f.readBytes()
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
assertEquals (B, deserializedCC.b)
assertEquals (D, deserializedCC.d)
}
@Suppress("UNUSED_VARIABLE")
@Test
fun addAndRemoveParameters() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.addAndRemoveParameters")
val f = File(path.toURI())
val A = 1
val B = "two"
val C = "three"
val D = 4
val E = null
// Original version of the class as it was serialised
//
// data class CC(val a: Int, val b: String, val c: String, val d: Int)
// val scc = SerializationOutput(sf).serialize(CC(A, B, C, D))
// f.writeBytes(scc.bytes)
// println ("Path = $path")
data class CC(val a: Int, val e: Boolean?, val d: Int)
val sc2 = f.readBytes()
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
assertEquals(A, deserializedCC.a)
assertEquals(E, deserializedCC.e)
assertEquals(D, deserializedCC.d)
}
@Test
fun addMandatoryFieldWithAltConstructor() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.addMandatoryFieldWithAltConstructor")
val f = File(path.toURI())
val A = 1
// Original version of the class as it was serialised
//
// data class CC(val a: Int)
// val scc = SerializationOutput(sf).serialize(CC(A))
// f.writeBytes(scc.bytes)
// println ("Path = $path")
@Suppress("UNUSED")
data class CC (val a: Int, val b: String) {
@DeprecatedConstructorForDeserialization(1)
constructor (a: Int) : this (a, "hello")
}
val sc2 = f.readBytes()
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
assertEquals (A, deserializedCC.a)
assertEquals ("hello", deserializedCC.b)
}
@Test(expected = NotSerializableException::class)
@Suppress("UNUSED")
fun addMandatoryFieldWithAltConstructorUnAnnotated() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource(
"EvolvabilityTests.addMandatoryFieldWithAltConstructorUnAnnotated")
val f = File(path.toURI())
@Suppress("UNUSED_VARIABLE")
val A = 1
// Original version of the class as it was serialised
//
// data class CC(val a: Int)
// val scc = SerializationOutput(sf).serialize(CC(A))
// f.writeBytes(scc.bytes)
// println ("Path = $path")
data class CC (val a: Int, val b: String) {
// constructor annotation purposefully omitted
constructor (a: Int) : this (a, "hello")
}
// we expect this to throw as we should not find any constructors
// capable of dealing with this
DeserializationInput(sf).deserialize(SerializedBytes<CC>(f.readBytes()))
}
@Test
fun addMandatoryFieldWithAltReorderedConstructor() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource(
"EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor")
val f = File(path.toURI())
val A = 1
val B = 100
val C = "This is not a banana"
// Original version of the class as it was serialised
//
// data class CC(val a: Int, val b: Int, val c: String)
// val scc = SerializationOutput(sf).serialize(CC(A, B, C))
// f.writeBytes(scc.bytes)
// println ("Path = $path")
@Suppress("UNUSED")
data class CC (val a: Int, val b: Int, val c: String, val d: String) {
// ensure none of the original parameters align with the initial
// construction order
@DeprecatedConstructorForDeserialization(1)
constructor (c: String, a: Int, b: Int) : this (a, b, c, "wibble")
}
val sc2 = f.readBytes()
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
assertEquals (A, deserializedCC.a)
assertEquals (B, deserializedCC.b)
assertEquals (C, deserializedCC.c)
assertEquals ("wibble", deserializedCC.d)
}
@Test
fun addMandatoryFieldWithAltReorderedConstructorAndRemoval() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource(
"EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval")
val f = File(path.toURI())
val A = 1
@Suppress("UNUSED_VARIABLE")
val B = 100
val C = "This is not a banana"
// Original version of the class as it was serialised
//
// data class CC(val a: Int, val b: Int, val c: String)
// val scc = SerializationOutput(sf).serialize(CC(A, B, C))
// f.writeBytes(scc.bytes)
// println ("Path = $path")
// b is removed, d is added
data class CC (val a: Int, val c: String, val d: String) {
// ensure none of the original parameters align with the initial
// construction order
@Suppress("UNUSED")
@DeprecatedConstructorForDeserialization(1)
constructor (c: String, a: Int) : this (a, c, "wibble")
}
val sc2 = f.readBytes()
val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes<CC>(sc2))
assertEquals (A, deserializedCC.a)
assertEquals (C, deserializedCC.c)
assertEquals ("wibble", deserializedCC.d)
}
@Test
fun multiVersion() {
val sf = testDefaultFactory()
val path1 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersion.1")
val path2 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersion.2")
val path3 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersion.3")
@Suppress("UNUSED_VARIABLE")
val f = File(path1.toURI())
val a = 100
val b = 200
val c = 300
val d = 400
// Original version of the class as it was serialised
//
// Version 1:
// data class C (val a: Int, val b: Int)
// Version 2 - add param c
// data class C (val c: Int, val b: Int, val a: Int)
// Version 3 - add param d
// data class C (val b: Int, val c: Int, val d: Int, val a: Int)
//
// val scc = SerializationOutput(sf).serialize(C(b, c, d, a))
// f.writeBytes(scc.bytes)
// println ("Path = $path1")
@Suppress("UNUSED")
data class C (val e: Int, val c: Int, val b: Int, val a: Int, val d: Int) {
@DeprecatedConstructorForDeserialization(1)
constructor (b: Int, a: Int) : this (-1, -1, b, a, -1)
@DeprecatedConstructorForDeserialization(2)
constructor (a: Int, c: Int, b: Int) : this (-1, c, b, a, -1)
@DeprecatedConstructorForDeserialization(3)
constructor (a: Int, b: Int, c: Int, d: Int) : this (-1, c, b, a, d)
}
val sb1 = File(path1.toURI()).readBytes()
val db1 = DeserializationInput(sf).deserialize(SerializedBytes<C>(sb1))
assertEquals(a, db1.a)
assertEquals(b, db1.b)
assertEquals(-1, db1.c)
assertEquals(-1, db1.d)
assertEquals(-1, db1.e)
val sb2 = File(path2.toURI()).readBytes()
val db2 = DeserializationInput(sf).deserialize(SerializedBytes<C>(sb2))
assertEquals(a, db2.a)
assertEquals(b, db2.b)
assertEquals(c, db2.c)
assertEquals(-1, db2.d)
assertEquals(-1, db2.e)
val sb3 = File(path3.toURI()).readBytes()
val db3 = DeserializationInput(sf).deserialize(SerializedBytes<C>(sb3))
assertEquals(a, db3.a)
assertEquals(b, db3.b)
assertEquals(c, db3.c)
assertEquals(d, db3.d)
assertEquals(-1, db3.e)
}
@Test
fun changeSubType() {
val sf = testDefaultFactory()
val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.changeSubType")
val f = File(path.toURI())
val oa = 100
val ia = 200
// Original version of the class as it was serialised
//
// data class Inner (val a: Int)
// data class Outer (val a: Int, val b: Inner)
// val scc = SerializationOutput(sf).serialize(Outer(oa, Inner (ia)))
// f.writeBytes(scc.bytes)
// println ("Path = $path")
// Add a parameter to inner but keep outer unchanged
data class Inner (val a: Int, val b: String?)
data class Outer (val a: Int, val b: Inner)
val sc2 = f.readBytes()
val outer = DeserializationInput(sf).deserialize(SerializedBytes<Outer>(sc2))
assertEquals (oa, outer.a)
assertEquals (ia, outer.b.a)
assertEquals (null, outer.b.b)
}
@Test
fun multiVersionWithRemoval() {
val sf = testDefaultFactory()
val path1 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersionWithRemoval.1")
val path2 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersionWithRemoval.2")
val path3 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersionWithRemoval.3")
@Suppress("UNUSED_VARIABLE")
val a = 100
val b = 200
val c = 300
val d = 400
val e = 500
val f = 600
// Original version of the class as it was serialised
//
// Version 1:
// data class C (val a: Int, val b: Int, val c: Int)
// Version 2 - add param c
// data class C (val b: Int, val c: Int, val d: Int, val e: Int)
// Version 3 - add param d
// data class C (val b: Int, val c: Int, val d: Int, val e: Int, val f: Int)
//
// val scc = SerializationOutput(sf).serialize(C(b, c, d, e, f))
// File(path1.toURI()).writeBytes(scc.bytes)
// println ("Path = $path1")
@Suppress("UNUSED")
data class C (val b: Int, val c: Int, val d: Int, val e: Int, val f: Int, val g: Int) {
@DeprecatedConstructorForDeserialization(1)
constructor (b: Int, c: Int) : this (b, c, -1, -1, -1, -1)
@DeprecatedConstructorForDeserialization(2)
constructor (b: Int, c: Int, d: Int) : this (b, c, d, -1, -1, -1)
@DeprecatedConstructorForDeserialization(3)
constructor (b: Int, c: Int, d: Int, e: Int) : this (b, c, d, e, -1, -1)
@DeprecatedConstructorForDeserialization(4)
constructor (b: Int, c: Int, d: Int, e: Int, f: Int) : this (b, c, d, e, f, -1)
}
val sb1 = File(path1.toURI()).readBytes()
val db1 = DeserializationInput(sf).deserialize(SerializedBytes<C>(sb1))
assertEquals(b, db1.b)
assertEquals(c, db1.c)
assertEquals(-1, db1.d) // must not be set by calling constructor 2 by mistake
assertEquals(-1, db1.e)
assertEquals(-1, db1.f)
assertEquals(-1, db1.g)
val sb2 = File(path2.toURI()).readBytes()
val db2 = DeserializationInput(sf).deserialize(SerializedBytes<C>(sb2))
assertEquals(b, db2.b)
assertEquals(c, db2.c)
assertEquals(d, db2.d)
assertEquals(e, db2.e)
assertEquals(-1, db2.f)
assertEquals(-1, db1.g)
val sb3 = File(path3.toURI()).readBytes()
val db3 = DeserializationInput(sf).deserialize(SerializedBytes<C>(sb3))
assertEquals(b, db3.b)
assertEquals(c, db3.c)
assertEquals(d, db3.d)
assertEquals(e, db3.e)
assertEquals(f, db3.f)
assertEquals(-1, db3.g)
}
}