mirror of
https://github.com/corda/corda.git
synced 2025-04-12 21:53:17 +00:00
CORDA-553 - Enable Enum Evolution
This commit is contained in:
parent
e8822ce391
commit
6fc736a5f5
@ -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
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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}")
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user