mirror of
https://github.com/corda/corda.git
synced 2024-12-19 04:57:58 +00:00
Merge pull request #2154 from corda/kat/feature/enumEvolution2
Kat/feature/enum evolution2
This commit is contained in:
commit
ae3a3444f2
@ -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
|
||||
|
@ -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,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")
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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}")
|
||||
|
@ -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")
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
Binary file not shown.
Loading…
Reference in New Issue
Block a user