CORDA-553 - Enable Enum Evolution

This commit is contained in:
Katelyn Baker 2017-11-27 19:21:27 +00:00
parent e8822ce391
commit 6fc736a5f5
49 changed files with 635 additions and 63 deletions

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,89 @@
package net.corda.nodeapi.internal.serialization.amqp
import org.apache.qpid.proton.amqp.Symbol
import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException
import java.lang.reflect.Type
import java.util.*
/**
* @property transforms
*
*/
class EnumEvolutionSerializer(
clazz: Type,
factory: SerializerFactory,
private val conversions : Map<String, String>,
private val ordinals : Map<String, Int>) : AMQPSerializer<Any> {
override val type: Type = clazz
override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")!!
companion object {
fun MutableMap<String, String>.mapInPlace(f : (String)->String) {
val i = this.iterator()
while(i.hasNext()) {
val curr = (i.next())
curr.setValue(f(curr.value))
}
}
/**
* @param old
* @param new
*/
fun make(old: RestrictedType,
new: AMQPSerializer<Any>,
factory: SerializerFactory,
transformsFromBlob: TransformsSchema): AMQPSerializer<Any> {
val wireTransforms = transformsFromBlob.types[old.name]
val localTransforms = TransformsSchema.get(old.name, factory)
val transforms = if (wireTransforms?.size ?: -1 > 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
@Suppress("UNCHECKED_CAST")
val defaultRules = transforms[TransformTypes.EnumDefault] as? List<EnumDefaultSchemaTransform>
@Suppress("UNCHECKED_CAST")
val renameRules = transforms[TransformTypes.Rename] as? List<RenameSchemaTransform>
// What values exist on the enum as it exists on the class path
val localVals = new.type.asClass()!!.enumConstants.map { it.toString() }
var conversions : MutableMap<String, String> = new.type.asClass()!!.enumConstants.map { it.toString() }
.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())
rules.putAll(renameRules?.associateBy({ it.to }, { it.from }) ?: emptyMap())
while (conversions.filter { it.value !in localVals }.isNotEmpty()) {
conversions.mapInPlace { rules[it] ?: it }
}
var idx = 0
return EnumEvolutionSerializer(new.type, factory, conversions, localVals.associateBy( {it}, { idx++ }))
}
}
override fun readObject(obj: Any, schemas: SerializationSchemas, input: DeserializationInput): Any {
var 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 IllegalAccessException("It should be impossible to write an evolution serializer")
}
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
throw IllegalAccessException("It should be impossible to write an evolution serializer")
}
}

View File

@ -50,7 +50,7 @@ open class SerializerFactory(val whitelist: ClassWhitelist, cl: ClassLoader) {
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, transforms)
}
}
}

View File

@ -27,14 +27,62 @@ 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: Set<String>) { }
},
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 types, 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(l : List<Transform>, constants: Set<String>) {
@Suppress("UNCHECKED_CAST") (l as List<EnumDefaultSchemaTransform>).forEach {
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")
}
}
}
},
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: Set<String>) {
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 +93,8 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType
//}
;
abstract fun validate(l: List<Transform>, constants: Set<String>)
companion object : DescribedTypeConstructor<TransformTypes> {
val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_ELEMENT_KEY.amqpDescriptor

View File

@ -148,6 +148,8 @@ class EnumDefaultSchemaTransform(val old: String, val new: String) : Transform()
*
* @property from the name at time of change of the property
* @property to the new name of the property
*
*
*/
class RenameSchemaTransform(val from: String, val to: String) : Transform() {
companion object : DescribedTypeConstructor<RenameSchemaTransform> {
@ -192,6 +194,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.map { it.toString() }.toSet())
}
}
} 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 +257,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 +305,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")
@ -35,9 +36,323 @@ 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 = "${this.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 = "${this.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(EvolvabilityTests::class.java.getResource(resource).toURI()).readBytes()))
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 = "${this.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 = "${this.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) }
}
}

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() {