Merge pull request #2154 from corda/kat/feature/enumEvolution2

Kat/feature/enum evolution2
This commit is contained in:
Katelyn Baker 2017-12-04 17:07:01 +00:00 committed by GitHub
commit ae3a3444f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 758 additions and 68 deletions

View File

@ -6,6 +6,12 @@ Here are release notes for each snapshot release from M9 onwards.
Unreleased
----------
* **Enum Class Evolution**
With the addition of AMQP serialization Corda now supports enum constant evolution.
That is the ability to alter an enum constant and, as long as certain rules are followed and the correct
annotations applied, have older and newer instances of that enumeration be understood.
Release 2.0
----------
Following quickly on the heels of the release of Corda 1.0, Corda version 2.0 consolidates

View File

@ -35,5 +35,5 @@ interface AMQPSerializer<out T> {
/**
* Read the given object from the input. The envelope is provided in case the schema is required.
*/
fun readObject(obj: Any, schema: SerializationSchemas, input: DeserializationInput): T
fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): T
}

View File

@ -0,0 +1,136 @@
package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.internal.uncheckedCast
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException
import java.lang.UnsupportedOperationException
import java.lang.reflect.Type
import java.util.*
/**
* Used whenever a deserialized enums fingerprint doesn't match the fingerprint of the generated
* serializer object. I.e. the deserializing code has a different version of the code either newer or
* older). The changes will have been documented using the transformation annotations, a copy of which
* are encoded as part of the AMQP envelope.
*
* This function ascertains which version of the enumeration is newer by comparing the length of the
* transformations list. Since transformation annotations should only ever be added, never removed even
* when seemingly unneeded (such as repeated renaming of a single constant), the longer list will dictate
* which is more up to date.
*
* The list of transforms come from two places, the class as it exists on the current class path and the
* class as it exists as it was serialized. In the case of the former we can build the list by using
* reflection on the class. In the case of the latter the transforms are retrieved from the AMQP envelope.
*
* With a set of transforms chosen we calculate the set of all possible constants, then using the
* transformation rules we create a mapping between those values and the values that exist on the
* current class
*
* @property clazz The enum as it exists now, not as it did when it was serialized (either in the past
* or future).
* @property factory the [SerializerFactory] that is building this serialization object.
* @property conversions A mapping between all potential enum constants that could've been assigned to
* an instance of the enum as it existed at time of serialisation and those that exist now
* @property ordinals Convenience mapping of constant to ordinality
*/
class EnumEvolutionSerializer(
override val type: Type,
factory: SerializerFactory,
private val conversions: Map<String, String>,
private val ordinals: Map<String, Int>) : AMQPSerializer<Any> {
override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")!!
companion object {
private fun MutableMap<String, String>.mapInPlace(f: (String) -> String) {
val i = iterator()
while (i.hasNext()) {
val curr = i.next()
curr.setValue(f(curr.value))
}
}
/**
* Builds an Enum Evolver serializer.
*
* @param old The description of the enum as it existed at the time of serialisation taken from the
* received AMQP header
* @param new The Serializer object we built based on the current state of the enum class on our classpath
* @param factory the [SerializerFactory] that is building this serialization object.
* @param transformsFromBlob the transforms attached to the class in the AMQP header, i.e. the transforms
* known at serialization time
*/
fun make(old: RestrictedType,
new: AMQPSerializer<Any>,
factory: SerializerFactory,
schemas: SerializationSchemas): AMQPSerializer<Any> {
val wireTransforms = schemas.transforms.types[old.name] ?: EnumMap<TransformTypes, MutableList<Transform>>(TransformTypes::class.java)
val localTransforms = TransformsSchema.get(old.name, factory)
// remember, the longer the list the newer we're assuming the transform set it as we assume
// evolution annotations are never removed, only added to
val transforms = if (wireTransforms.size > localTransforms.size) wireTransforms else localTransforms
// if either of these isn't of the cast type then something has gone terribly wrong
// elsewhere in the code
val defaultRules: List<EnumDefaultSchemaTransform>? = uncheckedCast(transforms[TransformTypes.EnumDefault])
val renameRules: List<RenameSchemaTransform>? = uncheckedCast(transforms[TransformTypes.Rename])
// What values exist on the enum as it exists on the class path
val localValues = new.type.asClass()!!.enumConstants.map { it.toString() }
val conversions: MutableMap<String, String> = localValues
.union(defaultRules?.map { it.new }?.toSet() ?: emptySet())
.union(renameRules?.map { it.to } ?: emptySet())
.associateBy({ it }, { it })
.toMutableMap()
val rules: MutableMap<String, String> = mutableMapOf()
rules.putAll(defaultRules?.associateBy({ it.new }, { it.old }) ?: emptyMap())
val renameRulesMap = renameRules?.associateBy({ it.to }, { it.from }) ?: emptyMap()
rules.putAll(renameRulesMap)
// take out set of all possible constants and build a map from those to the
// existing constants applying the rename and defaulting rules as defined
// in the schema
while (conversions.filterNot { it.value in localValues }.isNotEmpty()) {
conversions.mapInPlace { rules[it] ?: it }
}
// you'd think this was overkill to get access to the ordinal values for each constant but it's actually
// rather tricky when you don't have access to the actual type, so this is a nice way to be able
// to precompute and pass to the actual object
val ordinals = localValues.mapIndexed { i, s -> Pair(s, i) }.toMap()
// create a mapping between the ordinal value and the name as it was serialised converted
// to the name as it exists. We want to test any new constants have been added to the end
// of the enum class
val serialisedOrds = ((schemas.schema.types.find { it.name == old.name } as RestrictedType).choices
.associateBy ({ it.value.toInt() }, { conversions[it.name] }))
if (ordinals.filterNot { serialisedOrds[it.value] == it.key }.isNotEmpty()) {
throw NotSerializableException("Constants have been reordered, additions must be appended to the end")
}
return EnumEvolutionSerializer(new.type, factory, conversions, ordinals)
}
}
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any {
val enumName = (obj as List<*>)[0] as String
if (enumName !in conversions) {
throw NotSerializableException("No rule to evolve enum constant $type::$enumName")
}
return type.asClass()!!.enumConstants[ordinals[conversions[enumName]]!!]
}
override fun writeClassInfo(output: SerializationOutput) {
throw UnsupportedOperationException("It should be impossible to write an evolution serializer")
}
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
throw UnsupportedOperationException("It should be impossible to write an evolution serializer")
}
}

View File

@ -109,7 +109,7 @@ class EvolutionSerializer(
}
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
throw IllegalAccessException("It should be impossible to write an evolution serializer")
throw UnsupportedOperationException("It should be impossible to write an evolution serializer")
}
/**

View File

@ -46,11 +46,11 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
private fun getEvolutionSerializer(
typeNotation: TypeNotation,
newSerializer: AMQPSerializer<Any>,
transforms: TransformsSchema): AMQPSerializer<Any> {
schemas: SerializationSchemas): AMQPSerializer<Any> {
return serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) {
when (typeNotation) {
is CompositeType -> EvolutionSerializer.make(typeNotation, newSerializer as ObjectSerializer, this)
is RestrictedType -> throw NotSerializableException("Enum evolution is not currently supported")
is RestrictedType -> EnumEvolutionSerializer.make(typeNotation, newSerializer, this, schemas)
}
}
}
@ -210,7 +210,7 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
// 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, schemaAndDescriptor.schemas.transforms)
getEvolutionSerializer(typeNotation, serialiser, schemaAndDescriptor.schemas)
}
} catch (e: ClassNotFoundException) {
if (sentinel) throw e

View File

@ -1,5 +1,6 @@
package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.internal.uncheckedCast
import net.corda.core.serialization.CordaSerializationTransformEnumDefault
import net.corda.core.serialization.CordaSerializationTransformEnumDefaults
import net.corda.core.serialization.CordaSerializationTransformRename
@ -27,14 +28,72 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType
Unknown({ UnknownTransform() }) {
override fun getDescriptor(): Any = DESCRIPTOR
override fun getDescribed(): Any = ordinal
override fun validate(l : List<Transform>, constants: Map<String, Int>) { }
},
EnumDefault({ a -> EnumDefaultSchemaTransform((a as CordaSerializationTransformEnumDefault).old, a.new) }) {
override fun getDescriptor(): Any = DESCRIPTOR
override fun getDescribed(): Any = ordinal
/**
* Validates a list of constant additions to an enumerated type. To be valid a default (the value
* that should be used when we cannot use the new value) must refer to a constant that exists in the
* enum class as it exists now and it cannot refer to itself.
*
* @param l The list of transforms representing new constants and the mapping from that constant to an
* existing value
* @param constants The list of enum constants on the type the transforms are being applied to
*/
override fun validate(list : List<Transform>, constants: Map<String, Int>) {
uncheckedCast<List<Transform>, List<EnumDefaultSchemaTransform>>(list).forEach {
if (!constants.contains(it.new)) {
throw NotSerializableException("Unknown enum constant ${it.new}")
}
if (!constants.contains(it.old)) {
throw NotSerializableException(
"Enum extension defaults must be to a valid constant: ${it.new} -> ${it.old}. ${it.old} " +
"doesn't exist in constant set $constants")
}
if (it.old == it.new) {
throw NotSerializableException("Enum extension ${it.new} cannot default to itself")
}
if (constants[it.old]!! >= constants[it.new]!!) {
throw NotSerializableException(
"Enum extensions must default to older constants. ${it.new}[${constants[it.new]}] " +
"defaults to ${it.old}[${constants[it.old]}] which is greater")
}
}
}
},
Rename({ a -> RenameSchemaTransform((a as CordaSerializationTransformRename).from, a.to) }) {
override fun getDescriptor(): Any = DESCRIPTOR
override fun getDescribed(): Any = ordinal
/**
* Validates a list of rename transforms is valid. Such a list isn't valid if we detect a cyclic chain,
* that is a constant is renamed to something that used to exist in the enum. We do this for both
* the same constant (i.e. C -> D -> C) and multiple constants (C->D, B->C)
*
* @param l The list of transforms representing the renamed constants and the mapping between their new
* and old values
* @param constants The list of enum constants on the type the transforms are being applied to
*/
override fun validate(l : List<Transform>, constants: Map<String, Int>) {
object : Any() {
val from : MutableSet<String> = mutableSetOf()
val to : MutableSet<String> = mutableSetOf() }.apply {
@Suppress("UNCHECKED_CAST") (l as List<RenameSchemaTransform>).forEach { rename ->
if (rename.to in this.to || rename.from in this.from) {
throw NotSerializableException("Cyclic renames are not allowed (${rename.to})")
}
this.to.add(rename.from)
this.from.add(rename.to)
}
}
}
}
// Transform used to test the unknown handler, leave this at as the final constant, uncomment
// when regenerating test cases - if Java had a pre-processor this would be much neater
@ -45,6 +104,8 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType
//}
;
abstract fun validate(l: List<Transform>, constants: Map<String, Int>)
companion object : DescribedTypeConstructor<TransformTypes> {
val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_ELEMENT_KEY.amqpDescriptor

View File

@ -146,8 +146,8 @@ class EnumDefaultSchemaTransform(val old: String, val new: String) : Transform()
/**
* Transform applied to either a class or enum where a property is renamed
*
* @property from the name at time of change of the property
* @property to the new name of the property
* @property from the name of the property or constant prior to being changed, i.e. what it was
* @property to the new name of the property or constant after the change has been made, i.e. what it is now
*/
class RenameSchemaTransform(val from: String, val to: String) : Transform() {
companion object : DescribedTypeConstructor<RenameSchemaTransform> {
@ -192,6 +192,61 @@ data class TransformsSchema(val types: Map<String, EnumMap<TransformTypes, Mutab
companion object : DescribedTypeConstructor<TransformsSchema> {
val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_SCHEMA.amqpDescriptor
/**
* Takes a class name and either returns a cached instance of the TransformSet for it or, on a cache miss,
* instantiates the transform set before inserting into the cache and returning it.
*
* @param name fully qualified class name to lookup transforms for
* @param sf the [SerializerFactory] building this transform set. Needed as each can define it's own
* class loader and this dictates which classes we can and cannot see
*/
fun get(name: String, sf: SerializerFactory) = sf.transformsCache.computeIfAbsent(name) {
val transforms = EnumMap<TransformTypes, MutableList<Transform>>(TransformTypes::class.java)
try {
val clazz = sf.classloader.loadClass(name)
supportedTransforms.forEach { transform ->
clazz.getAnnotation(transform.type)?.let { list ->
transform.getAnnotations(list).forEach { annotation ->
val t = transform.enum.build(annotation)
// we're explicitly rejecting repeated annotations, whilst it's fine and we'd just
// ignore them it feels like a good thing to alert the user to since this is
// more than likely a typo in their code so best make it an actual error
if (transforms.computeIfAbsent(transform.enum) { mutableListOf() }
.filter { t == it }
.isNotEmpty()) {
throw NotSerializableException(
"Repeated unique transformation annotation of type ${t.name}")
}
transforms[transform.enum]!!.add(t)
}
transform.enum.validate(
transforms[transform.enum] ?: emptyList(),
clazz.enumConstants.mapIndexed { i, s -> Pair(s.toString(), i) }.toMap())
}
}
} catch (_: ClassNotFoundException) {
// if we can't load the class we'll end up caching an empty list which is fine as that
// list, on lookup, won't be included in the schema because it's empty
}
transforms
}
private fun getAndAdd(
type: String,
sf: SerializerFactory,
map: MutableMap<String, EnumMap<TransformTypes, MutableList<Transform>>>) {
get(type, sf).apply {
if (isNotEmpty()) {
map[type] = this
}
}
}
/**
* Prepare a schema for encoding, takes all of the types being transmitted and inspects each
* one for any transform annotations. If there are any build up a set that can be
@ -200,48 +255,10 @@ data class TransformsSchema(val types: Map<String, EnumMap<TransformTypes, Mutab
* @param schema should be a [Schema] generated for a serialised data structure
* @param sf should be provided by the same serialization context that generated the schema
*/
fun build(schema: Schema, sf: SerializerFactory): TransformsSchema {
val rtn = mutableMapOf<String, EnumMap<TransformTypes, MutableList<Transform>>>()
schema.types.forEach { type ->
sf.transformsCache.computeIfAbsent(type.name) {
val transforms = EnumMap<TransformTypes, MutableList<Transform>>(TransformTypes::class.java)
try {
val clazz = sf.classloader.loadClass(type.name)
supportedTransforms.forEach { transform ->
clazz.getAnnotation(transform.type)?.let { list ->
transform.getAnnotations(list).forEach { annotation ->
val t = transform.enum.build(annotation)
// we're explicitly rejecting repeated annotations, whilst it's fine and we'd just
// ignore them it feels like a good thing to alert the user to since this is
// more than likely a typo in their code so best make it an actual error
if (transforms.computeIfAbsent(transform.enum) { mutableListOf() }
.filter { t == it }.isNotEmpty()) {
throw NotSerializableException(
"Repeated unique transformation annotation of type ${t.name}")
}
transforms[transform.enum]!!.add(t)
}
}
}
} catch (_: ClassNotFoundException) {
// if we can't load the class we'll end up caching an empty list which is fine as that
// list, on lookup, won't be included in the schema because it's empty
}
transforms
}.apply {
if (isNotEmpty()) {
rtn[type.name] = this
}
}
}
return TransformsSchema(rtn)
}
fun build(schema: Schema, sf: SerializerFactory) = TransformsSchema(
mutableMapOf<String, EnumMap<TransformTypes, MutableList<Transform>>>().apply {
schema.types.forEach { type -> getAndAdd(type.name, sf, this) }
})
override fun getTypeClass(): Class<*> = TransformsSchema::class.java
@ -286,6 +303,7 @@ data class TransformsSchema(val types: Map<String, EnumMap<TransformTypes, Mutab
override fun getDescribed(): Any = types
@Suppress("NAME_SHADOWING")
override fun toString(): String {
data class Indent(val indent: String) {
@Suppress("UNUSED") constructor(i: Indent) : this(" ${i.indent}")

View File

@ -1,6 +1,7 @@
package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.serialization.*
import net.corda.testing.common.internal.ProjectStructure.projectRootDir
import org.assertj.core.api.Assertions
import org.junit.Test
import java.io.File
@ -10,8 +11,9 @@ import kotlin.test.assertEquals
import kotlin.test.assertTrue
class EnumEvolvabilityTests {
var localPath = "file:///home/katelyn/srcs/corda/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp"
@Suppress("UNUSED")
var localPath = projectRootDir.toUri().resolve(
"node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp")
companion object {
val VERBOSE = false
@ -21,11 +23,6 @@ class EnumEvolvabilityTests {
A, B, C, D
}
@CordaSerializationTransformEnumDefaults()
enum class MissingDefaults {
A, B, C, D
}
@CordaSerializationTransformRenames()
enum class MissingRenames {
A, B, C, D
@ -48,13 +45,6 @@ class EnumEvolvabilityTests {
A, B, C, E
}
@CordaSerializationTransformRenames(
CordaSerializationTransformRename("E", "C"),
CordaSerializationTransformRename("F", "D"))
enum class RenameEnumTwice {
A, B, E, F
}
@Test
fun noAnnotation() {
data class C (val n: NotAnnotated)
@ -66,6 +56,11 @@ class EnumEvolvabilityTests {
assertEquals(0, bAndS.transformsSchema.types.size)
}
@CordaSerializationTransformEnumDefaults()
enum class MissingDefaults {
A, B, C, D
}
@Test
fun missingDefaults() {
data class C (val m: MissingDefaults)
@ -228,6 +223,13 @@ class EnumEvolvabilityTests {
assertEquals("E", (deserialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).to)
}
@CordaSerializationTransformRenames(
CordaSerializationTransformRename("E", "C"),
CordaSerializationTransformRename("F", "D"))
enum class RenameEnumTwice {
A, B, E, F
}
@Test
fun doubleRenameAnnotationIsAdded() {
data class C (val annotatedEnum: RenameEnumTwice)
@ -433,4 +435,98 @@ class EnumEvolvabilityTests {
assertTrue(envelope.transformsSchema.types.containsKey(WithUnknownTest::class.java.name))
assertTrue(envelope.transformsSchema.types[WithUnknownTest::class.java.name]!!.containsKey(TransformTypes.Unknown))
}
//
// In this example we will have attempted to rename D back to C
//
// The life cycle of the class would've looked like this
//
// 1. enum class RejectCyclicRename { A, B, C }
// 2. enum class RejectCyclicRename { A, B, D }
// 3. enum class RejectCyclicRename { A, B, C }
//
// And we're not at 3. However, we ban this rename
//
@CordaSerializationTransformRenames (
CordaSerializationTransformRename("D", "C"),
CordaSerializationTransformRename("C", "D")
)
enum class RejectCyclicRename { A, B, C }
@Test
fun rejectCyclicRename() {
data class C (val e: RejectCyclicRename)
val sf = testDefaultFactory()
Assertions.assertThatThrownBy {
SerializationOutput(sf).serialize(C(RejectCyclicRename.A))
}.isInstanceOf(NotSerializableException::class.java)
}
//
// In this test, like the above, we're looking to ensure repeated renames are rejected as
// unserailzble. However, in this case, it isn't a struct cycle, rather one element
// is renamed to match what a different element used to be called
//
@CordaSerializationTransformRenames (
CordaSerializationTransformRename(from = "B", to = "C"),
CordaSerializationTransformRename(from = "C", to = "D")
)
enum class RejectCyclicRenameAlt { A, C, D }
@Test
fun rejectCyclicRenameAlt() {
data class C (val e: RejectCyclicRenameAlt)
val sf = testDefaultFactory()
Assertions.assertThatThrownBy {
SerializationOutput(sf).serialize(C(RejectCyclicRenameAlt.A))
}.isInstanceOf(NotSerializableException::class.java)
}
@CordaSerializationTransformRenames (
CordaSerializationTransformRename("G", "C"),
CordaSerializationTransformRename("F", "G"),
CordaSerializationTransformRename("E", "F"),
CordaSerializationTransformRename("D", "E"),
CordaSerializationTransformRename("C", "D")
)
enum class RejectCyclicRenameRedux { A, B, C }
@Test
fun rejectCyclicRenameRedux() {
data class C (val e: RejectCyclicRenameRedux)
val sf = testDefaultFactory()
Assertions.assertThatThrownBy {
SerializationOutput(sf).serialize(C(RejectCyclicRenameRedux.A))
}.isInstanceOf(NotSerializableException::class.java)
}
@CordaSerializationTransformEnumDefault (new = "D", old = "X")
enum class RejectBadDefault { A, B, C, D }
@Test
fun rejectBadDefault() {
data class C (val e: RejectBadDefault)
val sf = testDefaultFactory()
Assertions.assertThatThrownBy {
SerializationOutput(sf).serialize(C(RejectBadDefault.D))
}.isInstanceOf(NotSerializableException::class.java)
}
@CordaSerializationTransformEnumDefault (new = "D", old = "D")
enum class RejectBadDefaultToSelf { A, B, C, D }
@Test
fun rejectBadDefaultToSelf() {
data class C (val e: RejectBadDefaultToSelf)
val sf = testDefaultFactory()
Assertions.assertThatThrownBy {
SerializationOutput(sf).serialize(C(RejectBadDefaultToSelf.D))
}.isInstanceOf(NotSerializableException::class.java)
}
}

View File

@ -1,18 +1,19 @@
package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.serialization.CordaSerializationTransformEnumDefault
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.*
import net.corda.testing.common.internal.ProjectStructure.projectRootDir
import org.assertj.core.api.Assertions
import org.junit.Test
import java.io.File
import java.io.NotSerializableException
import java.net.URI
import kotlin.test.assertEquals
// NOTE: To recreate the test files used by these tests uncomment the original test classes and comment
// the new ones out, then change each test to write out the serialized bytes rather than read
// the file.
class EnumEvolveTests {
@Suppress("UNUSED")
var localPath = projectRootDir.toUri().resolve(
"node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp")
@ -26,7 +27,7 @@ class EnumEvolveTests {
@Test
fun deserialiseNewerSetToUnknown() {
val resource = "${this.javaClass.simpleName}.${testName()}"
val resource = "${javaClass.simpleName}.${testName()}"
val sf = testDefaultFactory()
data class C (val e : DeserializeNewerSetToUnknown)
@ -35,9 +36,379 @@ class EnumEvolveTests {
// File(URI("$localPath/$resource")).writeBytes(
// SerializationOutput(sf).serialize(C(DeserializeNewerSetToUnknown.D)).bytes)
val path = EvolvabilityTests::class.java.getResource(resource)
val obj = DeserializationInput(sf).deserialize(SerializedBytes<C>(File(path.toURI()).readBytes()))
assertEquals (DeserializeNewerSetToUnknown.C, obj.e)
}
// Version of the class as it was serialised
//
// @CordaSerializationTransformEnumDefaults (
// CordaSerializationTransformEnumDefault("D", "C"),
// CordaSerializationTransformEnumDefault("E", "D"))
// enum class DeserializeNewerSetToUnknown2 { A, B, C, D, E }
//
// Version of the class as it's used in the test
enum class DeserializeNewerSetToUnknown2 { A, B, C }
@Test
fun deserialiseNewerSetToUnknown2() {
val resource = "${javaClass.simpleName}.${testName()}"
val sf = testDefaultFactory()
data class C(val e: DeserializeNewerSetToUnknown2)
// Uncomment to re-generate test files
// val so = SerializationOutput(sf)
// File(URI("$localPath/$resource.C")).writeBytes(so.serialize(C(DeserializeNewerSetToUnknown2.C)).bytes)
// File(URI("$localPath/$resource.D")).writeBytes(so.serialize(C(DeserializeNewerSetToUnknown2.D)).bytes)
// File(URI("$localPath/$resource.E")).writeBytes(so.serialize(C(DeserializeNewerSetToUnknown2.E)).bytes)
val path1 = EvolvabilityTests::class.java.getResource("$resource.C")
val path2 = EvolvabilityTests::class.java.getResource("$resource.D")
val path3 = EvolvabilityTests::class.java.getResource("$resource.E")
// C will just work
val obj1 = DeserializationInput(sf).deserialize(SerializedBytes<C>(File(path1.toURI()).readBytes()))
// D will transform directly to C
val obj2 = DeserializationInput(sf).deserialize(SerializedBytes<C>(File(path2.toURI()).readBytes()))
// E will have to transform from E -> D -> C to work, so this should exercise that part
// of the evolution code
val obj3 = DeserializationInput(sf).deserialize(SerializedBytes<C>(File(path3.toURI()).readBytes()))
assertEquals (DeserializeNewerSetToUnknown2.C, obj1.e)
assertEquals (DeserializeNewerSetToUnknown2.C, obj2.e)
assertEquals (DeserializeNewerSetToUnknown2.C, obj3.e)
}
// Version of the class as it was serialised, evolve rule purposfuly not included to
// test failure conditions
//
// enum class DeserializeNewerWithNoRule { A, B, C, D }
//
// Class as it exists for the test
enum class DeserializeNewerWithNoRule { A, B, C }
// Lets test to see if they forgot to provide an upgrade rule
@Test
fun deserialiseNewerWithNoRule() {
val resource = "${javaClass.simpleName}.${testName()}"
val sf = testDefaultFactory()
data class C(val e: DeserializeNewerWithNoRule)
// Uncomment to re-generate test files
// val so = SerializationOutput(sf)
// File(URI("$localPath/$resource")).writeBytes(so.serialize(C(DeserializeNewerWithNoRule.D)).bytes)
val path = EvolvabilityTests::class.java.getResource(resource)
Assertions.assertThatThrownBy {
DeserializationInput(sf).deserialize(SerializedBytes<C>(File(path.toURI()).readBytes()))
}.isInstanceOf(NotSerializableException::class.java)
}
// Version of class as it was serialized, at some point in the "future" several
// values have been renamed
//
// First Change
// A -> AA
// @CordaSerializationTransformRenames (
// CordaSerializationTransformRename(from ="A", to = "AA")
// )
// enum class DeserializeWithRename { AA, B, C }
//
// Second Change
// B -> BB
// @CordaSerializationTransformRenames (
// CordaSerializationTransformRename(from = "B", to = "BB"),
// CordaSerializationTransformRename(from = "A", to = "AA")
// )
// enum class DeserializeWithRename { AA, BB, C }
//
// Third Change
// BB -> XX
// @CordaSerializationTransformRenames (
// CordaSerializationTransformRename(from = "B", to = "BB"),
// CordaSerializationTransformRename(from = "BB", to = "XX"),
// CordaSerializationTransformRename(from = "A", to = "AA")
// )
// enum class DeserializeWithRename { AA, XX, C }
//
// Finally, the version we're using to test with
enum class DeserializeWithRename { A, B, C }
@Test
fun deserializeWithRename() {
val resource = "${javaClass.simpleName}.${testName()}"
val sf = testDefaultFactory()
data class C(val e: DeserializeWithRename)
// Uncomment to re-generate test files, needs to be done in three stages
val so = SerializationOutput(sf)
// First change
// File(URI("$localPath/$resource.1.AA")).writeBytes(so.serialize(C(DeserializeWithRename.AA)).bytes)
// File(URI("$localPath/$resource.1.B")).writeBytes(so.serialize(C(DeserializeWithRename.B)).bytes)
// File(URI("$localPath/$resource.1.C")).writeBytes(so.serialize(C(DeserializeWithRename.C)).bytes)
// Second change
// File(URI("$localPath/$resource.2.AA")).writeBytes(so.serialize(C(DeserializeWithRename.AA)).bytes)
// File(URI("$localPath/$resource.2.BB")).writeBytes(so.serialize(C(DeserializeWithRename.BB)).bytes)
// File(URI("$localPath/$resource.2.C")).writeBytes(so.serialize(C(DeserializeWithRename.C)).bytes)
// Third change
// File(URI("$localPath/$resource.3.AA")).writeBytes(so.serialize(C(DeserializeWithRename.AA)).bytes)
// File(URI("$localPath/$resource.3.XX")).writeBytes(so.serialize(C(DeserializeWithRename.XX)).bytes)
// File(URI("$localPath/$resource.3.C")).writeBytes(so.serialize(C(DeserializeWithRename.C)).bytes)
//
// Test we can deserialize instances of the class after its first transformation
//
val path1_AA = EvolvabilityTests::class.java.getResource("$resource.1.AA")
val path1_B = EvolvabilityTests::class.java.getResource("$resource.1.B")
val path1_C = EvolvabilityTests::class.java.getResource("$resource.1.C")
val obj1_AA = DeserializationInput(sf).deserialize(SerializedBytes<C>(File(path1_AA.toURI()).readBytes()))
val obj1_B = DeserializationInput(sf).deserialize(SerializedBytes<C>(File(path1_B.toURI()).readBytes()))
val obj1_C = DeserializationInput(sf).deserialize(SerializedBytes<C>(File(path1_C.toURI()).readBytes()))
assertEquals(DeserializeWithRename.A, obj1_AA.e)
assertEquals(DeserializeWithRename.B, obj1_B.e)
assertEquals(DeserializeWithRename.C, obj1_C.e)
//
// Test we can deserialize instances of the class after its second transformation
//
val path2_AA = EvolvabilityTests::class.java.getResource("$resource.2.AA")
val path2_BB = EvolvabilityTests::class.java.getResource("$resource.2.BB")
val path2_C = EvolvabilityTests::class.java.getResource("$resource.2.C")
val obj2_AA = DeserializationInput(sf).deserialize(SerializedBytes<C>(File(path2_AA.toURI()).readBytes()))
val obj2_BB = DeserializationInput(sf).deserialize(SerializedBytes<C>(File(path2_BB.toURI()).readBytes()))
val obj2_C = DeserializationInput(sf).deserialize(SerializedBytes<C>(File(path2_C.toURI()).readBytes()))
assertEquals(DeserializeWithRename.A, obj2_AA.e)
assertEquals(DeserializeWithRename.B, obj2_BB.e)
assertEquals(DeserializeWithRename.C, obj2_C.e)
//
// Test we can deserialize instances of the class after its third transformation
//
val path3_AA = EvolvabilityTests::class.java.getResource("$resource.3.AA")
val path3_XX = EvolvabilityTests::class.java.getResource("$resource.3.XX")
val path3_C = EvolvabilityTests::class.java.getResource("$resource.3.C")
val obj3_AA = DeserializationInput(sf).deserialize(SerializedBytes<C>(File(path3_AA.toURI()).readBytes()))
val obj3_XX = DeserializationInput(sf).deserialize(SerializedBytes<C>(File(path3_XX.toURI()).readBytes()))
val obj3_C = DeserializationInput(sf).deserialize(SerializedBytes<C>(File(path3_C.toURI()).readBytes()))
assertEquals(DeserializeWithRename.A, obj3_AA.e)
assertEquals(DeserializeWithRename.B, obj3_XX.e)
assertEquals(DeserializeWithRename.C, obj3_C.e)
}
// The origional version of the enum, what we'll be eventually deserialising into
// enum class MultiOperations { A, B, C }
//
// First alteration, add D
// @CordaSerializationTransformEnumDefault(old = "C", new = "D")
// enum class MultiOperations { A, B, C, D }
//
// Second, add E
// @CordaSerializationTransformEnumDefaults(
// CordaSerializationTransformEnumDefault(old = "C", new = "D"),
// CordaSerializationTransformEnumDefault(old = "D", new = "E")
// )
// enum class MultiOperations { A, B, C, D, E }
//
// Third, Rename E to BOB
// @CordaSerializationTransformEnumDefaults(
// CordaSerializationTransformEnumDefault(old = "C", new = "D"),
// CordaSerializationTransformEnumDefault(old = "D", new = "E")
// )
// @CordaSerializationTransformRename(to = "BOB", from = "E")
// enum class MultiOperations { A, B, C, D, BOB }
//
// Fourth, Rename C to CAT, ADD F and G
// @CordaSerializationTransformEnumDefaults(
// CordaSerializationTransformEnumDefault(old = "F", new = "G"),
// CordaSerializationTransformEnumDefault(old = "BOB", new = "F"),
// CordaSerializationTransformEnumDefault(old = "D", new = "E"),
// CordaSerializationTransformEnumDefault(old = "C", new = "D")
// )
// @CordaSerializationTransformRenames (
// CordaSerializationTransformRename(to = "CAT", from = "C"),
// CordaSerializationTransformRename(to = "BOB", from = "E")
// )
// enum class MultiOperations { A, B, CAT, D, BOB, F, G}
//
// Fifth, Rename F to FLUMP, Rename BOB to BBB, Rename A to APPLE
// @CordaSerializationTransformEnumDefaults(
// CordaSerializationTransformEnumDefault(old = "F", new = "G"),
// CordaSerializationTransformEnumDefault(old = "BOB", new = "F"),
// CordaSerializationTransformEnumDefault(old = "D", new = "E"),
// CordaSerializationTransformEnumDefault(old = "C", new = "D")
// )
// @CordaSerializationTransformRenames (
// CordaSerializationTransformRename(to = "APPLE", from = "A"),
// CordaSerializationTransformRename(to = "BBB", from = "BOB"),
// CordaSerializationTransformRename(to = "FLUMP", from = "F"),
// CordaSerializationTransformRename(to = "CAT", from = "C"),
// CordaSerializationTransformRename(to = "BOB", from = "E")
// )
// enum class MultiOperations { APPLE, B, CAT, D, BBB, FLUMP, G}
//
// Finally, the original version of teh class that we're going to be testing with
enum class MultiOperations { A, B, C }
@Test
fun multiOperations() {
val resource = "${javaClass.simpleName}.${testName()}"
val sf = testDefaultFactory()
data class C(val e: MultiOperations)
// Uncomment to re-generate test files, needs to be done in three stages
val so = SerializationOutput(sf)
// First change
// File(URI("$localPath/$resource.1.A")).writeBytes(so.serialize(C(MultiOperations.A)).bytes)
// File(URI("$localPath/$resource.1.B")).writeBytes(so.serialize(C(MultiOperations.B)).bytes)
// File(URI("$localPath/$resource.1.C")).writeBytes(so.serialize(C(MultiOperations.C)).bytes)
// File(URI("$localPath/$resource.1.D")).writeBytes(so.serialize(C(MultiOperations.D)).bytes)
// Second change
// File(URI("$localPath/$resource.2.A")).writeBytes(so.serialize(C(MultiOperations.A)).bytes)
// File(URI("$localPath/$resource.2.B")).writeBytes(so.serialize(C(MultiOperations.B)).bytes)
// File(URI("$localPath/$resource.2.C")).writeBytes(so.serialize(C(MultiOperations.C)).bytes)
// File(URI("$localPath/$resource.2.D")).writeBytes(so.serialize(C(MultiOperations.D)).bytes)
// File(URI("$localPath/$resource.2.E")).writeBytes(so.serialize(C(MultiOperations.E)).bytes)
// Third change
// File(URI("$localPath/$resource.3.A")).writeBytes(so.serialize(C(MultiOperations.A)).bytes)
// File(URI("$localPath/$resource.3.B")).writeBytes(so.serialize(C(MultiOperations.B)).bytes)
// File(URI("$localPath/$resource.3.C")).writeBytes(so.serialize(C(MultiOperations.C)).bytes)
// File(URI("$localPath/$resource.3.D")).writeBytes(so.serialize(C(MultiOperations.D)).bytes)
// File(URI("$localPath/$resource.3.BOB")).writeBytes(so.serialize(C(MultiOperations.BOB)).bytes)
// Fourth change
// File(URI("$localPath/$resource.4.A")).writeBytes(so.serialize(C(MultiOperations.A)).bytes)
// File(URI("$localPath/$resource.4.B")).writeBytes(so.serialize(C(MultiOperations.B)).bytes)
// File(URI("$localPath/$resource.4.CAT")).writeBytes(so.serialize(C(MultiOperations.CAT)).bytes)
// File(URI("$localPath/$resource.4.D")).writeBytes(so.serialize(C(MultiOperations.D)).bytes)
// File(URI("$localPath/$resource.4.BOB")).writeBytes(so.serialize(C(MultiOperations.BOB)).bytes)
// File(URI("$localPath/$resource.4.F")).writeBytes(so.serialize(C(MultiOperations.F)).bytes)
// File(URI("$localPath/$resource.4.G")).writeBytes(so.serialize(C(MultiOperations.G)).bytes)
// Fifth change - { APPLE, B, CAT, D, BBB, FLUMP, G}
// File(URI("$localPath/$resource.5.APPLE")).writeBytes(so.serialize(C(MultiOperations.APPLE)).bytes)
// File(URI("$localPath/$resource.5.B")).writeBytes(so.serialize(C(MultiOperations.B)).bytes)
// File(URI("$localPath/$resource.5.CAT")).writeBytes(so.serialize(C(MultiOperations.CAT)).bytes)
// File(URI("$localPath/$resource.5.D")).writeBytes(so.serialize(C(MultiOperations.D)).bytes)
// File(URI("$localPath/$resource.5.BBB")).writeBytes(so.serialize(C(MultiOperations.BBB)).bytes)
// File(URI("$localPath/$resource.5.FLUMP")).writeBytes(so.serialize(C(MultiOperations.FLUMP)).bytes)
// File(URI("$localPath/$resource.5.G")).writeBytes(so.serialize(C(MultiOperations.G)).bytes)
val stage1Resources = listOf(
Pair("$resource.1.A", MultiOperations.A),
Pair("$resource.1.B", MultiOperations.B),
Pair("$resource.1.C", MultiOperations.C),
Pair("$resource.1.D", MultiOperations.C))
val stage2Resources = listOf(
Pair("$resource.2.A", MultiOperations.A),
Pair("$resource.2.B", MultiOperations.B),
Pair("$resource.2.C", MultiOperations.C),
Pair("$resource.2.D", MultiOperations.C),
Pair("$resource.2.E", MultiOperations.C))
val stage3Resources = listOf(
Pair("$resource.3.A", MultiOperations.A),
Pair("$resource.3.B", MultiOperations.B),
Pair("$resource.3.C", MultiOperations.C),
Pair("$resource.3.D", MultiOperations.C),
Pair("$resource.3.BOB", MultiOperations.C))
val stage4Resources = listOf(
Pair("$resource.4.A", MultiOperations.A),
Pair("$resource.4.B", MultiOperations.B),
Pair("$resource.4.CAT", MultiOperations.C),
Pair("$resource.4.D", MultiOperations.C),
Pair("$resource.4.BOB", MultiOperations.C),
Pair("$resource.4.F", MultiOperations.C),
Pair("$resource.4.G", MultiOperations.C))
val stage5Resources = listOf(
Pair("$resource.5.APPLE", MultiOperations.A),
Pair("$resource.5.B", MultiOperations.B),
Pair("$resource.5.CAT", MultiOperations.C),
Pair("$resource.5.D", MultiOperations.C),
Pair("$resource.5.BBB", MultiOperations.C),
Pair("$resource.5.FLUMP", MultiOperations.C),
Pair("$resource.5.G", MultiOperations.C))
fun load(l: List<Pair<String, MultiOperations>>) = l.map {
Pair (DeserializationInput(sf).deserialize(SerializedBytes<C>(
File(EvolvabilityTests::class.java.getResource(it.first).toURI()).readBytes())), it.second)
}
load (stage1Resources).forEach { assertEquals(it.second, it.first.e) }
load (stage2Resources).forEach { assertEquals(it.second, it.first.e) }
load (stage3Resources).forEach { assertEquals(it.second, it.first.e) }
load (stage4Resources).forEach { assertEquals(it.second, it.first.e) }
load (stage5Resources).forEach { assertEquals(it.second, it.first.e) }
}
@CordaSerializationTransformEnumDefault(old = "A", new = "F")
enum class BadNewValue { A, B, C, D }
@Test
fun badNewValue() {
val sf = testDefaultFactory()
data class C (val e : BadNewValue)
Assertions.assertThatThrownBy {
SerializationOutput(sf).serialize(C(BadNewValue.A))
}.isInstanceOf(NotSerializableException::class.java)
}
@CordaSerializationTransformEnumDefaults(
CordaSerializationTransformEnumDefault(new = "D", old = "E"),
CordaSerializationTransformEnumDefault(new = "E", old = "A")
)
enum class OutOfOrder { A, B, C, D, E}
@Test
fun outOfOrder() {
val sf = testDefaultFactory()
data class C (val e : OutOfOrder)
Assertions.assertThatThrownBy {
SerializationOutput(sf).serialize(C(OutOfOrder.A))
}.isInstanceOf(NotSerializableException::class.java)
}
// class as it existed as it was serialized
//
// enum class ChangedOrdinality { A, B, C }
//
// class as it exists for the tests
@CordaSerializationTransformEnumDefault("D", "A")
enum class ChangedOrdinality { A, B, D, C }
@Test
fun changedOrdinality() {
val resource = "${javaClass.simpleName}.${testName()}"
val sf = testDefaultFactory()
data class C(val e: ChangedOrdinality)
// Uncomment to re-generate test files, needs to be done in three stages
// File(URI("$localPath/$resource")).writeBytes(
// SerializationOutput(sf).serialize(C(ChangedOrdinality.A)).bytes)
Assertions.assertThatThrownBy {
DeserializationInput(sf).deserialize(SerializedBytes<C>(
File(EvolvabilityTests::class.java.getResource(resource).toURI()).readBytes()))
}.isInstanceOf(NotSerializableException::class.java)
}
}
}

View File

@ -2,6 +2,7 @@ package net.corda.nodeapi.internal.serialization.amqp
import net.corda.core.serialization.DeprecatedConstructorForDeserialization
import net.corda.core.serialization.SerializedBytes
import net.corda.testing.common.internal.ProjectStructure.projectRootDir
import org.junit.Test
import java.io.File
import java.io.NotSerializableException
@ -18,7 +19,8 @@ import kotlin.test.assertEquals
// 5. Comment back out the generation code and uncomment the actual test
class EvolvabilityTests {
// When regenerating the test files this needs to be set to the file system location of the resource files
var localPath = "file:///<path>/<to>/<toplevel of>/corda/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp"
var localPath = projectRootDir.toUri().resolve(
"node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp")
@Test
fun simpleOrderSwapSameType() {