From 3350605536d9fe1e0a536df302fee39f2232e797 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Thu, 26 Oct 2017 16:54:36 +0100 Subject: [PATCH] CORDA-553 - Cope with future transforms --- .../internal/serialization/amqp/Envelope.kt | 1 + .../serialization/amqp/SupportedTransforms.kt | 14 +- .../serialization/amqp/TansformTypes.kt | 25 ++- .../serialization/amqp/TransformsSchema.kt | 57 +++++-- .../amqp/EnumEvolvabilityTests.kt | 73 ++++++--- .../serialization/amqp/EvolvabilityTests.kt | 146 ++++++++---------- ...EnumEvolvabilityTests.testUnknownTransform | Bin 0 -> 785 bytes ...bilityTests.addAdditionalParamNotMandatory | Bin 273 -> 292 bytes .../EvolvabilityTests.addAndRemoveParameters | Bin 390 -> 403 bytes ...yTests.addMandatoryFieldWithAltConstructor | Bin 285 -> 298 bytes ...dMandatoryFieldWithAltReorderedConstructor | Bin 387 -> 400 bytes ...FieldWithAltReorderedConstructorAndRemoval | Bin 403 -> 416 bytes .../amqp/EvolvabilityTests.changeSubType | Bin 616 -> 629 bytes .../amqp/EvolvabilityTests.multiVersion.1 | Bin 294 -> 307 bytes .../amqp/EvolvabilityTests.multiVersion.2 | Bin 327 -> 340 bytes .../amqp/EvolvabilityTests.multiVersion.3 | Bin 372 -> 385 bytes ...volvabilityTests.multiVersionWithRemoval.1 | Bin 338 -> 351 bytes ...volvabilityTests.multiVersionWithRemoval.2 | Bin 392 -> 405 bytes ...volvabilityTests.multiVersionWithRemoval.3 | Bin 425 -> 438 bytes .../amqp/EvolvabilityTests.removeParameters | Bin 378 -> 391 bytes ...vabilityTests.simpleOrderSwapDifferentType | Bin 311 -> 324 bytes .../EvolvabilityTests.simpleOrderSwapSameType | Bin 302 -> 315 bytes 22 files changed, 198 insertions(+), 118 deletions(-) create mode 100644 node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.testUnknownTransform diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Envelope.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Envelope.kt index 358cecbb48..5b489c5d84 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Envelope.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Envelope.kt @@ -57,6 +57,7 @@ data class Envelope(val obj: Any?, val schema: Schema, val transformsSchema: Tra ENVELOPE_WITH_TRANSFORMS -> list[TRANSFORMS_SCHEMA_IDX] as TransformsSchema else -> throw NotSerializableException("Malformed list, bad length of ${list.size} (should be 2 or 3)") } + return Envelope(list[BLOB_IDX], list[SCHEMA_IDX] as Schema, transformSchema) } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt index 90ef03d79b..df3f4f8d45 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SupportedTransforms.kt @@ -36,11 +36,17 @@ private val wrapperExtract = { x: Annotation -> */ private val singleExtract = { x: Annotation -> listOf(x) } +// Transform annotation used to test the handling of transforms the de-serialising node doesn't understand. At +// some point test cases will have been created with this transform applied. +//@Target(AnnotationTarget.CLASS) +//@Retention(AnnotationRetention.RUNTIME) +//annotation class UnknownTransformAnnotation(val a: Int, val b: Int, val c: Int) + /** - * Utility list of all transforms we support that simplifies our generation code + * Utility list of all transforms we support that simplifies our generation code. * * NOTE: We have to support single instances of the transform annotations as well as the wrapping annotation - * when many instances are repeated + * when many instances are repeated. */ val supportedTransforms = listOf( SupportedTransform( @@ -63,4 +69,8 @@ val supportedTransforms = listOf( TransformTypes.Rename, singleExtract ) + //,SupportedTransform( + // UnknownTransformAnnotation::class.java, + // TransformTypes.UnknownTest, + // singleExtract) ) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt index 28f5b7e27c..de4f2d8703 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TansformTypes.kt @@ -14,11 +14,21 @@ import java.io.NotSerializableException * @property build should be a function that takes a transform [Annotation] (currently one of * [CordaSerializationTransformRename] or [CordaSerializationTransformEnumDefaults]) * and constructs an instance of the corresponding [Transform] type + * + * DO NOT REORDER THE CONSTANTS!!! Please append any new entries to the end */ // TODO: it would be awesome to auto build this list by scanning for transform annotations themselves // TODO: annotated with some annotation enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType { - EnumDefault({ a -> EnumDefaultSchemeTransform((a as CordaSerializationTransformEnumDefault).old, a.new) }) { + /** + * Placeholder entry for future transforms where a node receives a transform we've subsequently + * added and thus the de-serialising node doesn't know about that transform. + */ + Unknown({ UnknownTransform() }) { + override fun getDescriptor(): Any = DESCRIPTOR + override fun getDescribed(): Any = ordinal + }, + EnumDefault({ a -> EnumDefaultSchemaTransform((a as CordaSerializationTransformEnumDefault).old, a.new) }) { override fun getDescriptor(): Any = DESCRIPTOR override fun getDescribed(): Any = ordinal }, @@ -26,6 +36,13 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType override fun getDescriptor(): Any = DESCRIPTOR override fun getDescribed(): Any = ordinal }; + // 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 + // + //UnknownTest({ a -> UnknownTestTransform((a as UnknownTransformAnnotation).a, a.b, a.c)}) { + // override fun getDescriptor(): Any = DESCRIPTOR + // override fun getDescribed(): Any = ordinal + //}; companion object : DescribedTypeConstructor { val DESCRIPTOR = AMQPDescriptorRegistry.TRANSFORM_ELEMENT_KEY.amqpDescriptor @@ -42,10 +59,10 @@ enum class TransformTypes(val build: (Annotation) -> Transform) : DescribedType throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.") } - try { - return values()[describedType.described as Int] + return try { + values()[describedType.described as Int] } catch (e: IndexOutOfBoundsException) { - throw NotSerializableException("Bad ordinal value ${describedType.described}.") + values()[0] } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt index 54836bc158..2bde766c2c 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/TransformsSchema.kt @@ -37,15 +37,18 @@ abstract class Transform : DescribedType { * the schema as a list of class name and parameters.Using the class name (list element 0) * create the appropriate class instance * + * For future proofing any unknown transform types are not treated as errors, rather we + * simply create a placeholder object so we can ignore it + * * @param obj: a serialized instance of a described type, should be one of the * descendants of this class */ override fun newInstance(obj: Any?): Transform { val described = Transform.checkDescribed(obj) as List<*> return when (described[0]) { - EnumDefaultSchemeTransform.typeName -> EnumDefaultSchemeTransform.newInstance(described) + EnumDefaultSchemaTransform.typeName -> EnumDefaultSchemaTransform.newInstance(described) RenameSchemaTransform.typeName -> RenameSchemaTransform.newInstance(described) - else -> throw NotSerializableException("Unexpected transform type ${described[0]}") + else -> UnknownTransform() } } @@ -63,27 +66,64 @@ abstract class Transform : DescribedType { abstract val name: String } +/** + * Transform type placeholder that allows for backward compatibility. Should a noce recieve + * a transform type it doesn't recognise, we can will use this as a placeholder + */ +class UnknownTransform : Transform() { + companion object : DescribedTypeConstructor { + val typeName = "UnknownTransform" + + override fun newInstance(obj: Any?) = UnknownTransform() + + override fun getTypeClass(): Class<*> = UnknownTransform::class.java + } + + override fun getDescribed(): Any = emptyList() + override fun params() = "" + + override val name: String get() = typeName +} + +class UnknownTestTransform(val a: Int, val b: Int, val c: Int) : Transform() { + companion object : DescribedTypeConstructor { + val typeName = "UnknownTest" + + override fun newInstance(obj: Any?) : UnknownTestTransform { + val described = obj as List<*> + return UnknownTestTransform(described[1] as Int, described[2] as Int, described[3] as Int) + } + + override fun getTypeClass(): Class<*> = UnknownTransform::class.java + } + + override fun getDescribed(): Any = listOf(name, a, b, c) + override fun params() = "" + + override val name: String get() = typeName +} + /** * Transform to be used on an Enumerated Type whenever a new element is added * * @property old The value the [new] instance should default to when not available * @property new the value (as a String) that has been added */ -class EnumDefaultSchemeTransform(val old: String, val new: String) : Transform() { - companion object : DescribedTypeConstructor { +class EnumDefaultSchemaTransform(val old: String, val new: String) : Transform() { + companion object : DescribedTypeConstructor { /** * Value encoded into the schema that identifies a transform as this type */ val typeName = "EnumDefault" - override fun newInstance(obj: Any?): EnumDefaultSchemeTransform { + override fun newInstance(obj: Any?): EnumDefaultSchemaTransform { val described = obj as List<*> val old = described[1] as? String ?: throw IllegalStateException("Was expecting \"old\" as a String") val new = described[2] as? String ?: throw IllegalStateException("Was expecting \"new\" as a String") - return EnumDefaultSchemeTransform(old, new) + return EnumDefaultSchemaTransform(old, new) } - override fun getTypeClass(): Class<*> = EnumDefaultSchemeTransform::class.java + override fun getTypeClass(): Class<*> = EnumDefaultSchemaTransform::class.java } @Suppress("UNUSED") @@ -93,7 +133,7 @@ class EnumDefaultSchemeTransform(val old: String, val new: String) : Transform() override fun params() = "old=${old.esc()} new=${new.esc()}" override fun equals(other: Any?) = ( - (other is EnumDefaultSchemeTransform && other.new == new && other.old == old) || super.equals(other)) + (other is EnumDefaultSchemaTransform && other.new == new && other.old == old) || super.equals(other)) override fun hashCode() = (17 * new.hashCode()) + old.hashCode() @@ -138,7 +178,6 @@ class RenameSchemaTransform(val from: String, val to: String) : Transform() { override val name: String get() = typeName } - /** * Represents the set of all transforms that can be a applied to all classes represented as part of * an AMQP schema. It forms a part of the AMQP envelope alongside the [Schema] and the serialized bytes diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt index 2e333e15e7..2fffc13b28 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.kt @@ -1,16 +1,19 @@ package net.corda.nodeapi.internal.serialization.amqp -import net.corda.core.serialization.CordaSerializationTransformEnumDefault -import net.corda.core.serialization.CordaSerializationTransformEnumDefaults -import net.corda.core.serialization.CordaSerializationTransformRename -import net.corda.core.serialization.CordaSerializationTransformRenames +import net.corda.core.serialization.* 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 import kotlin.test.assertTrue class EnumEvolvabilityTests { +// var localPath = "file:///Users/katelynbaker/srcs-ide/corda/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp" + var localPath = "file:///home/katelyn/srcs/corda/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp" + + companion object { val VERBOSE = false } @@ -104,9 +107,9 @@ class EnumEvolvabilityTests { assertEquals(1, schema.size) assertTrue (schema.keys.contains(TransformTypes.EnumDefault)) assertEquals (1, schema[TransformTypes.EnumDefault]!!.size) - assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemeTransform) - assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).new) - assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).old) + assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform) + assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new) + assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old) } @Test @@ -125,12 +128,12 @@ class EnumEvolvabilityTests { assertEquals(1, schema.size) assertTrue (schema.keys.contains(TransformTypes.EnumDefault)) assertEquals (2, schema[TransformTypes.EnumDefault]!!.size) - assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemeTransform) - assertEquals ("E", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).new) - assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).old) - assertTrue (schema[TransformTypes.EnumDefault]!![1] is EnumDefaultSchemeTransform) - assertEquals ("D", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemeTransform).new) - assertEquals ("A", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemeTransform).old) + assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform) + assertEquals ("E", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new) + assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old) + assertTrue (schema[TransformTypes.EnumDefault]!![1] is EnumDefaultSchemaTransform) + assertEquals ("D", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemaTransform).new) + assertEquals ("A", (schema[TransformTypes.EnumDefault]!![1] as EnumDefaultSchemaTransform).old) } @Test @@ -157,9 +160,9 @@ class EnumEvolvabilityTests { assertTrue (schema!!.keys.contains(TransformTypes.EnumDefault)) assertEquals (1, schema[TransformTypes.EnumDefault]!!.size) - assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemeTransform) - assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).new) - assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).old) + assertTrue (schema[TransformTypes.EnumDefault]!![0] is EnumDefaultSchemaTransform) + assertEquals ("D", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new) + assertEquals ("A", (schema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old) } @Test @@ -183,10 +186,10 @@ class EnumEvolvabilityTests { val enumDefaults = transforms[AnnotatedEnumTwice::class.java.name]!![TransformTypes.EnumDefault]!! - assertEquals("E", (enumDefaults[0] as EnumDefaultSchemeTransform).new) - assertEquals("D", (enumDefaults[0] as EnumDefaultSchemeTransform).old) - assertEquals("D", (enumDefaults[1] as EnumDefaultSchemeTransform).new) - assertEquals("A", (enumDefaults[1] as EnumDefaultSchemeTransform).old) + assertEquals("E", (enumDefaults[0] as EnumDefaultSchemaTransform).new) + assertEquals("D", (enumDefaults[0] as EnumDefaultSchemaTransform).old) + assertEquals("D", (enumDefaults[1] as EnumDefaultSchemaTransform).new) + assertEquals("A", (enumDefaults[1] as EnumDefaultSchemaTransform).old) } @Test @@ -299,8 +302,8 @@ class EnumEvolvabilityTests { assertEquals("X", (serialisedSchema[TransformTypes.Rename]!![0] as RenameSchemaTransform).to) assertEquals(1, serialisedSchema[TransformTypes.EnumDefault]!!.size) - assertEquals("E", (serialisedSchema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).new) - assertEquals("X", (serialisedSchema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemeTransform).old) + assertEquals("E", (serialisedSchema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).new) + assertEquals("X", (serialisedSchema[TransformTypes.EnumDefault]!![0] as EnumDefaultSchemaTransform).old) } @CordaSerializationTransformEnumDefaults ( @@ -402,6 +405,32 @@ class EnumEvolvabilityTests { assertEquals (sb1.transformsSchema.types[AnnotatedEnumOnce::class.java.name], sb2.transformsSchema.types[AnnotatedEnumOnce::class.java.name]) + } + + // @UnknownTransformAnnotation (10, 20, 30) + enum class WithUnknownTest { + A, B, C, D + } + + data class WrapsUnknown(val unknown: WithUnknownTest) + + // To regenerate the types for this test uncomment the UnknownTransformAnnotation from + // TransformTypes.kt and SupportedTransforms.kt + @Test + fun testUnknownTransform() { + val resource = "EnumEvolvabilityTests.testUnknownTransform" + val sf = testDefaultFactory() + + //File(URI("$localPath/$resource")).writeBytes( + // SerializationOutput(sf).serialize(WrapsUnknown(WithUnknownTest.D)).bytes) + + val path = EvolvabilityTests::class.java.getResource(resource) + val sb1 = File(path.toURI()).readBytes() + + val envelope = DeserializationInput(sf).deserializeAndReturnEnvelope(SerializedBytes(sb1)).envelope + + assertTrue(envelope.transformsSchema.types.containsKey(WithUnknownTest::class.java.name)) + assertTrue(envelope.transformsSchema.types[WithUnknownTest::class.java.name]!!.containsKey(TransformTypes.Unknown)) } } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt index 9c4a949384..f5ab608e44 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt @@ -5,36 +5,40 @@ import net.corda.core.serialization.SerializedBytes import org.junit.Test import java.io.File import java.io.NotSerializableException +import java.net.URI import kotlin.test.assertEquals // To regenerate any of the binary test files do the following // +// 0. set localPath accordingly // 1. Uncomment the code where the original form of the class is defined in the test // 2. Comment out the rest of the test // 3. Run the test // 4. Using the printed path copy that file to the resources directory // 5. Comment back out the generation code and uncomment the actual test class EvolvabilityTests { + // When regenerating the test files this needs to be set to the file system location of the resource files + var localPath = "file://////corda/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp" @Test fun simpleOrderSwapSameType() { val sf = testDefaultFactory() - val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.simpleOrderSwapSameType") - val f = File(path.toURI()) + val resource= "EvolvabilityTests.simpleOrderSwapSameType" val A = 1 val B = 2 // Original version of the class for the serialised version of this class - // // data class C (val a: Int, val b: Int) // val sc = SerializationOutput(sf).serialize(C(A, B)) - // f.writeBytes(sc.bytes) - // println (path) + // File(URI("$localPath/$resource")).writeBytes(sc.bytes) // new version of the class, in this case the order of the parameters has been swapped data class C(val b: Int, val a: Int) + val path = EvolvabilityTests::class.java.getResource(resource) + val f = File(path.toURI()) + val sc2 = f.readBytes() val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -45,21 +49,20 @@ class EvolvabilityTests { @Test fun simpleOrderSwapDifferentType() { val sf = testDefaultFactory() - val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.simpleOrderSwapDifferentType") - val f = File(path.toURI()) val A = 1 val B = "two" + val resource = "EvolvabilityTests.simpleOrderSwapDifferentType" // Original version of the class as it was serialised - // // data class C (val a: Int, val b: String) // val sc = SerializationOutput(sf).serialize(C(A, B)) - // f.writeBytes(sc.bytes) - // println (path) + // File(URI("$localPath/$resource")).writeBytes(sc.bytes) // new version of the class, in this case the order of the parameters has been swapped data class C(val b: String, val a: Int) + val path = EvolvabilityTests::class.java.getResource(resource) + val f = File(path.toURI()) val sc2 = f.readBytes() val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -70,18 +73,18 @@ class EvolvabilityTests { @Test fun addAdditionalParamNotMandatory() { val sf = testDefaultFactory() - val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.addAdditionalParamNotMandatory") - val f = File(path.toURI()) val A = 1 + val resource = "EvolvabilityTests.addAdditionalParamNotMandatory" // Original version of the class as it was serialised - // // data class C(val a: Int) // val sc = SerializationOutput(sf).serialize(C(A)) - // f.writeBytes(sc.bytes) - // println ("Path = $path") + // File(URI("$localPath/$resource")).writeBytes(sc.bytes) + data class C(val a: Int, val b: Int?) + val path = EvolvabilityTests::class.java.getResource(resource) + val f = File(path.toURI()) val sc2 = f.readBytes() val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -119,22 +122,21 @@ class EvolvabilityTests { @Test fun removeParameters() { val sf = testDefaultFactory() - val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.removeParameters") - val f = File(path.toURI()) + val resource = "EvolvabilityTests.removeParameters" val A = 1 val B = "two" val C = "three" val D = 4 // Original version of the class as it was serialised - // // data class CC(val a: Int, val b: String, val c: String, val d: Int) // val scc = SerializationOutput(sf).serialize(CC(A, B, C, D)) - // f.writeBytes(scc.bytes) - // println ("Path = $path") + // File(URI("$localPath/$resource")).writeBytes(scc.bytes) data class CC(val b: String, val d: Int) + val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.removeParameters") + val f = File(path.toURI()) val sc2 = f.readBytes() val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -146,23 +148,23 @@ class EvolvabilityTests { @Test fun addAndRemoveParameters() { val sf = testDefaultFactory() - val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.addAndRemoveParameters") - val f = File(path.toURI()) val A = 1 val B = "two" val C = "three" val D = 4 val E = null + val resource = "EvolvabilityTests.addAndRemoveParameters" + // Original version of the class as it was serialised - // // data class CC(val a: Int, val b: String, val c: String, val d: Int) // val scc = SerializationOutput(sf).serialize(CC(A, B, C, D)) - // f.writeBytes(scc.bytes) - // println ("Path = $path") + // File(URI("$localPath/$resource")).writeBytes(scc.bytes) data class CC(val a: Int, val e: Boolean?, val d: Int) + val path = EvolvabilityTests::class.java.getResource(resource) + val f = File(path.toURI()) val sc2 = f.readBytes() val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -174,16 +176,13 @@ class EvolvabilityTests { @Test fun addMandatoryFieldWithAltConstructor() { val sf = testDefaultFactory() - val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.addMandatoryFieldWithAltConstructor") - val f = File(path.toURI()) val A = 1 + val resource = "EvolvabilityTests.addMandatoryFieldWithAltConstructor" // Original version of the class as it was serialised - // // data class CC(val a: Int) // val scc = SerializationOutput(sf).serialize(CC(A)) - // f.writeBytes(scc.bytes) - // println ("Path = $path") + // File(URI("$localPath/$resource")).writeBytes(scc.bytes) @Suppress("UNUSED") data class CC(val a: Int, val b: String) { @@ -191,6 +190,8 @@ class EvolvabilityTests { constructor (a: Int) : this(a, "hello") } + val path = EvolvabilityTests::class.java.getResource(resource) + val f = File(path.toURI()) val sc2 = f.readBytes() val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -228,19 +229,15 @@ class EvolvabilityTests { @Test fun addMandatoryFieldWithAltReorderedConstructor() { val sf = testDefaultFactory() - val path = EvolvabilityTests::class.java.getResource( - "EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor") - val f = File(path.toURI()) + val resource = "EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor" val A = 1 val B = 100 val C = "This is not a banana" // Original version of the class as it was serialised - // // data class CC(val a: Int, val b: Int, val c: String) // val scc = SerializationOutput(sf).serialize(CC(A, B, C)) - // f.writeBytes(scc.bytes) - // println ("Path = $path") + // File(URI("$localPath/$resource")).writeBytes(scc.bytes) @Suppress("UNUSED") data class CC(val a: Int, val b: Int, val c: String, val d: String) { @@ -250,6 +247,8 @@ class EvolvabilityTests { constructor (c: String, a: Int, b: Int) : this(a, b, c, "wibble") } + val path = EvolvabilityTests::class.java.getResource(resource) + val f = File(path.toURI()) val sc2 = f.readBytes() val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -262,20 +261,15 @@ class EvolvabilityTests { @Test fun addMandatoryFieldWithAltReorderedConstructorAndRemoval() { val sf = testDefaultFactory() - val path = EvolvabilityTests::class.java.getResource( - "EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval") - val f = File(path.toURI()) + val resource = "EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval" val A = 1 @Suppress("UNUSED_VARIABLE") val B = 100 val C = "This is not a banana" // Original version of the class as it was serialised - // // data class CC(val a: Int, val b: Int, val c: String) - // val scc = SerializationOutput(sf).serialize(CC(A, B, C)) - // f.writeBytes(scc.bytes) - // println ("Path = $path") + // File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(CC(A, B, C)).bytes) // b is removed, d is added data class CC(val a: Int, val c: String, val d: String) { @@ -286,6 +280,8 @@ class EvolvabilityTests { constructor (c: String, a: Int) : this(a, c, "wibble") } + val path = EvolvabilityTests::class.java.getResource(resource) + val f = File(path.toURI()) val sc2 = f.readBytes() val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -297,9 +293,9 @@ class EvolvabilityTests { @Test fun multiVersion() { val sf = testDefaultFactory() - val path1 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersion.1") - val path2 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersion.2") - val path3 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersion.3") + val resource1 = "EvolvabilityTests.multiVersion.1" + val resource2 = "EvolvabilityTests.multiVersion.2" + val resource3 = "EvolvabilityTests.multiVersion.3" val a = 100 val b = 200 @@ -310,24 +306,15 @@ class EvolvabilityTests { // // Version 1: // data class C (val a: Int, val b: Int) - // - // val scc = SerializationOutput(sf).serialize(C(a, b)) - // File(path1.toURI()).writeBytes(scc.bytes) - // println ("Path = $path1") + // File(URI("$localPath/$resource1")).writeBytes(SerializationOutput(sf).serialize(C(a, b)).bytes) // // Version 2 - add param c // data class C (val c: Int, val b: Int, val a: Int) - // - // val scc = SerializationOutput(sf).serialize(C(c, b, a)) - // File(path2.toURI()).writeBytes(scc.bytes) - // println ("Path = $path2") + // File(URI("$localPath/$resource2")).writeBytes(SerializationOutput(sf).serialize(C(c, b, a)).bytes) // // Version 3 - add param d // data class C (val b: Int, val c: Int, val d: Int, val a: Int) - // - // val scc = SerializationOutput(sf).serialize(C(b, c, d, a)) - // File(path3.toURI()).writeBytes(scc.bytes) - // println ("Path = $path3") + // File(URI("$localPath/$resource3")).writeBytes(SerializationOutput(sf).serialize(C(b, c, d, a)).bytes) @Suppress("UNUSED") data class C(val e: Int, val c: Int, val b: Int, val a: Int, val d: Int) { @@ -341,6 +328,10 @@ class EvolvabilityTests { constructor (a: Int, b: Int, c: Int, d: Int) : this(-1, c, b, a, d) } + val path1 = EvolvabilityTests::class.java.getResource(resource1) + val path2 = EvolvabilityTests::class.java.getResource(resource2) + val path3 = EvolvabilityTests::class.java.getResource(resource3) + val sb1 = File(path1.toURI()).readBytes() val db1 = DeserializationInput(sf).deserialize(SerializedBytes(sb1)) @@ -372,24 +363,21 @@ class EvolvabilityTests { @Test fun changeSubType() { val sf = testDefaultFactory() - val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.changeSubType") - val f = File(path.toURI()) + val resource = "EvolvabilityTests.changeSubType" val oa = 100 val ia = 200 // Original version of the class as it was serialised - // // data class Inner (val a: Int) // data class Outer (val a: Int, val b: Inner) - // val scc = SerializationOutput(sf).serialize(Outer(oa, Inner (ia))) - // f.writeBytes(scc.bytes) - // println ("Path = $path") + // File(URI("$localPath/$resource")).writeBytes(SerializationOutput(sf).serialize(Outer(oa, Inner (ia))).bytes) // Add a parameter to inner but keep outer unchanged data class Inner(val a: Int, val b: String?) - data class Outer(val a: Int, val b: Inner) + val path = EvolvabilityTests::class.java.getResource(resource) + val f = File(path.toURI()) val sc2 = f.readBytes() val outer = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) @@ -401,9 +389,10 @@ class EvolvabilityTests { @Test fun multiVersionWithRemoval() { val sf = testDefaultFactory() - val path1 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersionWithRemoval.1") - val path2 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersionWithRemoval.2") - val path3 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersionWithRemoval.3") + + val resource1 = "EvolvabilityTests.multiVersionWithRemoval.1" + val resource2 = "EvolvabilityTests.multiVersionWithRemoval.2" + val resource3 = "EvolvabilityTests.multiVersionWithRemoval.3" @Suppress("UNUSED_VARIABLE") val a = 100 @@ -417,24 +406,15 @@ class EvolvabilityTests { // // Version 1: // data class C (val a: Int, val b: Int, val c: Int) + // File(URI("$localPath/$resource1")).writeBytes(SerializationOutput(sf).serialize(C(a, b, c)).bytes) // - // val scc = SerializationOutput(sf).serialize(C(a, b, c)) - // File(path1.toURI()).writeBytes(scc.bytes) - // println ("Path = $path1") - // - // Version 2 - add param c + // Version 2 - remove property a, add property e // data class C (val b: Int, val c: Int, val d: Int, val e: Int) - // - // val scc = SerializationOutput(sf).serialize(C(b, c, d, e)) - // File(path2.toURI()).writeBytes(scc.bytes) - // println ("Path = $path2") + // File(URI("$localPath/$resource2")).writeBytes(SerializationOutput(sf).serialize(C(b, c, d, e)).bytes) // // Version 3 - add param d // data class C (val b: Int, val c: Int, val d: Int, val e: Int, val f: Int) - // - // val scc = SerializationOutput(sf).serialize(C(b, c, d, e, f)) - // File(path3.toURI()).writeBytes(scc.bytes) - // println ("Path = $path3") + // File(URI("$localPath/$resource3")).writeBytes(SerializationOutput(sf).serialize(C(b, c, d, e, f)).bytes) @Suppress("UNUSED") data class C(val b: Int, val c: Int, val d: Int, val e: Int, val f: Int, val g: Int) { @@ -451,6 +431,10 @@ class EvolvabilityTests { constructor (b: Int, c: Int, d: Int, e: Int, f: Int) : this(b, c, d, e, f, -1) } + val path1 = EvolvabilityTests::class.java.getResource(resource1) + val path2 = EvolvabilityTests::class.java.getResource(resource2) + val path3 = EvolvabilityTests::class.java.getResource(resource3) + val sb1 = File(path1.toURI()).readBytes() val db1 = DeserializationInput(sf).deserialize(SerializedBytes(sb1)) diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.testUnknownTransform b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EnumEvolvabilityTests.testUnknownTransform new file mode 100644 index 0000000000000000000000000000000000000000..74c557f8841e8ae8104dd447af0628e58fc06bd7 GIT binary patch literal 785 zcmYe!FG@*dWME)u_zwg}lNcB;FfcIv0tzrQELO@(EzwH`sjxCjc1;g+^m46;$_NiH zb9AmUGS5f}w6#5;$B3dXIV#sJr7FnK&@>>l*f=o4(k#*>vK*+6jcFmHO9(UE9wwkY zjQ@c4fGlME3S=AX6_huOzi7FEK~2IJGDuQb=SEI+3#F)1@Av!pU4wYa2MCA=uHpg1%yJ1@UH&%qUL0rLShCSrW& zaG;P8u8HM93j0F#Qix#-gQ?=Y%#sYK>p<>xb#QQWg4@S-;658otZ{H%$dZ#;TmrWV zJveYhqQe1S7Puz%1MI*kbzI13fF|U$kkJrL$ax{75t@+8LPle_5a+=TCYpHlpaCLP bafdJ;P((y0*8vfhh1^J?AHo$P5+Vlx^;tFt literal 0 HcmV?d00001 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParamNotMandatory b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParamNotMandatory index 8e37059133d3a9853cee8fff322dc0c207a23114..7892b7f89b2195eab0d703c515fd399b6dbf28f7 100644 GIT binary patch delta 115 zcmbQpw1i0}Ilm|+k&%Icq2WIe98F?iyuiS~$O{x;W|(+ZMfw0UV+bQ$lIg%P#si0; r{E4BtEFeu2j~S{%HM1NLU|-0XxR5zBuf%mBqk)5?6I?InK}H4utUWRT delta 70 zcmZ3&G?7U(Ilm|+k&%Icp#g{)jwUfM9{9^NQCdd?%4IrmjPbx>Mu_yp=v)?%_{1}Y KV0tnmqZj~bYY+MW diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAndRemoveParameters b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAndRemoveParameters index 3943c84020a798e6b7d85324210e8f08669b6fa2..d92fd43a0389b705fb7185aa77bac8f253056354 100644 GIT binary patch delta 188 zcmZo;p3E$roL`iZ$jHFJ(C{A!jwUfMUSMEgtN{uzPn5P&fC@5!1XX~7U|DGp1EOJK zX$}iW&BUjMQBbuk2L#v`GA1r$&de)uUC3zQ;OGQ5oaKNZP%dd9TX9KIW?s6h1Eyp$ S7Rkv4jAFcSLpTpIG5`Q1`AsAM delta 174 zcmbQt+{P@OoL`iZ$jHFJ&;Y~?N0S&BFEB7L<^$PG6Q!*bpn^;wK@}heEGrFSKr~D& z&0ztlnfTN&3Pb}9U^yVbzK}6-A#-M4iR(f}0|!SZxZx}Z1c7o%3)zZGiZb)kT^%qb Mld(unE?^V`02@9gy8r+H diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructor b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructor index a7e207ac7570921b04699d400c231783cff4597f..43d786ee3df01a947174bab0dba886a8c53cc59d 100644 GIT binary patch delta 120 zcmbQsw2DbOIlm|+k&%Icq2WIe98F?iyuiS~C=3)}o+zy&0u^LBaE9@~Nk)jGiP6Q} vP;ur1YD^PlRCFB<$U-H7Ca@e3U|-0XxR5zBuf%mBqk)5?6WnypgNzIS%)~P6 delta 107 zcmZ3*G?z&_Ilm|+k&%Icp#g{)jwUfMUSMEgWCOC9CQ9pwKn0l&oMAk0k`bb4VstS# pRGj&M8q-7>6$OU_vQSB&2`mQ$*cUP;E@aNkD{)=OXyD-J1OViM7p1>@foL`iZ$jHFJ(C{A!jwUfMUSMEgtON=$Pn331fC@5!1jT`ZU|Bv81EOJK zeGLmp&BXtPFtscP1lSicCN5;o%qwwS$Y|i;=mb$aIe<|@18N}40YRXm8=iNRX`_j9%N(y0Mckcc>n+a delta 144 zcmbQh+{`SUoL`iZ$jHFJ&;Y~?N0S&BFEB7L<^b7D6Qx}gpn^;wL2)1lEXxOCKr~FO zuVDeHnfTvO0Yn20U^yVbzK}6-A#-M4iR(f}0|!SZh}y{kj1tmtMS?&@$qU(vONuh{ J(p?>ZssO`u9t;2g diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval index 922eecc33539c7e8c483ad07ad2090138c8a0055..95ada2e128e9730af949662bab91f8e08555ff85 100644 GIT binary patch delta 158 zcmbQtyntCcIlm|+k&%Icq2WIe98F?iyuiS~*a;M1o+$0202O2c3917H!Lo8721LWe z`W_aLn#uf(hA_n}2L#v`GA1r$&de)uUC3zQ;OGQVJUM|;LIY|d%K<^4qU42a#U(|V RdFieWa8*Dza2{l2007o~Kn0mVg6cpHSXK_ifM}Rl z-@^h@Gnt>!PyxgMn!s{EfPEoj;zH)kyb{-ij0O&lP7uYD6Bs3=;fe%-ijo(y6_*rc L=B2wj09641z1ba) diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.changeSubType b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.changeSubType index 96e11c7f09fa2c31a82b92521617dbaca74dd69e..636217e01feac82cc33fbd123917d8c845fd05c7 100644 GIT binary patch delta 183 zcmaFC@|8t8Ilm|+k&%Icq2WIe98F?iyuiS~6bBSwo+uru02O2c3Nk(i3W8)A?*SQ1 z5DgQjq_Ke1O#EjEQ_FHdfPEoj;zH)kyb{-ij0O&lP7t+|{Tb7^pa!xYSUGV);A8vw@1R91vh%$e6s4IWw=sbs?jHgQFANXoy@ArrcyhMloKvGR}jH F3;@tFL{0zz delta 142 zcmcb@beu^#Ilm|+k&%Icp#g{)jwUfMUSMEgGyt-hCQ6%$Kn0l&ykR`>k`bb4Vrn!u yRGj&M8q-7>m5J}!Y@i}62L#v`GA1u%&de)uUC3zQ;OGQ58X}j3DL2`WQ49cI2_fMC diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.3 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.3 index 1e36577ca927ec4b8c0ad0f9d9a5a54794c47338..06a0d6698324959a560fd966846719783fa9199a 100644 GIT binary patch delta 186 zcmeyu)W|HIoL`iZ$jHFJ(C{A!jwUfMUSMEg%mNB9Pn5P+fC@5!1O7%$ui&V!5$ E0EI+Ln*aa+ delta 172 zcmZo<{=y`koL`iZ$jHFJ&;Y~?N0S&BFEB7L#sb+)6Q!*cpn^;wK|vq~EXx66Kr~D& yi)I0-nRwMO0z?B1U^yVbzK}6#A#-M4iR(f}0|!SZxZx1FWIS>ym~xZj7{vg?@g*_< diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.1 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.1 index 9496b5632a3dc176a8e7ea25eaebc5b58dcea412..70e8a1387a17c02b4071e5ba12fe6e174509a351 100644 GIT binary patch delta 144 zcmcb_be~B&Ilm|+k&%Icq2WIe98F?iyuiS~=m8X9o+xc50u^LB@SX9%7eO>Q8K0?pskz-85sa&r9mG6 delta 131 zcmcc5bcsniIlm|+k&%Icp#g{)jwUfMUSMEgv<9-7CQ6%$Kn0l&d}loHg%P4?Vrm)- sNPOaXLj|Zj%K-uQg^Y;{nKScBTo*DLI5;{%)J|4ll#oJEl#Hwh0LqFUvH$=8 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.2 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.2 index 44cd5fb07fa79b1ab257e7ffa18f71049dc2fa9d..3d4e9e897fbc360041e72ce9ab513da7c1d32108 100644 GIT binary patch delta 168 zcmeBRp2{qfoL`iZ$jHFJ(C{A!jwUfMUSMEgtOE)#Gfe!TGSNYj1tc*sRsup-=0NC| yhG2TK9HT@$NC8kM%K-uQg^bAynKScBTo*DLI5;}NO@+v%;E_wkD#v+{kpTd!*GcpM delta 155 zcmbQr+`%l9oL`iZ$jHFJ&;Y~?N0S&BFEB7L76RE!3===7OmvWB0ZB}Zm4MKdIS~4# tA();l$0!j5QUKJ+azKE6A!G7F=FGej*M*D*4vtO?4JdLcc;r%%D@60`#ff@MuX42Xt_ zbvY~`H51<%!ql=H5MW=(n6!{NGq1#TA)|qVqZ35!WP3)5c&LF8MJbqyfG&W@rQ(rG L!z#ylkdXlZfhJcS delta 199 zcmdnSypmZuIlm|+k&%Icp#g{)jwUfMUSMEgoCIVuO_X+4fC@5!1nqzvu&gPF0nsqA zE{6rAX5u?T1rQB1faQPy`$EQ~h0K|GC9Vq@4ICVuAZjPuGfD)ZC`!Rp1UDcRk6aqE F8~_y2EVuvw diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.removeParameters b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.removeParameters index 65d11e24e38b90de2ec4778b916be5447c96fed3..926661d87b79097940bd60201fd8b13576e1b4c0 100644 GIT binary patch delta 190 zcmeyx)XprEoL`iZ$jHFJ(C{A!jwUfMUSMEgEC32HGfaG-GSNX)0V=@+l8^vOfK~8= z7!XYpOA}Z?Y9`(^1k;oG7$p*+nph49vM*#zTF6#hQk0pO?&{#+=mfV4BAJX!QUItk UWg&BBUWw~MMgwGxoCg^h07cwR1^@s6 delta 177 zcmZo?{>3DdoL`iZ$jHFJ&;Y~?N0S&BFEB7LCIi__3=CQ2KM0BMjQ(}9PK2ktRK6irOX pW&w#$Ja4D~qJi=(2L#v`GA1r$&de)uUC3zQ;OGQVJ6VBI901B`7exR7 diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapSameType b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapSameType index b96b0ce60ecd349703faf4b3e207ec9d54d41f87..377e82d50e84774271bdecccc9fd376139dd9ea2 100644 GIT binary patch delta 119 zcmZ3-w3|sLIlm|+k&%Icq2WIe98F?iyuiS~r~(vVW|(+RWugP82vmaUz-`6@Hy9xb uC&s3+fW#*rH-ylCB-EkmSPlrVFJw$w$efv1;<}L0z`@Z8ZaC*bMg{GZF delta 106 zcmdnZw2nz8Ilm|+k&%Icp#g{)jwUfMUSMEg6bG`I7$%-mndrbN0+nDoaGUYK4MvE< oiLq%cAn}RE4I%U&2`Q*LmIDIp3mKCZGH2$MxGrQgaBy@200yQQ=l}o!