/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include "FileIOBaseTestTypes.h" #include #include #include #include #include using namespace AZ; namespace UnitTest { /** * Tests generating and applying patching to serialized structures. * \note There a lots special... \TODO add notes depending on the final solution */ namespace Patching { // Object that we will store in container and patch in the complex case class ContainedObjectPersistentId { public: AZ_TYPE_INFO(ContainedObjectPersistentId, "{D0C4D19C-7EFF-4F93-A5F0-95F33FC855AA}") ContainedObjectPersistentId() : m_data(0) , m_persistentId(0) {} u64 GetPersistentId() const { return m_persistentId; } void SetPersistentId(u64 pesistentId) { m_persistentId = pesistentId; } int m_data; u64 m_persistentId; ///< Returns the persistent object ID static u64 GetPersistentIdWrapper(const void* instance) { return reinterpret_cast(instance)->GetPersistentId(); } static void Reflect(AZ::SerializeContext& sc) { sc.Class()-> PersistentId(&ContainedObjectPersistentId::GetPersistentIdWrapper)-> Field("m_data", &ContainedObjectPersistentId::m_data)-> Field("m_persistentId", &ContainedObjectPersistentId::m_persistentId); } }; class ContainedObjectDerivedPersistentId : public ContainedObjectPersistentId { public: AZ_TYPE_INFO(ContainedObjectDerivedPersistentId, "{1c3ba36a-ceee-4118-89e7-807930bf2bec}"); static void Reflect(AZ::SerializeContext& sc) { sc.Class(); } }; class ContainedObjectNoPersistentId { public: AZ_CLASS_ALLOCATOR(ContainedObjectNoPersistentId, SystemAllocator, 0); AZ_TYPE_INFO(ContainedObjectNoPersistentId, "{A9980498-6E7A-42C0-BF9F-DFA48142DDAB}"); ContainedObjectNoPersistentId() : m_data(0) {} ContainedObjectNoPersistentId(int data) : m_data(data) {} int m_data; static void Reflect(AZ::SerializeContext& sc) { sc.Class()-> Field("m_data", &ContainedObjectNoPersistentId::m_data); } }; class CommonPatch { public: AZ_RTTI(CommonPatch, "{81FE64FA-23DB-40B5-BD1B-9DC145CB86EA}"); AZ_CLASS_ALLOCATOR(CommonPatch, AZ::SystemAllocator, 0); virtual ~CommonPatch() = default; static void Reflect(AZ::SerializeContext& sc) { sc.Class() ->SerializeWithNoData(); } }; class ObjectToPatch : public CommonPatch { public: AZ_RTTI(ObjectToPatch, "{47E5CF10-3FA1-4064-BE7A-70E3143B4025}", CommonPatch); AZ_CLASS_ALLOCATOR(ObjectToPatch, AZ::SystemAllocator, 0); ObjectToPatch() = default; ObjectToPatch(const ObjectToPatch&) = delete; int m_intValue = 0; AZStd::vector m_objectArray; AZStd::vector m_derivedObjectArray; AZStd::unordered_map> m_objectMap; AZStd::vector m_objectArrayNoPersistentId; AZ::DynamicSerializableField m_dynamicField; ~ObjectToPatch() override { m_dynamicField.DestroyData(); } static void Reflect(AZ::SerializeContext& sc) { sc.Class()-> Field("m_dynamicField", &ObjectToPatch::m_dynamicField)-> Field("m_intValue", &ObjectToPatch::m_intValue)-> Field("m_objectArray", &ObjectToPatch::m_objectArray)-> Field("m_derivedObjectArray", &ObjectToPatch::m_derivedObjectArray)-> Field("m_objectMap", &ObjectToPatch::m_objectMap)-> Field("m_objectArrayNoPersistentId", &ObjectToPatch::m_objectArrayNoPersistentId); } }; class DifferentObjectToPatch : public CommonPatch { public: AZ_RTTI(DifferentObjectToPatch, "{2E107ABB-E77A-4188-AC32-4CA8EB3C5BD1}", CommonPatch); AZ_CLASS_ALLOCATOR(DifferentObjectToPatch, AZ::SystemAllocator, 0); float m_data; static void Reflect(AZ::SerializeContext& sc) { sc.Class()-> Field("m_data", &DifferentObjectToPatch::m_data); } }; class ObjectsWithGenerics { public: AZ_CLASS_ALLOCATOR(ObjectsWithGenerics, SystemAllocator, 0); AZ_TYPE_INFO(ObjectsWithGenerics, "{DE1EE15F-3458-40AE-A206-C6C957E2432B}"); static void Reflect(AZ::SerializeContext& sc) { sc.Class()-> Field("m_string", &ObjectsWithGenerics::m_string); } AZStd::string m_string; }; class ObjectWithPointer { public: AZ_CLASS_ALLOCATOR(ObjectWithPointer, SystemAllocator, 0); AZ_TYPE_INFO(ObjectWithPointer, "{D1FD3240-A7C5-4EA3-8E55-CD18193162B8}"); static void Reflect(AZ::SerializeContext& sc) { sc.Class() ->Field("m_int", &ObjectWithPointer::m_int) ->Field("m_pointerInt", &ObjectWithPointer::m_pointerInt) ; } AZ::s32 m_int; AZ::s32* m_pointerInt = nullptr; }; class ObjectWithMultiPointers { public: AZ_CLASS_ALLOCATOR(ObjectWithMultiPointers, SystemAllocator, 0); AZ_TYPE_INFO(ObjectWithMultiPointers, "{EBA25BFA-CFA0-4397-929C-A765BA72DE28}"); static void Reflect(AZ::SerializeContext& sc) { sc.Class() ->Field("m_int", &ObjectWithMultiPointers::m_int) ->Field("m_pointerInt", &ObjectWithMultiPointers::m_pointerInt) ->Field("m_pointerFloat", &ObjectWithMultiPointers::m_pointerFloat) ; } AZ::s32 m_int; AZ::s32* m_pointerInt = nullptr; float* m_pointerFloat = nullptr; }; static AZStd::string IntToString(int) { AZ_Assert(false, "Version 0 Type Converter for ObjectWithNumericFieldV1 should never be called"); return {}; }; // If the version 1 to 2 version converter ran // A sentinel value of 32.0 is always returned which can be represented exactly // in floating point(power of 2) static double IntToDouble(int) { return 32.0; }; struct ObjectWithNumericFieldV1 { AZ_CLASS_ALLOCATOR(ObjectWithNumericFieldV1, SystemAllocator, 0); AZ_TYPE_INFO(ObjectWithNumericFieldV1, "{556A83B0-77BC-41D1-B3BC-C1CD0A4F5845}"); static void Reflect(AZ::ReflectContext* reflectContext) { if (auto serializeContext = azrtti_cast(reflectContext)) { serializeContext->Class() ->Version(1) ->Field("IntValue", &ObjectWithNumericFieldV1::m_value) //! Provide a name converter for Version 0 -> 1 //! This should never be called as there is no Version 0 of this class //! It is here to validate that it is never called ->NameChange(0, 1, "IntValue", "NameValueThatShouldNeverBeSet") //! Version 0 -> 1 type converter should never be called ->TypeChange("IntValue", 0, 1, AZStd::function(&IntToString)) ; } } int m_value{}; }; struct ObjectWithNumericFieldV2 { AZ_CLASS_ALLOCATOR(ObjectWithNumericFieldV2, SystemAllocator, 0); AZ_TYPE_INFO(ObjectWithNumericFieldV2, "{556A83B0-77BC-41D1-B3BC-C1CD0A4F5845}"); static void Reflect(AZ::ReflectContext* reflectContext) { if (auto serializeContext = azrtti_cast(reflectContext)) { serializeContext->Class() ->Version(2) ->Field("DoubleValue", &ObjectWithNumericFieldV2::m_value) //! Provide a name converter for Version 0 -> 1 //! This should never be called as there is no Version 0 of this class //! It is here to validate that it is never called ->NameChange(0, 1, "IntValue", "NameValueThatShouldNeverBeSet") //! Version 0 -> 1 type converter should never be called ->TypeChange("IntValue", 0, 1, AZStd::function(&IntToString)) ->NameChange(1, 2, "IntValue", "DoubleValue") ->TypeChange("IntValue", 1, 2, AZStd::function(&IntToDouble)) ; } } double m_value{}; }; class InnerObjectFieldConverterV1 { public: AZ_CLASS_ALLOCATOR(InnerObjectFieldConverterV1, SystemAllocator, 0); AZ_RTTI(InnerObjectFieldConverterV1, "{28E61B17-F321-4D4E-9F4C-00846C6631DE}"); virtual ~InnerObjectFieldConverterV1() = default; static void Reflect(AZ::ReflectContext* reflectContext) { if (auto serializeContext = azrtti_cast(reflectContext)) { serializeContext->Class() ->Version(1) ->Field("InnerBaseStringField", &InnerObjectFieldConverterV1::m_stringField) ->Field("InnerBaseStringVector", &InnerObjectFieldConverterV1::m_stringVector) ; } } //! AZStd::string uses IDataSerializer for Serialization. //! This is to test Field Converters for patched element that are on a class that is a descendant element of the patched class AZStd::string m_stringField; //! AZStd::string uses IDataSerializer for Serialization. //! This is to test Field Converters for patched element that are on a class that is a descendant element of the patched class AZStd::vector m_stringVector; }; template class InnerObjectFieldConverterDerivedV1Template : public BaseClass { public: AZ_CLASS_ALLOCATOR(InnerObjectFieldConverterDerivedV1Template, SystemAllocator, 0); AZ_RTTI(InnerObjectFieldConverterDerivedV1Template, "{C68BE9B8-33F8-4969-B521-B44F5BA1C0DE}", BaseClass); static void Reflect(AZ::ReflectContext* reflectContext) { if (auto serializeContext = azrtti_cast(reflectContext)) { serializeContext->Class() ->Version(1) ->Field("InnerDerivedNumericField", &InnerObjectFieldConverterDerivedV1Template::m_objectWithNumericField) ; } } //! ObjectWithNumericFieldV1 uses the normal SerializeContext::ClassBulder for for Serialization. //! This is to test Field Converters for a patched element serialized in a pointer to the base class ObjectWithNumericFieldV1 m_objectWithNumericField; }; using InnerObjectFieldConverterDerivedV1 = InnerObjectFieldConverterDerivedV1Template; class ObjectFieldConverterV1 { public: AZ_CLASS_ALLOCATOR(ObjectFieldConverterV1, SystemAllocator, 0); AZ_TYPE_INFO(ObjectFieldConverterV1, "{5722C4E4-25DE-48C5-BC89-0EE9D38DF433}"); static void Reflect(AZ::ReflectContext* reflectContext) { if (auto serializeContext = azrtti_cast(reflectContext)) { serializeContext->Class() ->Version(1) ->Field("RootStringField", &ObjectFieldConverterV1::m_rootStringField) ->Field("RootStringVector", &ObjectFieldConverterV1::m_rootStringVector) ->Field("RootInnerObjectValue", &ObjectFieldConverterV1::m_rootInnerObject) ->Field("RootInnerObjectPointer", &ObjectFieldConverterV1::m_baseInnerObjectPolymorphic) ; } } //! AZStd::string uses IDataSerializer for Serialization. //! This is to test Field Converters for patched element that are directly on the patched class AZStd::string m_rootStringField; //! AZStd::vector uses IDataContainer for Serialization. The inner AZStd::string class uses IDataSerializer for serialization //! This is to test Field Converters for patched element that are directly on the patched class AZStd::vector m_rootStringVector; //! AZStd::vector uses IDataContainer for Serialization. The inner AZStd::string class uses IDataSerializer for serialization //! This is to test Field Converters for patched element that are directly on the patched class InnerObjectFieldConverterV1 m_rootInnerObject{}; InnerObjectFieldConverterV1* m_baseInnerObjectPolymorphic{}; }; class ObjectBaseClass { public: AZ_CLASS_ALLOCATOR(ObjectBaseClass, SystemAllocator, 0); AZ_RTTI(ObjectBaseClass, "{9CFEC143-9C78-4566-A541-46F9CA6FE66E}"); virtual ~ObjectBaseClass() = default; static void Reflect(AZ::SerializeContext& sc) { sc.Class(); } }; class ObjectDerivedClass1 : public ObjectBaseClass { public: AZ_CLASS_ALLOCATOR(ObjectDerivedClass1, SystemAllocator, 0); AZ_RTTI(ObjectDerivedClass1, "{9D6502E8-999D-46B8-AF37-EAAA0D53385A}", ObjectBaseClass); static void Reflect(AZ::SerializeContext& sc) { sc.Class(); } }; class ObjectDerivedClass2 : public ObjectBaseClass { public: AZ_CLASS_ALLOCATOR(ObjectDerivedClass2, SystemAllocator, 0); AZ_RTTI(ObjectDerivedClass2, "{91D1812E-17A2-4BC3-A9A1-13196BE50803}", ObjectBaseClass); static void Reflect(AZ::SerializeContext& sc) { sc.Class(); } }; class ObjectDerivedClass3 : public ObjectBaseClass { public: AZ_CLASS_ALLOCATOR(ObjectDerivedClass3, SystemAllocator, 0); AZ_RTTI(ObjectDerivedClass2, "{E80E926B-5750-4E8D-80E0-D06057692847}", ObjectBaseClass); static void Reflect(AZ::SerializeContext& sc) { sc.Class(); } }; static bool ConvertDerivedClass2ToDerivedClass3(AZ::SerializeContext& context, AZ::SerializeContext::DataElementNode& classElement) { classElement.Convert(context, AZ::AzTypeInfo::Uuid()); return true; } class ObjectWithVectorOfBaseClasses { public: AZ_CLASS_ALLOCATOR(ObjectWithVectorOfBaseClasses, SystemAllocator, 0); AZ_TYPE_INFO(ObjectWithVectorOfBaseClasses, "{BC9D5346-1BC5-41C4-8CF0-7ACD96F7790F}"); static void Reflect(AZ::SerializeContext& sc) { sc.Class() ->Field("m_vectorOfBaseClasses", &ObjectWithVectorOfBaseClasses::m_vectorOfBaseClasses); } virtual ~ObjectWithVectorOfBaseClasses() { for (auto element : m_vectorOfBaseClasses) { delete element; } m_vectorOfBaseClasses.clear(); } AZStd::vector m_vectorOfBaseClasses; }; } class PatchingTest : public AllocatorsTestFixture { protected: void SetUp() override { AllocatorsFixture::SetUp(); m_serializeContext = AZStd::make_unique(); using namespace Patching; CommonPatch::Reflect(*m_serializeContext); ContainedObjectPersistentId::Reflect(*m_serializeContext); ContainedObjectDerivedPersistentId::Reflect(*m_serializeContext); ContainedObjectNoPersistentId::Reflect(*m_serializeContext); ObjectToPatch::Reflect(*m_serializeContext); DifferentObjectToPatch::Reflect(*m_serializeContext); ObjectsWithGenerics::Reflect(*m_serializeContext); ObjectWithPointer::Reflect(*m_serializeContext); ObjectWithMultiPointers::Reflect(*m_serializeContext); AZ::DataPatch::Reflect(m_serializeContext.get()); const SerializeContext::ClassData* addressTypeSerializerClassData = m_serializeContext.get()->FindClassData(azrtti_typeid()); AZ_Assert(addressTypeSerializerClassData, "AddressType class not reflected, required to run DataPatch Unit Tests"); m_addressTypeSerializer = static_cast(addressTypeSerializerClassData->m_serializer.get()); AZ_Assert(m_addressTypeSerializer, "AddressTypeSerializer not provided in class AddressType's reflection, required to run DataPatch Unit Tests"); } void TearDown() override { m_serializeContext.reset(); m_addressTypeSerializer = nullptr; AllocatorsFixture::TearDown(); } void LoadPatchFromXML(const AZStd::string_view& xmlSrc, DataPatch& patchDest) { AZ::IO::MemoryStream xmlStream(xmlSrc.data(), xmlSrc.size()); Utils::LoadObjectFromStreamInPlace(xmlStream, patchDest, m_serializeContext.get()); } void LoadPatchFromByteStream(const AZStd::vector& byteStreamSrc, DataPatch& patchDest) { AZ::IO::MemoryStream streamRead(byteStreamSrc.data(), byteStreamSrc.size()); AZ::Utils::LoadObjectFromStreamInPlace(streamRead, patchDest, m_serializeContext.get()); } void WritePatchToByteStream(const DataPatch& patchSrc, AZStd::vector& byteStreamDest) { byteStreamDest.clear(); AZ::IO::ByteContainerStream> streamWrite(&byteStreamDest); AZ::Utils::SaveObjectToStream(streamWrite, AZ::DataStream::ST_XML, &patchSrc, m_serializeContext.get()); } // Template XML that can be formatted for multiple tests // ObjectToPatch m_intValue override const char* m_XMLDataPatchV1AddressTypeIntOverrideTemplate = R"( )"; // Valid address for above XML for easy formatting of tests expected to pass const char* m_XMLDataPatchV1AddressTypeIntOverrideValidAddress = R"(int({72039442-EB38-4D42-A1AD-CB68F7E0EEF6})::m_intValue%s0%s)"; // ObjectToPatch m_objectArray overrides. Container is size 5. Can format the first address and each element's data and persistent ids const char* m_XMLDataPatchV1AddressTypeIntVectorOverrideTemplate = R"( )"; // Valid address for above XML for easy formatting of tests expected to pass const char* m_XMLDataPatchV1AddressTypeIntVectorOverrideValidAddress = R"(AZStd::vector({861A12B0-BD91-528E-9CEC-505246EE98DE})::m_objectArray%s0%sContainedObjectPersistentId({D0C4D19C-7EFF-4F93-A5F0-95F33FC855AA})#%i%s0%s)"; /* Builds a valid address for m_XMLDataPatchV1AddressTypeIntOverrideTemplate while allowing formatting of the path and version delimiters */ AZStd::string GetValidAddressForXMLDataPatchV1AddressTypeIntXML() { return AZStd::string::format(m_XMLDataPatchV1AddressTypeIntOverrideValidAddress, V1AddressTypeElementVersionDelimiter, V1AddressTypeElementPathDelimiter); } /* Sets the data value for the int type stored in m_XMLDataPatchV1AddressTypeIntOverrideTemplate's xml stream Can also optionally set the address, otherwise defaults to a valid address if testAddress is nullptr */ AZStd::string BuildXMLDataPatchV1AddressTypeIntXML(const char* testAddress, int intValue) { AZStd::string editableAddress; if (testAddress) { editableAddress = testAddress; } else { editableAddress = GetValidAddressForXMLDataPatchV1AddressTypeIntXML(); } return AZStd::string::format(m_XMLDataPatchV1AddressTypeIntOverrideTemplate, editableAddress.c_str(), intValue); } /* Sets the data values and persistentId values for the ContainedObjectPersistentId type stored in m_XMLDataPatchV1AddressTypeIntVectorOverrideTemplate's xml stream Can also optionally set the address of the first element otherwise defaults to a valid address if testAddress is nullptr The stream contains 5 elements within the vector */ AZStd::string BuildXMLDataPatchV1AddressTypeIntVectorXML(const char* testAddress, int dataModifier, int persistentIdModifier) { AZStd::string editableAddress; // Allow customization of our delimiters const char* versionDelimiter = V1AddressTypeElementVersionDelimiter; const char* pathDelimiter = V1AddressTypeElementPathDelimiter; // If a testAddress was supplied then use it. // Otherwise format with a valid path if (testAddress) { editableAddress = testAddress; } else { editableAddress = AZStd::string::format(m_XMLDataPatchV1AddressTypeIntVectorOverrideValidAddress, versionDelimiter, pathDelimiter, 4 + persistentIdModifier, versionDelimiter, pathDelimiter); } // Format our xml. // It is size 5 so we format the data and persistent IDs to match this size. return AZStd::string::format(m_XMLDataPatchV1AddressTypeIntVectorOverrideTemplate, editableAddress.c_str(), 4 + dataModifier, 4 + persistentIdModifier, versionDelimiter, pathDelimiter, 3 + persistentIdModifier, versionDelimiter, pathDelimiter, 3 + dataModifier, 3 + persistentIdModifier, versionDelimiter, pathDelimiter, 2 + persistentIdModifier, versionDelimiter, pathDelimiter, 2 + dataModifier, 2 + persistentIdModifier, versionDelimiter, pathDelimiter, 1 + persistentIdModifier, versionDelimiter, pathDelimiter, 1 + dataModifier, 1 + persistentIdModifier, versionDelimiter, pathDelimiter, 0 + persistentIdModifier, versionDelimiter, pathDelimiter, 0 + dataModifier, 0 + persistentIdModifier); } // Store each AddressTypeElement version's delimiters seperately from the class so our tests don't auto update to a new version if these delimiters change in V2+ static constexpr const char* V1AddressTypeElementPathDelimiter = "/"; static constexpr const char* V1AddressTypeElementVersionDelimiter = AZ::DataPatchInternal::AddressTypeElement::VersionDelimiter; // utf-8 for AZStd::unique_ptr m_serializeContext; DataPatchInternal::AddressTypeSerializer* m_addressTypeSerializer; }; namespace Patching { TEST_F(PatchingTest, UberTest) { ObjectToPatch sourceObj; sourceObj.m_intValue = 101; sourceObj.m_objectArray.push_back(); sourceObj.m_objectArray.push_back(); sourceObj.m_objectArray.push_back(); sourceObj.m_dynamicField.Set(aznew ContainedObjectNoPersistentId(40)); { // derived sourceObj.m_derivedObjectArray.push_back(); sourceObj.m_derivedObjectArray.push_back(); sourceObj.m_derivedObjectArray.push_back(); } // test generic containers with persistent ID sourceObj.m_objectArray[0].m_persistentId = 1; sourceObj.m_objectArray[0].m_data = 201; sourceObj.m_objectArray[1].m_persistentId = 2; sourceObj.m_objectArray[1].m_data = 202; sourceObj.m_objectArray[2].m_persistentId = 3; sourceObj.m_objectArray[2].m_data = 203; { // derived sourceObj.m_derivedObjectArray[0].m_persistentId = 1; sourceObj.m_derivedObjectArray[0].m_data = 2010; sourceObj.m_derivedObjectArray[1].m_persistentId = 2; sourceObj.m_derivedObjectArray[1].m_data = 2020; sourceObj.m_derivedObjectArray[2].m_persistentId = 3; sourceObj.m_derivedObjectArray[2].m_data = 2030; } ObjectToPatch targetObj; targetObj.m_intValue = 121; targetObj.m_objectArray.push_back(); targetObj.m_objectArray.push_back(); targetObj.m_objectArray.push_back(); targetObj.m_objectArray[0].m_persistentId = 1; targetObj.m_objectArray[0].m_data = 301; targetObj.m_dynamicField.Set(aznew ContainedObjectNoPersistentId(50)); { // derived targetObj.m_derivedObjectArray.push_back(); targetObj.m_derivedObjectArray.push_back(); targetObj.m_derivedObjectArray.push_back(); targetObj.m_derivedObjectArray[0].m_persistentId = 1; targetObj.m_derivedObjectArray[0].m_data = 3010; } // remove element 2 targetObj.m_objectArray[1].m_persistentId = 3; targetObj.m_objectArray[1].m_data = 303; { // derived targetObj.m_derivedObjectArray[1].m_persistentId = 3; targetObj.m_derivedObjectArray[1].m_data = 3030; } // add new element targetObj.m_objectArray[2].m_persistentId = 4; targetObj.m_objectArray[2].m_data = 304; { // derived targetObj.m_derivedObjectArray[2].m_persistentId = 4; targetObj.m_derivedObjectArray[2].m_data = 3040; } // insert lots of objects without persistent id targetObj.m_objectArrayNoPersistentId.resize(999); for (size_t i = 0; i < targetObj.m_objectArrayNoPersistentId.size(); ++i) { targetObj.m_objectArrayNoPersistentId[i].m_data = static_cast(i); } DataPatch patch; patch.Create(&sourceObj, &targetObj, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); // Compare the generated and original target object EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_intValue, targetObj.m_intValue); EXPECT_EQ(generatedObj->m_objectArray.size(), targetObj.m_objectArray.size()); EXPECT_EQ(generatedObj->m_objectArray[0].m_data, targetObj.m_objectArray[0].m_data); EXPECT_EQ(generatedObj->m_objectArray[0].m_persistentId, targetObj.m_objectArray[0].m_persistentId); EXPECT_EQ(generatedObj->m_objectArray[1].m_data, targetObj.m_objectArray[1].m_data); EXPECT_EQ(generatedObj->m_objectArray[1].m_persistentId, targetObj.m_objectArray[1].m_persistentId); EXPECT_EQ(generatedObj->m_objectArray[2].m_data, targetObj.m_objectArray[2].m_data); EXPECT_EQ(generatedObj->m_objectArray[2].m_persistentId, targetObj.m_objectArray[2].m_persistentId); EXPECT_EQ(50, generatedObj->m_dynamicField.Get()->m_data); { // derived EXPECT_EQ(generatedObj->m_derivedObjectArray.size(), targetObj.m_derivedObjectArray.size()); EXPECT_EQ(generatedObj->m_derivedObjectArray[0].m_data, targetObj.m_derivedObjectArray[0].m_data); EXPECT_EQ(generatedObj->m_derivedObjectArray[0].m_persistentId, targetObj.m_derivedObjectArray[0].m_persistentId); EXPECT_EQ(generatedObj->m_derivedObjectArray[1].m_data, targetObj.m_derivedObjectArray[1].m_data); EXPECT_EQ(generatedObj->m_derivedObjectArray[1].m_persistentId, targetObj.m_derivedObjectArray[1].m_persistentId); EXPECT_EQ(generatedObj->m_derivedObjectArray[2].m_data, targetObj.m_derivedObjectArray[2].m_data); EXPECT_EQ(generatedObj->m_derivedObjectArray[2].m_persistentId, targetObj.m_derivedObjectArray[2].m_persistentId); } // test that the relative order of elements without persistent ID is preserved EXPECT_EQ(generatedObj->m_objectArrayNoPersistentId.size(), targetObj.m_objectArrayNoPersistentId.size()); for (size_t i = 0; i < targetObj.m_objectArrayNoPersistentId.size(); ++i) { EXPECT_EQ(generatedObj->m_objectArrayNoPersistentId[i].m_data, targetObj.m_objectArrayNoPersistentId[i].m_data); } // \note do we need to add support for base class patching and recover for root elements with proper casting generatedObj->m_dynamicField.DestroyData(m_serializeContext.get()); targetObj.m_dynamicField.DestroyData(m_serializeContext.get()); sourceObj.m_dynamicField.DestroyData(m_serializeContext.get()); //delete generatedObj; } TEST_F(PatchingTest, PatchArray_RemoveAllObjects_DataPatchAppliesCorrectly) { // Init Source with arbitrary Persistent IDs and data ObjectToPatch sourceObj; sourceObj.m_objectArray.resize(999); for (size_t i = 0; i < sourceObj.m_objectArray.size(); ++i) { sourceObj.m_objectArray[i].m_persistentId = static_cast(i + 10); sourceObj.m_objectArray[i].m_data = static_cast(i + 200); } // Init empty Target ObjectToPatch targetObj; // Create and Apply Patch DataPatch patch; patch.Create(&sourceObj, &targetObj, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); // Test Phase EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_objectArray.size(), targetObj.m_objectArray.size()); EXPECT_TRUE(targetObj.m_objectArray.empty()); EXPECT_TRUE(generatedObj->m_objectArray.empty()); } TEST_F(PatchingTest, PatchArray_AddObjects_DataPatchAppliesCorrectly) { // Init empty Source ObjectToPatch sourceObj; // Init Target with arbitrary Persistent IDs and data ObjectToPatch targetObj; targetObj.m_objectArray.resize(999); for (size_t i = 0; i < targetObj.m_objectArray.size(); ++i) { targetObj.m_objectArray[i].m_persistentId = static_cast(i + 10); targetObj.m_objectArray[i].m_data = static_cast(i + 200); } // Create and Apply Patch DataPatch patch; patch.Create(&sourceObj, &targetObj, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); // Test Phase EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_objectArray.size(), targetObj.m_objectArray.size()); for (size_t i = 0; i < generatedObj->m_objectArray.size(); ++i) { EXPECT_EQ(generatedObj->m_objectArray[i].m_persistentId, targetObj.m_objectArray[i].m_persistentId); EXPECT_EQ(generatedObj->m_objectArray[i].m_data, targetObj.m_objectArray[i].m_data); } } TEST_F(PatchingTest, PatchArray_EditAllObjects_DataPatchAppliesCorrectly) { // Init Source and Target with arbitrary Persistent IDs (the same) and data (different) ObjectToPatch sourceObj; sourceObj.m_objectArray.resize(999); ObjectToPatch targetObj; targetObj.m_objectArray.resize(999); for (size_t i = 0; i < sourceObj.m_objectArray.size(); ++i) { sourceObj.m_objectArray[i].m_persistentId = static_cast(i + 10); sourceObj.m_objectArray[i].m_data = static_cast(i + 200); // Keep the Persistent IDs the same but change the data targetObj.m_objectArray[i].m_persistentId = sourceObj.m_objectArray[i].m_persistentId; targetObj.m_objectArray[i].m_data = sourceObj.m_objectArray[i].m_data + 100; } // Create and Apply Patch DataPatch patch; patch.Create(&sourceObj, &targetObj, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); // Test Phase EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_objectArray.size(), targetObj.m_objectArray.size()); for (size_t i = 0; i < generatedObj->m_objectArray.size(); ++i) { EXPECT_EQ(generatedObj->m_objectArray[i].m_persistentId, targetObj.m_objectArray[i].m_persistentId); EXPECT_EQ(generatedObj->m_objectArray[i].m_data, targetObj.m_objectArray[i].m_data); } } TEST_F(PatchingTest, PatchArray_AddRemoveEdit_DataPatchAppliesCorrectly) { // Init Source ObjectToPatch sourceObj; sourceObj.m_objectArray.resize(3); sourceObj.m_objectArray[0].m_persistentId = 1; sourceObj.m_objectArray[0].m_data = 201; sourceObj.m_objectArray[1].m_persistentId = 2; sourceObj.m_objectArray[1].m_data = 202; sourceObj.m_objectArray[2].m_persistentId = 3; sourceObj.m_objectArray[2].m_data = 203; // Init Target ObjectToPatch targetObj; targetObj.m_objectArray.resize(4); // Edit ID 1 targetObj.m_objectArray[0].m_persistentId = 1; targetObj.m_objectArray[0].m_data = 301; // Remove ID 2, do not edit ID 3 targetObj.m_objectArray[1].m_persistentId = 3; targetObj.m_objectArray[1].m_data = 203; // Add ID 4 and 5 targetObj.m_objectArray[2].m_persistentId = 4; targetObj.m_objectArray[2].m_data = 304; targetObj.m_objectArray[3].m_persistentId = 5; targetObj.m_objectArray[3].m_data = 305; // Create and Apply Patch DataPatch patch; patch.Create(&sourceObj, &targetObj, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); // Test Phase EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_objectArray.size(), targetObj.m_objectArray.size()); EXPECT_EQ(generatedObj->m_objectArray[0].m_persistentId, targetObj.m_objectArray[0].m_persistentId); EXPECT_EQ(generatedObj->m_objectArray[0].m_data, targetObj.m_objectArray[0].m_data); EXPECT_EQ(generatedObj->m_objectArray[1].m_persistentId, targetObj.m_objectArray[1].m_persistentId); EXPECT_EQ(generatedObj->m_objectArray[1].m_data, targetObj.m_objectArray[1].m_data); EXPECT_EQ(generatedObj->m_objectArray[2].m_persistentId, targetObj.m_objectArray[2].m_persistentId); EXPECT_EQ(generatedObj->m_objectArray[2].m_data, targetObj.m_objectArray[2].m_data); EXPECT_EQ(generatedObj->m_objectArray[3].m_persistentId, targetObj.m_objectArray[3].m_persistentId); EXPECT_EQ(generatedObj->m_objectArray[3].m_data, targetObj.m_objectArray[3].m_data); } TEST_F(PatchingTest, PatchArray_ObjectsHaveNoPersistentId_RemoveAllObjects_DataPatchAppliesCorrectly) { // Init Source ObjectToPatch sourceObj; sourceObj.m_objectArrayNoPersistentId.resize(999); for (size_t i = 0; i < sourceObj.m_objectArrayNoPersistentId.size(); ++i) { sourceObj.m_objectArrayNoPersistentId[i].m_data = static_cast(i); } // Init empty Target ObjectToPatch targetObj; // Create and Apply Patch DataPatch patch; patch.Create(&sourceObj, &targetObj, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); // Test Phase EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_objectArrayNoPersistentId.size(), targetObj.m_objectArrayNoPersistentId.size()); EXPECT_TRUE(targetObj.m_objectArrayNoPersistentId.empty()); EXPECT_TRUE(generatedObj->m_objectArrayNoPersistentId.empty()); } TEST_F(PatchingTest, PatchArray_ObjectsHaveNoPersistentId_AddObjects_DataPatchAppliesCorrectly) { // Init empty Source ObjectToPatch sourceObj; // Init Target ObjectToPatch targetObj; targetObj.m_objectArrayNoPersistentId.resize(999); for (size_t i = 0; i < targetObj.m_objectArrayNoPersistentId.size(); ++i) { targetObj.m_objectArrayNoPersistentId[i].m_data = static_cast(i); } // Create and Apply Patch DataPatch patch; patch.Create(&sourceObj, &targetObj, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); // Test Phase EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_objectArrayNoPersistentId.size(), targetObj.m_objectArrayNoPersistentId.size()); for (size_t i = 0; i < targetObj.m_objectArrayNoPersistentId.size(); ++i) { EXPECT_EQ(generatedObj->m_objectArrayNoPersistentId[i].m_data, targetObj.m_objectArrayNoPersistentId[i].m_data); } } TEST_F(PatchingTest, PatchArray_ObjectsHaveNoPersistentId_EditAllObjects_DataPatchAppliesCorrectly) { // Init Source ObjectToPatch sourceObj; sourceObj.m_objectArrayNoPersistentId.resize(999); for (size_t i = 0; i < sourceObj.m_objectArrayNoPersistentId.size(); ++i) { sourceObj.m_objectArrayNoPersistentId[i].m_data = static_cast(i); } // Init Target ObjectToPatch targetObj; targetObj.m_objectArrayNoPersistentId.resize(999); for (size_t i = 0; i < targetObj.m_objectArrayNoPersistentId.size(); ++i) { targetObj.m_objectArrayNoPersistentId[i].m_data = static_cast(i + 1); } // Create and Apply Patch DataPatch patch; patch.Create(&sourceObj, &targetObj, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); // Test Phase EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_objectArrayNoPersistentId.size(), targetObj.m_objectArrayNoPersistentId.size()); for (size_t i = 0; i < targetObj.m_objectArrayNoPersistentId.size(); ++i) { EXPECT_EQ(generatedObj->m_objectArrayNoPersistentId[i].m_data, targetObj.m_objectArrayNoPersistentId[i].m_data); } } TEST_F(PatchingTest, PatchArray_ObjectsHaveNoPersistentId_RemoveEdit_DataPatchAppliesCorrectly) { // Init Source ObjectToPatch sourceObj; sourceObj.m_objectArrayNoPersistentId.resize(4); sourceObj.m_objectArrayNoPersistentId[0].m_data = static_cast(1000); sourceObj.m_objectArrayNoPersistentId[1].m_data = static_cast(1001); sourceObj.m_objectArrayNoPersistentId[2].m_data = static_cast(1002); sourceObj.m_objectArrayNoPersistentId[3].m_data = static_cast(1003); // Init Target ObjectToPatch targetObj; targetObj.m_objectArrayNoPersistentId.resize(2); targetObj.m_objectArrayNoPersistentId[0].m_data = static_cast(2000); targetObj.m_objectArrayNoPersistentId[1].m_data = static_cast(2001); // Create and Apply Patch DataPatch patch; patch.Create(&sourceObj, &targetObj, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); // Test Phase EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_objectArrayNoPersistentId.size(), 2); EXPECT_EQ(generatedObj->m_objectArrayNoPersistentId.size(), targetObj.m_objectArrayNoPersistentId.size()); EXPECT_EQ(generatedObj->m_objectArrayNoPersistentId[0].m_data, targetObj.m_objectArrayNoPersistentId[0].m_data); EXPECT_EQ(generatedObj->m_objectArrayNoPersistentId[1].m_data, targetObj.m_objectArrayNoPersistentId[1].m_data); } TEST_F(PatchingTest, PatchUnorderedMap_ObjectsHaveNoPersistentId_RemoveAllObjects_DataPatchAppliesCorrectly) { // test generic containers without persistent ID (by index) // Init Source ObjectToPatch sourceObj; sourceObj.m_objectMap.emplace(1, aznew ContainedObjectNoPersistentId(401)); sourceObj.m_objectMap.emplace(2, aznew ContainedObjectNoPersistentId(402)); sourceObj.m_objectMap.emplace(3, aznew ContainedObjectNoPersistentId(403)); sourceObj.m_objectMap.emplace(4, aznew ContainedObjectNoPersistentId(404)); // Init empty Target ObjectToPatch targetObj; // Create and Apply Patch DataPatch patch; patch.Create(&sourceObj, &targetObj, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); // Test Phase EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_objectMap.size(), targetObj.m_objectMap.size()); EXPECT_TRUE(targetObj.m_objectMap.empty()); EXPECT_TRUE(generatedObj->m_objectMap.empty()); } TEST_F(PatchingTest, PatchUnorderedMap_ObjectsHaveNoPersistentId_AddObjects_DataPatchAppliesCorrectly) { // test generic containers without persistent ID (by index) // Init empty Source ObjectToPatch sourceObj; // Init Target ObjectToPatch targetObj; targetObj.m_objectMap.emplace(1, aznew ContainedObjectNoPersistentId(401)); targetObj.m_objectMap.emplace(2, aznew ContainedObjectNoPersistentId(402)); targetObj.m_objectMap.emplace(3, aznew ContainedObjectNoPersistentId(403)); targetObj.m_objectMap.emplace(4, aznew ContainedObjectNoPersistentId(404)); // Create and Apply Patch DataPatch patch; patch.Create(&sourceObj, &targetObj, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); // Test Phase EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_objectMap.size(), targetObj.m_objectMap.size()); EXPECT_EQ(generatedObj->m_objectMap[1]->m_data, targetObj.m_objectMap[1]->m_data); EXPECT_EQ(generatedObj->m_objectMap[2]->m_data, targetObj.m_objectMap[2]->m_data); EXPECT_EQ(generatedObj->m_objectMap[3]->m_data, targetObj.m_objectMap[3]->m_data); EXPECT_EQ(generatedObj->m_objectMap[4]->m_data, targetObj.m_objectMap[4]->m_data); } TEST_F(PatchingTest, PatchUnorderedMap_ObjectsHaveNoPersistentId_EditAllObjects_DataPatchAppliesCorrectly) { // test generic containers without persistent ID (by index) // Init Source ObjectToPatch sourceObj; sourceObj.m_objectMap.emplace(1, aznew ContainedObjectNoPersistentId(401)); sourceObj.m_objectMap.emplace(2, aznew ContainedObjectNoPersistentId(402)); sourceObj.m_objectMap.emplace(3, aznew ContainedObjectNoPersistentId(403)); sourceObj.m_objectMap.emplace(4, aznew ContainedObjectNoPersistentId(404)); // Init Target ObjectToPatch targetObj; targetObj.m_objectMap.emplace(1, aznew ContainedObjectNoPersistentId(501)); targetObj.m_objectMap.emplace(2, aznew ContainedObjectNoPersistentId(502)); targetObj.m_objectMap.emplace(3, aznew ContainedObjectNoPersistentId(503)); targetObj.m_objectMap.emplace(4, aznew ContainedObjectNoPersistentId(504)); // Create and Apply Patch DataPatch patch; patch.Create(&sourceObj, &targetObj, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); // Test Phase EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_objectMap.size(), targetObj.m_objectMap.size()); EXPECT_EQ(generatedObj->m_objectMap[1]->m_data, targetObj.m_objectMap[1]->m_data); EXPECT_EQ(generatedObj->m_objectMap[2]->m_data, targetObj.m_objectMap[2]->m_data); EXPECT_EQ(generatedObj->m_objectMap[3]->m_data, targetObj.m_objectMap[3]->m_data); EXPECT_EQ(generatedObj->m_objectMap[4]->m_data, targetObj.m_objectMap[4]->m_data); } TEST_F(PatchingTest, PatchUnorderedMap_ObjectsHaveNoPersistentId_AddRemoveEdit_DataPatchAppliesCorrectly) { // test generic containers without persistent ID (by index) // Init Source ObjectToPatch sourceObj; sourceObj.m_objectMap.emplace(1, aznew ContainedObjectNoPersistentId(401)); sourceObj.m_objectMap.emplace(2, aznew ContainedObjectNoPersistentId(402)); sourceObj.m_objectMap.emplace(3, aznew ContainedObjectNoPersistentId(403)); sourceObj.m_objectMap.emplace(4, aznew ContainedObjectNoPersistentId(404)); // Init Target ObjectToPatch targetObj; // This will mark the object at index 1 as an edit, objects 2-4 as removed, and 5 as an addition targetObj.m_objectMap.emplace(1, aznew ContainedObjectNoPersistentId(501)); targetObj.m_objectMap.emplace(5, aznew ContainedObjectNoPersistentId(405)); // Create and Apply Patch DataPatch patch; patch.Create(&sourceObj, &targetObj, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); // Test Phase EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_objectMap.size(), 2); EXPECT_EQ(generatedObj->m_objectMap.size(), targetObj.m_objectMap.size()); EXPECT_EQ(generatedObj->m_objectMap[1]->m_data, targetObj.m_objectMap[1]->m_data); EXPECT_EQ(generatedObj->m_objectMap[5]->m_data, targetObj.m_objectMap[5]->m_data); } TEST_F(PatchingTest, ReplaceRootElement_DifferentObjects_DataPatchAppliesCorrectly) { ObjectToPatch obj1; DifferentObjectToPatch obj2; obj1.m_intValue = 99; obj2.m_data = 3.33f; DataPatch patch1; patch1.Create(static_cast(&obj1), static_cast(&obj2), DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); // cast to base classes DifferentObjectToPatch* obj2Generated = patch1.Apply(&obj1, m_serializeContext.get()); EXPECT_EQ(obj2.m_data, obj2Generated->m_data); delete obj2Generated; } TEST_F(PatchingTest, CompareWithGenerics_DifferentObjects_DataPatchAppliesCorrectly) { ObjectsWithGenerics sourceGeneric; sourceGeneric.m_string = "Hello"; ObjectsWithGenerics targetGeneric; targetGeneric.m_string = "Ola"; DataPatch genericPatch; genericPatch.Create(&sourceGeneric, &targetGeneric, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); ObjectsWithGenerics* targerGenericGenerated = genericPatch.Apply(&sourceGeneric, m_serializeContext.get()); EXPECT_EQ(targetGeneric.m_string, targerGenericGenerated->m_string); delete targerGenericGenerated; } TEST_F(PatchingTest, CompareIdentical_DataPatchIsEmpty) { ObjectToPatch sourceObj; ObjectToPatch targetObj; // Patch without overrides should be empty DataPatch patch; patch.Create(&sourceObj, &targetObj, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); EXPECT_FALSE(patch.IsData()); } TEST_F(PatchingTest, CompareIdenticalWithForceOverride_DataPatchHasData) { ObjectToPatch sourceObj; ObjectToPatch targetObj; DataPatch::AddressType forceOverrideAddress; forceOverrideAddress.emplace_back(AZ_CRC("m_intValue")); DataPatch::FlagsMap sourceFlagsMap; DataPatch::FlagsMap targetFlagsMap; targetFlagsMap.emplace(forceOverrideAddress, DataPatch::Flag::ForceOverrideSet); DataPatch patch; patch.Create(&sourceObj, &targetObj, sourceFlagsMap, targetFlagsMap, m_serializeContext.get()); EXPECT_TRUE(patch.IsData()); } TEST_F(PatchingTest, ChangeSourceAfterForceOverride_TargetDataUnchanged) { ObjectToPatch sourceObj; ObjectToPatch targetObj; DataPatch::AddressType forceOverrideAddress; forceOverrideAddress.emplace_back(AZ_CRC("m_intValue")); DataPatch::FlagsMap sourceFlagsMap; DataPatch::FlagsMap targetFlagsMap; targetFlagsMap.emplace(forceOverrideAddress, DataPatch::Flag::ForceOverrideSet); DataPatch patch; patch.Create(&sourceObj, &targetObj, sourceFlagsMap, targetFlagsMap, m_serializeContext.get()); // change source after patch is created sourceObj.m_intValue = 5; AZStd::unique_ptr targetObj2(patch.Apply(&sourceObj, m_serializeContext.get())); EXPECT_EQ(targetObj.m_intValue, targetObj2->m_intValue); } TEST_F(PatchingTest, ForceOverrideAndPreventOverrideBothSet_DataPatchIsEmpty) { ObjectToPatch sourceObj; ObjectToPatch targetObj; targetObj.m_intValue = 43; DataPatch::AddressType forceOverrideAddress; forceOverrideAddress.emplace_back(AZ_CRC("m_intValue")); DataPatch::FlagsMap sourceFlagsMap; sourceFlagsMap.emplace(forceOverrideAddress, DataPatch::Flag::PreventOverrideSet); DataPatch::FlagsMap targetFlagsMap; targetFlagsMap.emplace(forceOverrideAddress, DataPatch::Flag::ForceOverrideSet); DataPatch patch; patch.Create(&sourceObj, &targetObj, sourceFlagsMap, targetFlagsMap, m_serializeContext.get()); EXPECT_FALSE(patch.IsData()); } TEST_F(PatchingTest, PreventOverrideOnSource_BlocksValueFromPatch) { // targetObj is different from sourceObj ObjectToPatch sourceObj; ObjectToPatch targetObj; targetObj.m_intValue = 5; // create patch from sourceObj -> targetObj DataPatch patch; patch.Create(&sourceObj, &targetObj, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); // create flags that prevent m_intValue from being patched DataPatch::AddressType forceOverrideAddress; forceOverrideAddress.emplace_back(AZ_CRC("m_intValue")); DataPatch::FlagsMap sourceFlagsMap; sourceFlagsMap.emplace(forceOverrideAddress, DataPatch::Flag::PreventOverrideSet); DataPatch::FlagsMap targetFlagsMap; // m_intValue should be the same as it was in sourceObj AZStd::unique_ptr targetObj2(patch.Apply(&sourceObj, m_serializeContext.get(), ObjectStream::FilterDescriptor(), sourceFlagsMap, targetFlagsMap)); EXPECT_EQ(sourceObj.m_intValue, targetObj2->m_intValue); } TEST_F(PatchingTest, PreventOverrideOnTarget_DoesntAffectPatching) { // targetObj is different from sourceObj ObjectToPatch sourceObj; ObjectToPatch targetObj; targetObj.m_intValue = 5; // create patch from sourceObj -> targetObj DataPatch patch; patch.Create(&sourceObj, &targetObj, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); // create flags that prevent m_intValue from being patched, but put them on the target instead of source DataPatch::AddressType forceOverrideAddress; forceOverrideAddress.emplace_back(AZ_CRC("m_intValue")); DataPatch::FlagsMap sourceFlagsMap; DataPatch::FlagsMap targetFlagsMap; targetFlagsMap.emplace(forceOverrideAddress, DataPatch::Flag::PreventOverrideSet); // m_intValue should have been patched AZStd::unique_ptr targetObj2(patch.Apply(&sourceObj, m_serializeContext.get(), ObjectStream::FilterDescriptor(), sourceFlagsMap, targetFlagsMap)); EXPECT_EQ(targetObj.m_intValue, targetObj2->m_intValue); } TEST_F(PatchingTest, PatchNullptrInSource) { ObjectWithPointer sourceObj; sourceObj.m_int = 7; ObjectWithPointer targetObj; targetObj.m_int = 8; targetObj.m_pointerInt = new AZ::s32(-1); DataPatch patch; patch.Create(&sourceObj, &targetObj, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); ObjectWithPointer* patchedTargetObj = patch.Apply(&sourceObj, m_serializeContext.get()); EXPECT_EQ(targetObj.m_int, patchedTargetObj->m_int); EXPECT_NE(nullptr, patchedTargetObj->m_pointerInt); EXPECT_EQ(*targetObj.m_pointerInt, *patchedTargetObj->m_pointerInt); delete targetObj.m_pointerInt; azdestroy(patchedTargetObj->m_pointerInt); delete patchedTargetObj; } TEST_F(PatchingTest, PatchNullptrInTarget) { ObjectWithPointer sourceObj; sourceObj.m_int = 20; sourceObj.m_pointerInt = new AZ::s32(500); ObjectWithPointer targetObj; targetObj.m_int = 23054; DataPatch patch; patch.Create(&sourceObj, &targetObj, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); ObjectWithPointer* patchedTargetObj = patch.Apply(&sourceObj, m_serializeContext.get()); EXPECT_EQ(targetObj.m_int, patchedTargetObj->m_int); EXPECT_EQ(nullptr, patchedTargetObj->m_pointerInt); delete sourceObj.m_pointerInt; delete patchedTargetObj; } // prove that properly deprecated container elements are removed and do not leave nulls behind. TEST_F(PatchingTest, DeprecatedContainerElements_AreRemoved) { ObjectBaseClass::Reflect(*m_serializeContext); ObjectDerivedClass1::Reflect(*m_serializeContext); ObjectDerivedClass2::Reflect(*m_serializeContext); ObjectWithVectorOfBaseClasses::Reflect(*m_serializeContext); // step 1: Make a patch that includes both classes. ObjectWithVectorOfBaseClasses sourceObject; ObjectWithVectorOfBaseClasses targetObject; targetObject.m_vectorOfBaseClasses.push_back(new ObjectDerivedClass1()); targetObject.m_vectorOfBaseClasses.push_back(new ObjectDerivedClass2()); targetObject.m_vectorOfBaseClasses.push_back(new ObjectDerivedClass2()); targetObject.m_vectorOfBaseClasses.push_back(new ObjectDerivedClass2()); targetObject.m_vectorOfBaseClasses.push_back(new ObjectDerivedClass1()); // <-- we expect to see this second one, it should not be lost DataPatch patch; patch.Create(&sourceObject, &targetObject, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); // step 2: DerivedClass2 no longer exists: m_serializeContext->EnableRemoveReflection(); ObjectDerivedClass2::Reflect(*m_serializeContext); m_serializeContext->DisableRemoveReflection(); m_serializeContext->ClassDeprecate("ObjectDerivedClass2", azrtti_typeid()); // generate a patch which will turn a given source object into the targetObject. ObjectWithVectorOfBaseClasses* patchedTargetObj = patch.Apply(&sourceObject, m_serializeContext.get()); // at this point, the patched target object should only have ObjectDerivedClass1s on it. // two of them exactly. There should be no other types and there should be no null holes in it. EXPECT_EQ(patchedTargetObj->m_vectorOfBaseClasses.size(), 2); for (auto element : patchedTargetObj->m_vectorOfBaseClasses) { EXPECT_EQ(azrtti_typeid(*element), azrtti_typeid() ); } delete patchedTargetObj; } // prove that unreadable container elements (ie, no deprecation info) generate warnings but also // do not leave nulls behind. TEST_F(PatchingTest, UnreadableContainerElements_WithNoDeprecation_GenerateWarning_AreRemoved) { ObjectBaseClass::Reflect(*m_serializeContext); ObjectDerivedClass1::Reflect(*m_serializeContext); ObjectDerivedClass2::Reflect(*m_serializeContext); ObjectWithVectorOfBaseClasses::Reflect(*m_serializeContext); // Make a patch that includes both classes. ObjectWithVectorOfBaseClasses sourceObject; ObjectWithVectorOfBaseClasses targetObject; targetObject.m_vectorOfBaseClasses.push_back(new ObjectDerivedClass1()); targetObject.m_vectorOfBaseClasses.push_back(new ObjectDerivedClass2()); targetObject.m_vectorOfBaseClasses.push_back(new ObjectDerivedClass2()); targetObject.m_vectorOfBaseClasses.push_back(new ObjectDerivedClass2()); targetObject.m_vectorOfBaseClasses.push_back(new ObjectDerivedClass1()); // <-- we expect to see this second one, it should not be lost DataPatch patch; patch.Create(&sourceObject, &targetObject, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); // Remove DerivedClass2 from the serialize context: m_serializeContext->EnableRemoveReflection(); ObjectDerivedClass2::Reflect(*m_serializeContext); m_serializeContext->DisableRemoveReflection(); // apply the patch despite it containing deprecated things with no deprecation tag, expect 1 error per unknown instance: AZ_TEST_START_TRACE_SUPPRESSION; ObjectWithVectorOfBaseClasses* patchedTargetObj = patch.Apply(&sourceObject, m_serializeContext.get()); AZ_TEST_STOP_TRACE_SUPPRESSION(0); // at this point, the patched target object should only have ObjectDerivedClass1s on it. // two of them exactly. There should be no other types and there should be no null holes in it. EXPECT_EQ(patchedTargetObj->m_vectorOfBaseClasses.size(), 2); for (auto element : patchedTargetObj->m_vectorOfBaseClasses) { EXPECT_EQ(azrtti_typeid(*element), azrtti_typeid() ); } delete patchedTargetObj; } // note that the entire conversion subsystem is based on loading through an ObjectStream, not a direct patch. // It is not a real use case to deprecate a class during execution and then expect data patch upgrading to function. // Instead, deprecated classes always come from data "at rest" such as on disk / network stream, which means // they come via ObjectStream, which does perform conversion and has its own tests. // This test is just to ensure that when you do load a patch (Using ObjectStream) and elements in that patch have been // deprecated, it does not cause unexpected errors. TEST_F(PatchingTest, UnreadableContainerElements_WithDeprecationConverters_AreConverted) { ObjectBaseClass::Reflect(*m_serializeContext); ObjectDerivedClass1::Reflect(*m_serializeContext); ObjectDerivedClass2::Reflect(*m_serializeContext); ObjectDerivedClass3::Reflect(*m_serializeContext); ObjectWithVectorOfBaseClasses::Reflect(*m_serializeContext); // step 1: Make a patch that includes deprecated classes. ObjectWithVectorOfBaseClasses sourceObject; ObjectWithVectorOfBaseClasses targetObject; targetObject.m_vectorOfBaseClasses.push_back(new ObjectDerivedClass1()); targetObject.m_vectorOfBaseClasses.push_back(new ObjectDerivedClass2()); targetObject.m_vectorOfBaseClasses.push_back(new ObjectDerivedClass1()); targetObject.m_vectorOfBaseClasses.push_back(new ObjectDerivedClass2()); DataPatch patch; patch.Create(&sourceObject, &targetObject, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); // save the patch itself to a stream. AZStd::vector charBuffer; AZ::IO::ByteContainerStream > containerStream(&charBuffer); bool success = AZ::Utils::SaveObjectToStream(containerStream, AZ::ObjectStream::ST_XML, &patch, m_serializeContext.get()); EXPECT_TRUE(success); // step 2: DerivedClass2 no longer exists: m_serializeContext->EnableRemoveReflection(); ObjectDerivedClass2::Reflect(*m_serializeContext); m_serializeContext->DisableRemoveReflection(); m_serializeContext->ClassDeprecate("Dummy UUID", azrtti_typeid(), ConvertDerivedClass2ToDerivedClass3); // load it from the container DataPatch loadedPatch; // it should generate no warnings but the deprecated ones should not be there. success = AZ::Utils::LoadObjectFromBufferInPlace(charBuffer.data(), charBuffer.size(), loadedPatch, m_serializeContext.get()); EXPECT_TRUE(success); // patch the original source object with the new patch which was loaded: ObjectWithVectorOfBaseClasses* patchedTargetObj = loadedPatch.Apply(&sourceObject, m_serializeContext.get()); // prove that all deprecated classes were converted and order did not shuffle: ASSERT_EQ(patchedTargetObj->m_vectorOfBaseClasses.size(), 4); EXPECT_EQ(azrtti_typeid(patchedTargetObj->m_vectorOfBaseClasses[0]), azrtti_typeid() ); EXPECT_EQ(azrtti_typeid(patchedTargetObj->m_vectorOfBaseClasses[1]), azrtti_typeid() ); EXPECT_EQ(azrtti_typeid(patchedTargetObj->m_vectorOfBaseClasses[2]), azrtti_typeid() ); EXPECT_EQ(azrtti_typeid(patchedTargetObj->m_vectorOfBaseClasses[3]), azrtti_typeid() ); delete patchedTargetObj; } TEST_F(PatchingTest, PatchDistinctNullptrSourceTarget) { ObjectWithMultiPointers sourceObj; sourceObj.m_int = 54; sourceObj.m_pointerInt = new AZ::s32(500); ObjectWithMultiPointers targetObj; targetObj.m_int = -2493; targetObj.m_pointerFloat = new float(3.14f); DataPatch patch; patch.Create(&sourceObj, &targetObj, DataPatch::FlagsMap(), DataPatch::FlagsMap(), m_serializeContext.get()); ObjectWithMultiPointers* patchedTargetObj = patch.Apply(&sourceObj, m_serializeContext.get()); EXPECT_EQ(targetObj.m_int, patchedTargetObj->m_int); EXPECT_EQ(nullptr, patchedTargetObj->m_pointerInt); EXPECT_NE(nullptr, patchedTargetObj->m_pointerFloat); EXPECT_EQ(*targetObj.m_pointerFloat, *patchedTargetObj->m_pointerFloat); delete sourceObj.m_pointerInt; delete targetObj.m_pointerFloat; delete patchedTargetObj->m_pointerInt; azdestroy(patchedTargetObj->m_pointerFloat); delete patchedTargetObj; } TEST_F(PatchingTest, Apply_LegacyDataPatchWithValidValueOverride_ApplySucceeds_FT) { // A Legacy DataPatch containing an int set to 150 AZStd::string_view legacyPatchXML = R"( )"; // Load the patch from XML // This triggers the Legacy DataPatch converter // Patch Data will be wrapped in the StreamWrapper type until Apply is called // Apply provides the remaining class data to complete the conversion DataPatch patch; LoadPatchFromXML(legacyPatchXML, patch); // Apply the patch and complete conversion ObjectToPatch sourceObj; AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); // Verify the patch applied as expected EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_intValue, 150); } TEST_F(PatchingTest, Apply_LegacyDataPatchWithValidValueOverride_LegacyPatchUsesObjectStreamVersion1_ApplySucceeds_FT) { // A Legacy DataPatch containing an int set to 180 and using ObjectStream V1 types for the unordered map, pair, and bytestream. // Note: Does not use legacy types in the patch themselves (EX: a patched AZStd::string will use it's V3 typeId not V1) AZStd::string_view legacyPatchXML = R"( )"; // Load the patch from XML // This triggers the Legacy DataPatch converter // Patch Data will be wrapped in the StreamWrapper type until Apply is called // Apply provides the remaining class data to complete the conversion DataPatch patch; LoadPatchFromXML(legacyPatchXML, patch); // Apply the patch and complete conversion ObjectToPatch sourceObj; AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); // Verify the patch applied as expected EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_intValue, 180); } TEST_F(PatchingTest, Apply_LegacyDataPatchWithValidPointerOverride_ApplySucceeds_FT) { // A Legacy DataPatch containing a pointer to an int set to 56 AZStd::string_view legacyPatchXML = R"( )"; // Load the patch from XML // This triggers the Legacy DataPatch converter // Patch Data will be wrapped in the StreamWrapper type until Apply is called // Apply provides the remaining class data to complete the conversion DataPatch patch; LoadPatchFromXML(legacyPatchXML, patch); // Apply the patch and complete conversion ObjectWithPointer sourceObj; AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); // Verify the patch applied as expected EXPECT_TRUE(generatedObj); EXPECT_TRUE(generatedObj->m_pointerInt); EXPECT_EQ(*generatedObj->m_pointerInt, 56); azdestroy(generatedObj->m_pointerInt); } TEST_F(PatchingTest, Apply_LegacyDataPatchWithValidPointerOverride_LegacyPatchUsesObjectStreamVersion1_ApplySucceeds_FT) { // A Legacy DataPatch containing a pointer to an int set to 74 and using ObjectStream V1 types for the unordered map, pair, and bytestream. // Note: Does not use legacy types in the patch themselves (EX: a patched AZStd::string will use it's V3 typeId not V1) AZStd::string_view legacyPatchXML = R"( )"; // Load the patch from XML // This triggers the Legacy DataPatch converter // Patch Data will be wrapped in the StreamWrapper type until Apply is called // Apply provides the remaining class data to complete the conversion DataPatch patch; LoadPatchFromXML(legacyPatchXML, patch); // Apply the patch and complete conversion ObjectWithPointer sourceObj; AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); // Verify the patch applied as expected EXPECT_TRUE(generatedObj); EXPECT_TRUE(generatedObj->m_pointerInt); EXPECT_EQ(*generatedObj->m_pointerInt, 74); azdestroy(generatedObj->m_pointerInt); } TEST_F(PatchingTest, Apply_LegacyDataPatchWithValidContainerOverride_ApplySucceeds_FT) { // A Legacy DataPatch containing a vector of 5 objects with incrementing values and persistent Ids AZStd::string_view legacyPatchXML = R"( )"; // Load the patch from XML // This triggers the Legacy DataPatch converter // Patch Data will be wrapped in the StreamWrapper type until Apply is called // Apply provides the remaining class data to complete the conversion DataPatch patch; LoadPatchFromXML(legacyPatchXML, patch); // Apply the Patch and complete conversion ObjectToPatch sourceObj; AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); constexpr int expectedSize = 5; constexpr int persistentIdOffset = 10; constexpr int dataOffset = 200; // Verify the patch applied as expected for each value in the patched array EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_objectArray.size(), expectedSize); for (int arrayIndex = 0; arrayIndex < expectedSize; ++arrayIndex) { EXPECT_EQ(generatedObj->m_objectArray[arrayIndex].m_persistentId, arrayIndex + persistentIdOffset); EXPECT_EQ(generatedObj->m_objectArray[arrayIndex].m_data, arrayIndex + dataOffset); } } TEST_F(PatchingTest, Apply_LegacyDataPatchWithValidContainerOverride_LegacyPatchUsesObjectStreamVersion1_ApplySucceeds_FT) { // A Legacy DataPatch containing a vector of 5 objects with incrementing values and persistent Ids // Using ObjectStream V1 types for the unordered map, pair, and bytestream. // Note: Does not use legacy types in the patch themselves (EX: a patched AZStd::string will use it's V3 typeId not V1) AZStd::string_view legacyPatchXML = R"( )"; // Load the patch from XML // This triggers the Legacy DataPatch converter // Patch Data will be wrapped in the StreamWrapper type until Apply is called // Apply provides the remaining class data to complete the conversion DataPatch patch; LoadPatchFromXML(legacyPatchXML, patch); // Apply the patch and complete conversion ObjectToPatch sourceObj; AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); constexpr int expectedSize = 5; constexpr int persistentIdOffset = 10; constexpr int dataOffset = 200; // Verify the patch applied as expected for each value in the patched array EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_objectArray.size(), expectedSize); for (int arrayIndex = 0; arrayIndex < expectedSize; ++arrayIndex) { EXPECT_EQ(generatedObj->m_objectArray[arrayIndex].m_persistentId, arrayIndex + persistentIdOffset); EXPECT_EQ(generatedObj->m_objectArray[arrayIndex].m_data, arrayIndex + dataOffset); } } TEST_F(PatchingTest, Apply_LegacyDataPatchWithValidGenericTypeOverride_ApplySucceeds_FT) { // A Legacy DataPatch containing a string set to "Hello World" AZStd::string_view legacyPatchXML = R"( )"; // Load the patch from XML // This triggers the Legacy DataPatch converter // Patch Data will be wrapped in the StreamWrapper type until Apply is called // Apply provides the remaining class data to complete the conversion DataPatch patch; LoadPatchFromXML(legacyPatchXML, patch); // Apply the patch and complete conversion ObjectsWithGenerics sourceObj; AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); constexpr const char* expectedString = "Hello World"; // Verify the patch applied as expected EXPECT_TRUE(generatedObj); EXPECT_STREQ(generatedObj->m_string.c_str(), expectedString); } TEST_F(PatchingTest, Apply_LegacyDataPatchWithValidGenericTypeOverride_LegacyPatchUsesObjectStreamVersion1_ApplySucceeds_FT) { // A Legacy DataPatch containing a string set to "Hello World" and using ObjectStream V1 types for the unordered map, pair, and bytestream. // Note: Does not use legacy types in the patch themselves (EX: a patched AZStd::string will use it's V3 typeId not V1) AZStd::string_view legacyPatchXML = R"( )"; // Load the patch from XML // This triggers the Legacy DataPatch converter // Patch Data will be wrapped in the StreamWrapper type until Apply is called // Apply provides the remaining class data to complete the conversion DataPatch patch; LoadPatchFromXML(legacyPatchXML, patch); // Apply the patch and complete conversion ObjectsWithGenerics sourceObj; AZStd::unique_ptr generatedObj(patch.Apply(&sourceObj, m_serializeContext.get())); const char* expectedString = "Hello World"; // Verify the patch applied as expected EXPECT_TRUE(generatedObj); EXPECT_STREQ(generatedObj->m_string.c_str(), expectedString); } TEST_F(PatchingTest, Apply_LegacyDataPatchAppliedTwice_OnSecondApplyPatchHasBeenConverted_BothPatchAppliesSucceed_FT) { // A dataPatch containing an int set to 22 AZStd::string_view legacyPatchXML = R"( )"; constexpr int expectedValue = 22; // Load the patch from stream DataPatch patch; LoadPatchFromXML(legacyPatchXML, patch); // Apply the patch ObjectToPatch sourceObj; AZStd::unique_ptr generatedObjFirstApply(patch.Apply(&sourceObj, m_serializeContext.get())); // Verify patch applied as expected EXPECT_TRUE(generatedObjFirstApply); EXPECT_EQ(generatedObjFirstApply->m_intValue, expectedValue); // Apply the patch again AZStd::unique_ptr generatedObjSecondApply(patch.Apply(&sourceObj, m_serializeContext.get())); // Verify patch applied successfully the second time EXPECT_TRUE(generatedObjSecondApply); EXPECT_EQ(generatedObjSecondApply->m_intValue, expectedValue); } TEST_F(PatchingTest, Apply_PatchWrittenToThenReadFromStreamBeforeApply_PatchApplySucceeds_FT) { ObjectToPatch source; ObjectToPatch target; constexpr int targetArraySize = 999; constexpr int targetValueScalar = 2; constexpr int persistentIdOffset = 100; // Build target array target.m_objectArray.resize(targetArraySize); for (size_t arrayIndex = 0; arrayIndex < target.m_objectArray.size(); ++arrayIndex) { target.m_objectArray[arrayIndex].m_data = static_cast(arrayIndex * targetValueScalar); target.m_objectArray[arrayIndex].m_persistentId = static_cast((arrayIndex * targetValueScalar) + persistentIdOffset); } // Create patch in memory DataPatch patch; patch.Create(&source, &target, AZ::DataPatch::FlagsMap(), AZ::DataPatch::FlagsMap(), m_serializeContext.get()); // Serialize patch into stream AZStd::vector streamBuffer; WritePatchToByteStream(patch, streamBuffer); // Load patch from stream DataPatch loadedPatch; LoadPatchFromByteStream(streamBuffer, loadedPatch); // Verify integrity of loaded patch EXPECT_TRUE(loadedPatch.IsValid() && loadedPatch.IsData()); // Apply the patch AZStd::unique_ptr generatedObj(loadedPatch.Apply(&source, m_serializeContext.get())); // Verify patch applied as expected EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_objectArray.size(), targetArraySize); for (int arrayIndex = 0; arrayIndex < targetArraySize; ++arrayIndex) { EXPECT_EQ(generatedObj->m_objectArray[arrayIndex].m_data, arrayIndex * targetValueScalar); EXPECT_EQ(generatedObj->m_objectArray[arrayIndex].m_persistentId, (arrayIndex * targetValueScalar) + persistentIdOffset); } } TEST_F(PatchingTest, Apply_LegacyPatchWrittenToThenReadFromStreamBeforeApply_PatchApplySucceeds_FT) { // A Legacy DataPatch containing an int set to 57 AZStd::string_view legacyPatchXML = R"( )"; // Load the patch from stream // Loading the legacy patch will run the converter // Patch Data will be wrapped in the StreamWrapper type until Apply is called // Apply provides the remaining class data to complete the conversion DataPatch patch; LoadPatchFromXML(legacyPatchXML, patch); // Serialize partially converted patch to stream AZStd::vector streamBuffer; WritePatchToByteStream(patch, streamBuffer); // Load partially converted patch from stream DataPatch loadedPatch; LoadPatchFromByteStream(streamBuffer, loadedPatch); // Verify integrity of loaded patch EXPECT_TRUE(loadedPatch.IsValid() && loadedPatch.IsData()); // Apply the patch ObjectToPatch source; AZStd::unique_ptr generatedObj(loadedPatch.Apply(&source, m_serializeContext.get())); // Verify the patch applied as expected EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_intValue, 57); } TEST_F(PatchingTest, Apply_LegacyPatchAppliedTwice_AppliedAndWrittenToStream_LoadedFromStreamAndApplied_PatchApplySucceeds_FT) { // A Legacy DataPatch containing an int set to 92 AZStd::string_view legacyPatchXML = R"( )"; constexpr int expectedValue = 92; // Load the patch from stream // Loading the legacy patch will run the converter // Patch Data will be wrapped in the StreamWrapper type until Apply is called // Apply provides the remaining class data to complete the conversion DataPatch patch; LoadPatchFromXML(legacyPatchXML, patch); // Apply provides the remaining class data to complete the conversion ObjectToPatch source; AZStd::unique_ptr generatedObjFirstApply(patch.Apply(&source, m_serializeContext.get())); EXPECT_TRUE(generatedObjFirstApply); EXPECT_EQ(generatedObjFirstApply->m_intValue, expectedValue); // Serialize fully converted patch to stream AZStd::vector streamBuffer; WritePatchToByteStream(patch, streamBuffer); // Load fully converted patch from stream DataPatch loadedPatch; LoadPatchFromByteStream(streamBuffer, loadedPatch); // Verify integrity of loaded patch EXPECT_TRUE(loadedPatch.IsValid() && loadedPatch.IsData()); // Apply the patch AZStd::unique_ptr generatedObjSecondApply(loadedPatch.Apply(&source, m_serializeContext.get())); // Verify the patch applied as expected EXPECT_TRUE(generatedObjSecondApply); EXPECT_EQ(generatedObjSecondApply->m_intValue, expectedValue); } TEST_F(PatchingTest, LegacyDataPatchConverter_LegacyPatchXMLMissingTargetClassId_ConverterThrowsError_FT) { // A Legacy DataPatch containing an int set to 178 but missing its TargetClassId // This should fail conversion AZStd::string_view legacyPatchXML = R"( )"; DataPatch patch; // Load the patch from XML // This triggers the Legacy DataPatch converter // Verify the expected number of Errors/Asserts occur // Expected errors: Failed to get data from m_targetClassId field during conversion, found in LegacyDataPatchConverter (DataPatch.cpp) // Converter failed error found in ObjectStreamImpl::LoadClass (ObjectStream.cpp) AZ_TEST_START_ASSERTTEST; LoadPatchFromXML(legacyPatchXML, patch); AZ_TEST_STOP_ASSERTTEST(2); } TEST_F(PatchingTest, LegacyDataPatchConverter_LegacyPatchXMLMissingAddressType_ConverterThrowsError_FT) { // A Legacy DataPatch containing an int set to 154 but missing its AddressType // This should fail conversion AZStd::string_view legacyPatchXML = R"( )"; DataPatch patch; // Load the patch from XML // This triggers the Legacy DataPatch Converter // Verify the expected number of Errors/Asserts occur on conversion // Expected errors: Failed to find both first and second values in pair during conversion, found in ConvertByteStreamMapToAnyMap (DataPatch.cpp) // Converter failed error found in ObjectStreamImpl::LoadClass (ObjectStream.cpp) AZ_TEST_START_ASSERTTEST; LoadPatchFromXML(legacyPatchXML, patch); AZ_TEST_STOP_ASSERTTEST(2); } TEST_F(PatchingTest, LegacyDataPatchConverter_LegacyPatchXMLMissingByteStream_ConverterThrowsError_FT) { // A Legacy DataPatch missing its ByteStream data and is expected to fail conversion AZStd::string_view legacyPatchXML = R"( )"; DataPatch patch; // Load the patch from XML // This triggers the Legacy DataPatch Converter // Verify the expected nummber of Errors/Asserts occur on conversion // Expected errors: Failed to find both first and second values in pair during conversion, found in ConvertByteStreamMapToAnyMap (DataPatch.cpp) // Converter failed error found in ObjectStreamImpl::LoadClass (ObjectStream.cpp) AZ_TEST_START_ASSERTTEST; LoadPatchFromXML(legacyPatchXML, patch); AZ_TEST_STOP_ASSERTTEST(2); } TEST_F(PatchingTest, LegacyDataPatchConverter_LegacyPatchXMLMissingAddressTypeAndByteStream_ConverterThrowsError_FT) { // A Legacy DataPatch missing both its AddressType and ByteStream and is expected to fail conversion AZStd::string_view legacyPatchXML = R"( )"; DataPatch patch; // Load the patch from XML // This triggers the Legacy DataPatch Converter // Verify the expected number of Errors/Asserts occur // Expected errors: Failed to find both first and second values in pair during conversion, found in ConvertByteStreamMapToAnyMap (DataPatch.cpp) // Converter failed error found in ObjectStreamImpl::LoadClass (ObjectStream.cpp) AZ_TEST_START_ASSERTTEST; LoadPatchFromXML(legacyPatchXML, patch); AZ_TEST_STOP_ASSERTTEST(2); } TEST_F(PatchingTest, Apply_LegacyPatchXMLHasInvalidByteStream_ConverterThrowsError_FT) { // A Legacy DataPatch expecting to hold an int but containing an invalid bytestream and is expected to fail conversion AZStd::string_view legacyPatchXML = R"( )"; // Load the patch from XML // This triggers the Legacy DataPatch converter // The invalid bytestream will be stored in a StreamWrapper until Apply DataPatch patch; LoadPatchFromXML(legacyPatchXML, patch); ObjectToPatch source; // Apply the patch and complete conversion // The stored StreamWrapper will attempt to load and fail // Verify the expected number of Errors/Asserts occur // Expected errors: Stream is a newer version than object stream supports, found in ObjectStreamImpl::Start (ObjectStream.cpp) // Failed to load StreamWrapper during DataPatch Apply, found in DataNodeTree::ApplyToElements (DataPatch.cpp) AZ_TEST_START_ASSERTTEST; AZStd::unique_ptr generatedObj(patch.Apply(&source, m_serializeContext.get())); AZ_TEST_STOP_ASSERTTEST(2); } TEST_F(PatchingTest, Apply_LegacyPatchXMLHasIncorrectAddressType_ApplyFails_FT) { // A Legacy DataPatch with an int set to 39 but with an incorrect AddressType // AddressType to location for an int replaced with AddressType for value in an array structure AZStd::string_view legacyPatchXML = R"( )"; // Load the patch from XML // This triggers the Legacy DataPatch converter // The incorrect AddressType will be stored to direct patching for the valid ByteStream DataPatch patch; LoadPatchFromXML(legacyPatchXML, patch); ObjectToPatch source; source.m_intValue = 0; // Apply the patch, conversion will not complete during this stage // Since AddressType is invalid, the underlying data will not be requested during apply and will not be fully converted AZ_TEST_START_TRACE_SUPPRESSION; AZStd::unique_ptr generatedObj(patch.Apply(&source, m_serializeContext.get())); AZ_TEST_STOP_TRACE_SUPPRESSION(0); // We expect the value 39 to not be patched during apply and m_intValue to remain at 0 EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_intValue, 0); } TEST_F(PatchingTest, Apply_LegacyPatchXMLHasIncorrectTargetClassId_ApplyFailsAndReturnsNull_FT) { // A Legacy DataPatch containing an int set to 203 // TargetClassId set to DataPatch type Id which is incorrect for the type being contained AZStd::string_view legacyPatchXML = R"( )"; // Load the patch from XML // This triggers the Legacy DataPatch converter DataPatch patch; LoadPatchFromXML(legacyPatchXML, patch); // Apply the patch, conversion will not complete during this stage // Since targetClassId does not match supplied source type Apply is expected to return nullptr ObjectToPatch source; AZStd::unique_ptr generatedObj(patch.Apply(&source, m_serializeContext.get())); // Verify Apply returned a nullptr EXPECT_FALSE(generatedObj); } TEST_F(PatchingTest, AddressTypeSerializerLoad_AddressTypeIsValid_AddressHasOnlyClassData_LoadSucceeds_FT) { const int expectedValue = 52; AZStd::string patchXML = BuildXMLDataPatchV1AddressTypeIntXML(nullptr, expectedValue); DataPatch patch; ObjectToPatch source; // Verify address deserializes with no errors AZ_TEST_START_TRACE_SUPPRESSION; LoadPatchFromXML(patchXML, patch); AZ_TEST_STOP_TRACE_SUPPRESSION(0); AZStd::unique_ptr generatedObj(patch.Apply(&source, m_serializeContext.get())); // Verify addressed field m_int was patched correctly (verifies integrity of address) EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_intValue, expectedValue); } TEST_F(PatchingTest, AddressTypeSerializerLoad_AddressTypeIsValid_AddressHasClassAndIndexData_LoadSucceeds_FT) { const size_t expectedContainerSize = 5; const size_t persistentIdOffset = 10; const size_t dataOffset = 0; AZStd::string patchXML = BuildXMLDataPatchV1AddressTypeIntVectorXML(nullptr, dataOffset, persistentIdOffset); DataPatch patch; ObjectToPatch source; // Verify address deserializes with no errors AZ_TEST_START_ASSERTTEST; LoadPatchFromXML(patchXML, patch); AZ_TEST_STOP_ASSERTTEST(0); AZStd::unique_ptr generatedObj(patch.Apply(&source, m_serializeContext.get())); // Verify integrity of patched object EXPECT_TRUE(generatedObj); EXPECT_EQ(generatedObj->m_objectArray.size(), expectedContainerSize); for (size_t arrayIndex = 0; arrayIndex < expectedContainerSize; ++arrayIndex) { EXPECT_EQ(generatedObj->m_objectArray[arrayIndex].m_data, arrayIndex); EXPECT_EQ(generatedObj->m_objectArray[arrayIndex].m_persistentId, arrayIndex + persistentIdOffset); } } TEST_F(PatchingTest, AddressTypeSerializerLoad_AddressTypeIsInvalid_InvalidElementsInPath_LoadFails_FT) { AZStd::string pathWithInvalidElements = GetValidAddressForXMLDataPatchV1AddressTypeIntXML(); pathWithInvalidElements += "not/a/valid/path"; AZStd::string patchXML = BuildXMLDataPatchV1AddressTypeIntXML(pathWithInvalidElements.c_str(), 0); DataPatch patch; // Load the patch from XML // This triggers AddressTypeSerializer::Load // Expected error: AddressType failed to load due to invalid element in path AZ_TEST_START_ASSERTTEST; LoadPatchFromXML(patchXML, patch); AZ_TEST_STOP_ASSERTTEST(2); } TEST_F(PatchingTest, AddressTypeSerializerLoad_AddressTypeIsInvalid_MissingPathDelimiter_LoadFails_FT) { // Build a path from a valid path minus the trailing "/" delimiter AZStd::string validPath = GetValidAddressForXMLDataPatchV1AddressTypeIntXML(); AZStd::string validPathMissingDelimiter(validPath.c_str(), strlen(validPath.c_str()) - 1); AZStd::string patchXML = BuildXMLDataPatchV1AddressTypeIntXML(validPathMissingDelimiter.c_str(), 0); DataPatch patch; // Load the patch from XML // This triggers AddressTypeSerializer::Load // Expected error: AddressType failed to load due to path not containing valid delimiter "/" AZ_TEST_START_ASSERTTEST; LoadPatchFromXML(patchXML, patch); AZ_TEST_STOP_ASSERTTEST(2); } TEST_F(PatchingTest, Apply_AddressTypeIsInvalid_SingleEntryInPatch_ApplyFails_FT) { AZStd::string patchXML = BuildXMLDataPatchV1AddressTypeIntXML("not/a/valid/path", 0); DataPatch patch; ObjectToPatch source; // Load the patch from XML // This triggers AddressTypeSerializer::Load // Expected errors: AddressType failed to load due to invalid element in path (DataPatch.cpp) // Apply fails due to patch containing Invalid address during Apply (DataPatch.cpp) AZ_TEST_START_ASSERTTEST; LoadPatchFromXML(patchXML, patch); AZStd::unique_ptr generatedObj(patch.Apply(&source, m_serializeContext.get())); AZ_TEST_STOP_ASSERTTEST(3); } TEST_F(PatchingTest, AddressTypeSerializerLoad_AddressTypeIsEmpty_SingleEntryInPatch_ApplySucceeds_FT) { // An empty address on a single entry patch denotes that the root element is being patched // Validate that we succesfully load an emtpy address AZStd::string patchXML = BuildXMLDataPatchV1AddressTypeIntXML("", 0); DataPatch patch; ObjectToPatch source; // Verify address deserializes with no errors AZ_TEST_START_ASSERTTEST; LoadPatchFromXML(patchXML, patch); AZ_TEST_STOP_ASSERTTEST(0); } TEST_F(PatchingTest, Apply_AddressTypeIsInvalid_MultipleEntriesInPatch_ApplyFails_FT) { AZStd::string patchXML = BuildXMLDataPatchV1AddressTypeIntVectorXML("not/a/valid/path", 0, 0); DataPatch patch; ObjectToPatch source; // Load the patch from XML // This triggers AddressTypeSerializer::Load // Expected errors: AddressType failed to load due to invalid element in path (DataPatch.cpp) // Apply fails due to empty AddressType (DataPatch.cpp) AZ_TEST_START_ASSERTTEST; LoadPatchFromXML(patchXML, patch); AZStd::unique_ptr generatedObj(patch.Apply(&source, m_serializeContext.get())); AZ_TEST_STOP_ASSERTTEST(3); } TEST_F(PatchingTest, AddressTypeElementLoad_PathElementHasValidPathForClassType_LoadIsSuccessful_FT) { const char* expectedTypeId = "{D0C4D19C-7EFF-4F93-A5F0-95F33FC855AA}"; const char* expectedAddressElement = "ClassA"; const int expectedVersion = 5000; DataPatch::AddressTypeElement addressTypeElement = m_addressTypeSerializer->LoadAddressElementFromPath(AZStd::string::format("somecharacters(%s)::%s%s%i/", expectedTypeId, expectedAddressElement, V1AddressTypeElementVersionDelimiter, expectedVersion)); EXPECT_TRUE(addressTypeElement.IsValid()); EXPECT_EQ(addressTypeElement.GetElementTypeId(), AZ::Uuid(expectedTypeId)); EXPECT_EQ(addressTypeElement.GetAddressElement(), AZ_CRC(expectedAddressElement)); EXPECT_EQ(addressTypeElement.GetElementVersion(), expectedVersion); } TEST_F(PatchingTest, AddressTypeElementLoad_PathElementHasValidPathForIndexType_LoadIsSuccessful_FT) { const char* expectedTypeId = "{07DEDB71-0585-5BE6-83FF-1C9029B9E5DB}"; const int expectedAddressElement = 4321; const int expectedVersion = 2222; DataPatch::AddressTypeElement addressTypeElement = m_addressTypeSerializer->LoadAddressElementFromPath(AZStd::string::format("somecharacters(%s)#%i%s%i/", expectedTypeId, expectedAddressElement, V1AddressTypeElementVersionDelimiter, expectedVersion)); EXPECT_TRUE(addressTypeElement.IsValid()); EXPECT_EQ(addressTypeElement.GetAddressElement(), expectedAddressElement); EXPECT_EQ(addressTypeElement.GetElementTypeId(), AZ::Uuid(expectedTypeId)); EXPECT_EQ(addressTypeElement.GetElementVersion(), expectedVersion); } TEST_F(PatchingTest, AddressTypeElementLoad_PathElementHasValidPathForNoneType_LoadIsSuccessful_FT) { const int expectedAddressElement = 9999; DataPatch::AddressTypeElement addressTypeElement = m_addressTypeSerializer->LoadAddressElementFromPath(AZStd::string::format("%i/", expectedAddressElement)); EXPECT_TRUE(addressTypeElement.IsValid()); EXPECT_EQ(addressTypeElement.GetAddressElement(), expectedAddressElement); EXPECT_EQ(addressTypeElement.GetElementTypeId(), AZ::Uuid::CreateNull()); EXPECT_EQ(addressTypeElement.GetElementVersion(), std::numeric_limits::max()); } TEST_F(PatchingTest, AddressTypeElementLoad_PathElementHasInvalidTypeId_LoadFails_FT) { AZStd::string pathElementWithInvalidTypeId = AZStd::string::format("somecharacters(invalidTypeId)::classB%s5678%s", V1AddressTypeElementVersionDelimiter, V1AddressTypeElementPathDelimiter); DataPatch::AddressTypeElement addressTypeElement = m_addressTypeSerializer->LoadAddressElementFromPath(pathElementWithInvalidTypeId); EXPECT_FALSE(addressTypeElement.IsValid()); EXPECT_EQ(addressTypeElement.GetElementTypeId(), AZ::Uuid::CreateNull()); } TEST_F(PatchingTest, AddressTypeElementLoad_TypeIdMissingParentheses_LoadFails_FT) { AZStd::string pathMissingParentheses = AZStd::string::format("somecharacters{3A8D5EC9-D70E-41CB-879C-DEF6A6D6ED03}::classE%s3000%s", V1AddressTypeElementVersionDelimiter, V1AddressTypeElementPathDelimiter); DataPatch::AddressTypeElement addressTypeElement = m_addressTypeSerializer->LoadAddressElementFromPath(pathMissingParentheses); EXPECT_FALSE(addressTypeElement.IsValid()); } TEST_F(PatchingTest, AddressTypeElementLoad_TypeIdMissingCurlyBraces_LoadFails_FT) { AZStd::string pathMissingCurlyBraces = AZStd::string::format("somecharacters(07DEDB71-0585-5BE6-83FF-1C9029B9E5DB)::classF%s9876%s", V1AddressTypeElementVersionDelimiter, V1AddressTypeElementPathDelimiter); DataPatch::AddressTypeElement addressTypeElement = m_addressTypeSerializer->LoadAddressElementFromPath(pathMissingCurlyBraces); EXPECT_FALSE(addressTypeElement.IsValid()); } TEST_F(PatchingTest, AddressTypeElementLoad_ClassTypePathElementMissingColons_LoadFails_FT) { AZStd::string pathElementMissingColons = AZStd::string::format("somecharacters({861A12B0-BD91-528E-9CEC-505246EE98DE})classC%s5432%s", V1AddressTypeElementVersionDelimiter, V1AddressTypeElementPathDelimiter); DataPatch::AddressTypeElement addressTypeElement = m_addressTypeSerializer->LoadAddressElementFromPath(pathElementMissingColons); EXPECT_FALSE(addressTypeElement.IsValid()); } TEST_F(PatchingTest, AddressTypeElementLoad_IndexTypePathElementMissingPound_LoadFails_FT) { AZStd::string pathElementMissingPound = AZStd::string::format("somecharacters({ADFD596B-7177-5519-9752-BC418FE42963})91011%s1122%s", V1AddressTypeElementVersionDelimiter, V1AddressTypeElementPathDelimiter); DataPatch::AddressTypeElement addressTypeElement = m_addressTypeSerializer->LoadAddressElementFromPath(pathElementMissingPound); EXPECT_FALSE(addressTypeElement.IsValid()); } TEST_F(PatchingTest, AddressTypeElementLoad_PathElementMissingDotBeforeVersion_LoadFails_FT) { AZStd::string pathMissingDot = AZStd::string::format("somecharacters({07DEDB71-0585-5BE6-83FF-1C9029B9E5DB})::classD4000%s", V1AddressTypeElementPathDelimiter); DataPatch::AddressTypeElement addressTypeElement = m_addressTypeSerializer->LoadAddressElementFromPath(pathMissingDot); EXPECT_FALSE(addressTypeElement.IsValid()); } TEST_F(PatchingTest, DataPatchFieldConverterForVersion1Patch_DoesNotRunVersion0To1Converter_Succeeds) { ObjectWithNumericFieldV1::Reflect(m_serializeContext.get()); const ObjectWithNumericFieldV1 initialObject; ObjectWithNumericFieldV1 testObject; testObject.m_value = 3946393; DataPatch testPatch; testPatch.Create(&initialObject, &testObject, {}, {}, m_serializeContext.get()); // Unreflect ObjectWithNumericFieldV1 and reflect ObjectWithNumericFieldV2 { m_serializeContext->EnableRemoveReflection(); ObjectWithNumericFieldV1::Reflect(m_serializeContext.get()); m_serializeContext->DisableRemoveReflection(); ObjectWithNumericFieldV2::Reflect(m_serializeContext.get()); } ObjectWithNumericFieldV2 initialObjectV2; ObjectWithNumericFieldV2* patchedObject = testPatch.Apply(&initialObjectV2, m_serializeContext.get()); ASSERT_NE(nullptr, patchedObject); EXPECT_DOUBLE_EQ(32.0, patchedObject->m_value); // Clean up ObjectWithNumericFieldV2 patch data; delete patchedObject; // Unreflect remaining reflected classes m_serializeContext->EnableRemoveReflection(); ObjectWithNumericFieldV2::Reflect(m_serializeContext.get()); m_serializeContext->DisableRemoveReflection(); } TEST_F(PatchingTest, ObjectFieldConverter_CreateDataPatchInMemoryCanBeAppliedSuccessfully) { ObjectWithNumericFieldV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterDerivedV1::Reflect(m_serializeContext.get()); ObjectFieldConverterV1::Reflect(m_serializeContext.get()); const ObjectFieldConverterV1 initialObject; ObjectFieldConverterV1 testObject; // Change the defaults of elements on the ObjectFieldConverterV1 object testObject.m_rootStringField = "Test1"; testObject.m_rootStringVector.emplace_back("Test2"); testObject.m_rootInnerObject.m_stringField = "InnerTest1"; testObject.m_rootInnerObject.m_stringVector.emplace_back("InnerTest2"); auto derivedInnerObject = aznew InnerObjectFieldConverterDerivedV1; derivedInnerObject->m_stringField = "DerivedTest1"; derivedInnerObject->m_stringVector.emplace_back("DerivedTest2"); derivedInnerObject->m_objectWithNumericField.m_value = 1; testObject.m_baseInnerObjectPolymorphic = derivedInnerObject; DataPatch testPatch; testPatch.Create(&initialObject, &testObject, {}, {}, m_serializeContext.get()); ObjectFieldConverterV1* patchedObject = testPatch.Apply(&initialObject, m_serializeContext.get()); EXPECT_EQ(testObject.m_rootStringField, patchedObject->m_rootStringField); EXPECT_EQ(testObject.m_rootStringVector, patchedObject->m_rootStringVector); InnerObjectFieldConverterV1& testInnerObject = testObject.m_rootInnerObject; InnerObjectFieldConverterV1& patchedInnerObject = patchedObject->m_rootInnerObject; EXPECT_EQ(testInnerObject.m_stringField, patchedInnerObject.m_stringField); EXPECT_EQ(testInnerObject.m_stringVector, patchedInnerObject.m_stringVector); auto patchedInnerObjectDerived = azrtti_cast(patchedObject->m_baseInnerObjectPolymorphic); ASSERT_NE(nullptr, patchedInnerObjectDerived); EXPECT_EQ(derivedInnerObject->m_stringField, patchedInnerObjectDerived->m_stringField); EXPECT_EQ(derivedInnerObject->m_stringVector, patchedInnerObjectDerived->m_stringVector); EXPECT_EQ(derivedInnerObject->m_objectWithNumericField.m_value, patchedInnerObjectDerived->m_objectWithNumericField.m_value); // Clean up original ObjectFieldConverterV1 object delete testObject.m_baseInnerObjectPolymorphic; // Clean up patched ObjectFieldConverterV1 object delete patchedObject->m_baseInnerObjectPolymorphic; delete patchedObject; m_serializeContext->EnableRemoveReflection(); ObjectWithNumericFieldV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterDerivedV1::Reflect(m_serializeContext.get()); ObjectFieldConverterV1::Reflect(m_serializeContext.get()); m_serializeContext->DisableRemoveReflection(); } inline namespace NestedMemberFieldChangeConverter { class InnerObjectFieldConverterV2 { public: AZ_CLASS_ALLOCATOR(InnerObjectFieldConverterV2, SystemAllocator, 0); AZ_RTTI(InnerObjectFieldConverterV2, "{28E61B17-F321-4D4E-9F4C-00846C6631DE}"); virtual ~InnerObjectFieldConverterV2() = default; static int64_t StringToInt64(const AZStd::string& value) { return static_cast(value.size()); } static void Reflect(AZ::ReflectContext* reflectContext) { if (auto serializeContext = azrtti_cast(reflectContext)) { serializeContext->Class() // A class version converter is needed to load an InnerObjectFieldConverterV2 when it is stored directly in patch. // This occurs when patched element is a pointer to a class. In that case the entire class is serialized out // Therefore when the DataPatch is loaded, the patch Data will load it's AZStd::any, which goes through the normal // serialization flow, so if the class stored in the AZStd::any is an old version it needs to run through a Version Converter ->Version(2, &VersionConverter) ->Field("InnerBaseIntField", &InnerObjectFieldConverterV2::m_int64Field) ->TypeChange("InnerBaseStringField", 1, 2, AZStd::function(&StringToInt64)) ->NameChange(1, 2, "InnerBaseStringField", "InnerBaseIntField") ->Field("InnerBaseStringVector", &InnerObjectFieldConverterV2::m_stringVector) ; } } static bool VersionConverter(AZ::SerializeContext& context, AZ::SerializeContext::DataElementNode& rootElement) { if (rootElement.GetVersion() < 2) { AZStd::string stringField; if (!rootElement.GetChildData(AZ_CRC("InnerBaseStringField"), stringField)) { AZ_Error("PatchingTest", false, "Unable to retrieve 'InnerBaseStringField' data for %u version of the InnerObjectFieldConverterClass", rootElement.GetVersion()) return false; } rootElement.RemoveElementByName(AZ_CRC("InnerBaseStringField")); rootElement.AddElementWithData(context, "InnerBaseIntField", static_cast(stringField.size())); } return true; } int64_t m_int64Field; AZStd::vector m_stringVector; }; //! InnerObjectFieldConverterDerivedV2 is exactly the same as InnerObjectFieldConverterDerivedV1 //! It is just needed to state that InnerObjectFieldConverterV2 is a base class using InnerObjectFieldConverterDerivedV1WithV2Base = InnerObjectFieldConverterDerivedV1Template; //! ObjectFieldConverterV1WithMemberVersionChange is the same has the ObjectFieldConverterV1 class, it just substitutes out //! the InnerObjectFieldConverterV1 with InnerObjectFieldConverterV2 that has the same typeid, but newer version class ObjectFieldConverterV1WithMemberVersionChange { using ClassType = ObjectFieldConverterV1WithMemberVersionChange; public: AZ_CLASS_ALLOCATOR(ClassType, SystemAllocator, 0); AZ_TYPE_INFO(ClassType, "{5722C4E4-25DE-48C5-BC89-0EE9D38DF433}"); static void Reflect(AZ::ReflectContext* reflectContext) { if (auto serializeContext = azrtti_cast(reflectContext)) { serializeContext->Class() ->Version(1) ->Field("RootStringField", &ClassType::m_rootStringField) ->Field("RootStringVector", &ClassType::m_rootStringVector) ->Field("RootInnerObjectValue", &ClassType::m_rootInnerObject) ->Field("RootInnerObjectPointer", &ClassType::m_baseInnerObjectPolymorphic) ; } } //! AZStd::string uses IDataSerializer for Serialization. //! This is to test Field Converters for patched element that are directly on the patched class AZStd::string m_rootStringField; //! AZStd::vector uses IDataContainer for Serialization. The inner AZStd::string class uses IDataSerializer for serialization //! This is to test Field Converters for patched element that are directly on the patched class AZStd::vector m_rootStringVector; //! AZStd::vector uses IDataContainer for Serialization. The inner AZStd::string class uses IDataSerializer for serialization //! This is to test Field Converters for patched element that are directly on the patched class InnerObjectFieldConverterV2 m_rootInnerObject{}; InnerObjectFieldConverterV2* m_baseInnerObjectPolymorphic{}; }; TEST_F(PatchingTest, ObjectFieldConverter_ChangeInnerFieldVersion_FieldConverterRunsSuccessfully) { using OriginalObjectField = ObjectFieldConverterV1; using PatchedObjectField = ObjectFieldConverterV1WithMemberVersionChange; ObjectWithNumericFieldV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterDerivedV1::Reflect(m_serializeContext.get()); OriginalObjectField::Reflect(m_serializeContext.get()); OriginalObjectField testObject; // Change the defaults of the InnerObjectFieldConverterV2 member and InnerObjectFieldConverterV2 pointer member testObject.m_rootInnerObject.m_stringField = "InnerTest1"; auto derivedInnerObject = aznew InnerObjectFieldConverterDerivedV1; derivedInnerObject->m_stringField = "DerivedTest1"; derivedInnerObject->m_objectWithNumericField.m_value = 10; testObject.m_baseInnerObjectPolymorphic = derivedInnerObject; AZStd::vector byteBuffer; // Create DataPatch using ObjectFieldConverterV1 { DataPatch testPatch; const OriginalObjectField initialObjectV1; testPatch.Create(&initialObjectV1, &testObject, {}, {}, m_serializeContext.get()); // Write DataPatch to ByteStream before unreflected Version 1 of the InnerObjectFieldConverterV1 class AZ::IO::ByteContainerStream byteStream(&byteBuffer); WritePatchToByteStream(testPatch, byteBuffer); } // Now unreflect the ObjectFieldConverterV1, InnerObjectFieldConverterDerivedV1 and InnerObjectFieldConverterDerivedV1 // and reflect ObjectFieldConverterV1WithMemberVersionChange, InnerObjectFieldConverterV2 and InnerObjectFieldConverterDerivedV1WithV2Base { m_serializeContext->EnableRemoveReflection(); InnerObjectFieldConverterV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterDerivedV1::Reflect(m_serializeContext.get()); OriginalObjectField::Reflect(m_serializeContext.get()); m_serializeContext->DisableRemoveReflection(); InnerObjectFieldConverterV2::Reflect(m_serializeContext.get()); InnerObjectFieldConverterDerivedV1WithV2Base::Reflect(m_serializeContext.get()); PatchedObjectField::Reflect(m_serializeContext.get()); } // Read DataPatch from ByteStream after reflecting Version 2 of the InnerObjectFieldConverterV2 class // the reason this is required is the testPatch variable has as part of the patch data AZStd::any // that wraps a instance of an InnerObjectFieldConverterDerivedV1 stored in an InnerObjectFieldConverterV1 pointer // The virtual table points to the InnerObjectFieldConverterDerivedV1 class, which in a normal patching scenario // The Data Patch would be loaded from disk after the new version of the InnerObjectFieldConverterV2 has reflected // and therefore the patch instance data would be an object of InnerObjectFieldConverterDerivedV1WithV2Base with the // correct vtable AZ::DataPatch freshDataPatch; LoadPatchFromByteStream(byteBuffer, freshDataPatch); const PatchedObjectField initialObjectV2; PatchedObjectField *patchedObjectV2 = freshDataPatch.Apply(&initialObjectV2, m_serializeContext.get()); ASSERT_NE(nullptr, patchedObjectV2); EXPECT_EQ(10, patchedObjectV2->m_rootInnerObject.m_int64Field); auto patchedDerivedInnerObject = azrtti_cast(patchedObjectV2->m_baseInnerObjectPolymorphic); ASSERT_NE(nullptr, patchedDerivedInnerObject); EXPECT_EQ(12, patchedDerivedInnerObject->m_int64Field); ASSERT_NE(nullptr, patchedDerivedInnerObject); EXPECT_EQ(10, patchedDerivedInnerObject->m_objectWithNumericField.m_value); // Clean up original ObjectFieldConverterV1 object delete testObject.m_baseInnerObjectPolymorphic; // Clean up patched ObjectFieldConverterV1 object delete patchedObjectV2->m_baseInnerObjectPolymorphic; delete patchedObjectV2; m_serializeContext->EnableRemoveReflection(); ObjectWithNumericFieldV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterV2::Reflect(m_serializeContext.get()); InnerObjectFieldConverterDerivedV1WithV2Base::Reflect(m_serializeContext.get()); PatchedObjectField::Reflect(m_serializeContext.get()); m_serializeContext->DisableRemoveReflection(); } } inline namespace RootLevelDataSerializerFieldConverter { // ObjectFieldConverterReplaceMemberDataSerializerV2, uses the same TypeId as ObjectFieldConverterV1 // for to test the FieldConverter for an IDataSerializer class ObjectFieldConverterReplaceMemberDataSerializerV2 { using ClassType = ObjectFieldConverterReplaceMemberDataSerializerV2; public: AZ_CLASS_ALLOCATOR(ObjectFieldConverterReplaceMemberDataSerializerV2, SystemAllocator, 0); AZ_TYPE_INFO(ObjectFieldConverterReplaceMemberDataSerializerV2, "{5722C4E4-25DE-48C5-BC89-0EE9D38DF433}"); static AZ::Uuid ConvertStringToUuid(const AZStd::string& value) { return AZ::Uuid::CreateName(value.data()); } static void Reflect(AZ::ReflectContext* reflectContext) { if (auto serializeContext = azrtti_cast(reflectContext)) { serializeContext->Class() ->Version(2) ->Field("RootUuidField", &ClassType::m_rootUuidField) ->NameChange(1, 2, "RootStringField", "RootUuidField") // NOTE!! Type Change is prioritized before Name Change, so it works on the old Field name ->TypeChange("RootStringField", 1, 2, AZStd::function(&ClassType::ConvertStringToUuid)) ->Field("RootStringVector", &ClassType::m_rootStringVector) ->Field("RootInnerObjectValue", &ClassType::m_rootInnerObject) ->Field("RootInnerObjectPointer", &ClassType::m_baseInnerObjectPolymorphic) ; } } //! AZStd::string uses IDataSerializer for Serialization. //! This is to test Field Converters for patched element that are directly on the patched class AZ::Uuid m_rootUuidField{ AZ::Uuid::CreateNull() }; //! AZStd::vector uses IDataContainer for Serialization. The inner AZStd::string class uses IDataSerializer for serialization //! This is to test Field Converters for patched element that are directly on the patched class AZStd::vector m_rootStringVector; //! AZStd::vector uses IDataContainer for Serialization. The inner AZStd::string class uses IDataSerializer for serialization //! This is to test Field Converters for patched element that are directly on the patched class InnerObjectFieldConverterV1 m_rootInnerObject{}; InnerObjectFieldConverterV1* m_baseInnerObjectPolymorphic{}; }; // ObjectFieldConverterReplaceMemberDataSerializerV3, uses the same TypeId as ObjectFieldConverterV1 // for to test the FieldConverter that skips a level class ObjectFieldConverterReplaceMemberDataSerializerV3 { using ClassType = ObjectFieldConverterReplaceMemberDataSerializerV3; public: AZ_CLASS_ALLOCATOR(ObjectFieldConverterReplaceMemberDataSerializerV3, SystemAllocator, 0); AZ_TYPE_INFO(ObjectFieldConverterReplaceMemberDataSerializerV3, "{5722C4E4-25DE-48C5-BC89-0EE9D38DF433}"); static bool ConvertStringToBool(const AZStd::string& value) { return !value.empty(); } static bool ConvertUuidToBool(const AZ::Uuid& value) { return !value.IsNull(); } static void Reflect(AZ::ReflectContext* reflectContext) { if (auto serializeContext = azrtti_cast(reflectContext)) { serializeContext->Class() ->Version(3) ->Field("RootBoolField", &ClassType::m_rootBoolField) ->NameChange(2, 3, "RootUuidField", "RootBoolField") ->NameChange(1, 3, "RootStringField", "RootBoolField") //! NOTE Type Change is prioritized before Name Change, so it works on the old Field name ->TypeChange("RootUuidField", 2, 3, AZStd::function(&ClassType::ConvertUuidToBool)) ->TypeChange("RootStringField", 1, 3, AZStd::function(&ClassType::ConvertStringToBool)) ->Field("RootStringVector", &ClassType::m_rootStringVector) ->Field("RootInnerObjectValue", &ClassType::m_rootInnerObject) ->Field("RootInnerObjectPointer", &ClassType::m_baseInnerObjectPolymorphic) ; } } //! AZStd::string uses IDataSerializer for Serialization. //! This is to test Field Converters for patched element that are directly on the patched class bool m_rootBoolField{}; //! AZStd::vector uses IDataContainer for Serialization. The inner AZStd::string class uses IDataSerializer for serialization //! This is to test Field Converters for patched element that are directly on the patched class AZStd::vector m_rootStringVector; //! AZStd::vector uses IDataContainer for Serialization. The inner AZStd::string class uses IDataSerializer for serialization //! This is to test Field Converters for patched element that are directly on the patched class InnerObjectFieldConverterV1 m_rootInnerObject{}; InnerObjectFieldConverterV1* m_baseInnerObjectPolymorphic{}; }; TEST_F(PatchingTest, ObjectFieldConverter_ChangeRootDataSerializerField_ConvertsFromV1ToV2_Successfully) { using OriginalObjectField = ObjectFieldConverterV1; using PatchedObjectField = ObjectFieldConverterReplaceMemberDataSerializerV2; ObjectWithNumericFieldV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterDerivedV1::Reflect(m_serializeContext.get()); OriginalObjectField::Reflect(m_serializeContext.get()); OriginalObjectField testObject; // Change the defaults of elements on the ObjectFieldConverterV1 object testObject.m_rootStringField = "Test1"; testObject.m_rootStringVector.emplace_back("Test2"); testObject.m_rootInnerObject.m_stringField = "InnerTest1"; testObject.m_rootInnerObject.m_stringVector.emplace_back("InnerTest2"); auto derivedInnerObject = aznew InnerObjectFieldConverterDerivedV1; derivedInnerObject->m_stringField = "DerivedTest1"; derivedInnerObject->m_stringVector.emplace_back("DerivedTest2"); derivedInnerObject->m_objectWithNumericField.m_value = 1; testObject.m_baseInnerObjectPolymorphic = derivedInnerObject; // Create DataPatch using ObjectFieldConverterV1 DataPatch testPatch; const OriginalObjectField initialObjectV1; testPatch.Create(&initialObjectV1, &testObject, {}, {}, m_serializeContext.get()); // Now unreflect the ObjectFieldConverterV1 and reflect ObjectFieldConverterReplaceMemberDataSerializerV2 { m_serializeContext->EnableRemoveReflection(); OriginalObjectField::Reflect(m_serializeContext.get()); m_serializeContext->DisableRemoveReflection(); PatchedObjectField::Reflect(m_serializeContext.get()); } const PatchedObjectField initialObjectV2; PatchedObjectField *patchedObjectV2 = testPatch.Apply(&initialObjectV2, m_serializeContext.get()); ASSERT_NE(nullptr, patchedObjectV2); EXPECT_FALSE(patchedObjectV2->m_rootUuidField.IsNull()); // Clean up original ObjectFieldConverterV1 object delete testObject.m_baseInnerObjectPolymorphic; // Clean up patched ObjectFieldConverterV1 object delete patchedObjectV2->m_baseInnerObjectPolymorphic; delete patchedObjectV2; m_serializeContext->EnableRemoveReflection(); ObjectWithNumericFieldV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterDerivedV1::Reflect(m_serializeContext.get()); PatchedObjectField::Reflect(m_serializeContext.get()); m_serializeContext->DisableRemoveReflection(); } TEST_F(PatchingTest, ObjectFieldConverter_ChangeRootDataSerializerField_ConvertsFromV2ToV3_Successfully) { using OriginalObjectField = ObjectFieldConverterReplaceMemberDataSerializerV2; using PatchedObjectField = ObjectFieldConverterReplaceMemberDataSerializerV3; ObjectWithNumericFieldV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterDerivedV1::Reflect(m_serializeContext.get()); OriginalObjectField::Reflect(m_serializeContext.get()); OriginalObjectField testObject; // Change the defaults of elements on the ObjectFieldConverterV1 object testObject.m_rootUuidField = AZ::Uuid::CreateString("{10000000-0000-0000-0000-000000000000}"); testObject.m_rootStringVector.emplace_back("Test2"); testObject.m_rootInnerObject.m_stringField = "InnerTest1"; testObject.m_rootInnerObject.m_stringVector.emplace_back("InnerTest2"); auto derivedInnerObject = aznew InnerObjectFieldConverterDerivedV1; derivedInnerObject->m_stringField = "DerivedTest1"; derivedInnerObject->m_stringVector.emplace_back("DerivedTest2"); derivedInnerObject->m_objectWithNumericField.m_value = 1; testObject.m_baseInnerObjectPolymorphic = derivedInnerObject; // Create DataPatch using ObjectFieldConverterV1 DataPatch testPatch; const OriginalObjectField initialObjectV1; testPatch.Create(&initialObjectV1, &testObject, {}, {}, m_serializeContext.get()); // Now unreflect the ObjectFieldConverterV1 and reflect ObjectFieldConverterReplaceMemberDataSerializerV2 { m_serializeContext->EnableRemoveReflection(); OriginalObjectField::Reflect(m_serializeContext.get()); m_serializeContext->DisableRemoveReflection(); PatchedObjectField::Reflect(m_serializeContext.get()); } const PatchedObjectField initialObjectV3; PatchedObjectField *patchedObjectV3 = testPatch.Apply(&initialObjectV3, m_serializeContext.get()); ASSERT_NE(nullptr, patchedObjectV3); EXPECT_TRUE(patchedObjectV3->m_rootBoolField); // Clean up original ObjectFieldConverterV1 object delete testObject.m_baseInnerObjectPolymorphic; // Clean up patched ObjectFieldConverterV1 object delete patchedObjectV3->m_baseInnerObjectPolymorphic; delete patchedObjectV3; m_serializeContext->EnableRemoveReflection(); ObjectWithNumericFieldV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterDerivedV1::Reflect(m_serializeContext.get()); PatchedObjectField::Reflect(m_serializeContext.get()); m_serializeContext->DisableRemoveReflection(); } TEST_F(PatchingTest, ObjectFieldConverter_ChangeRootDataSerializerField_ConvertsFromV1ToV3_Successfully) { using OriginalObjectField = ObjectFieldConverterV1; using PatchedObjectField = ObjectFieldConverterReplaceMemberDataSerializerV3; ObjectWithNumericFieldV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterDerivedV1::Reflect(m_serializeContext.get()); OriginalObjectField::Reflect(m_serializeContext.get()); OriginalObjectField testObject; // Change the defaults of elements on the ObjectFieldConverterV1 object testObject.m_rootStringField = "StringV1"; testObject.m_rootStringVector.emplace_back("Test2"); testObject.m_rootInnerObject.m_stringField = "InnerTest1"; testObject.m_rootInnerObject.m_stringVector.emplace_back("InnerTest2"); auto derivedInnerObject = aznew InnerObjectFieldConverterDerivedV1; derivedInnerObject->m_stringField = "DerivedTest1"; derivedInnerObject->m_stringVector.emplace_back("DerivedTest2"); derivedInnerObject->m_objectWithNumericField.m_value = 1; testObject.m_baseInnerObjectPolymorphic = derivedInnerObject; // Create DataPatch using ObjectFieldConverterV1 DataPatch testPatch; const OriginalObjectField initialObjectV1; testPatch.Create(&initialObjectV1, &testObject, {}, {}, m_serializeContext.get()); // Now unreflect the ObjectFieldConverterV1 and reflect ObjectFieldConverterReplaceMemberDataSerializerV2 { m_serializeContext->EnableRemoveReflection(); OriginalObjectField::Reflect(m_serializeContext.get()); m_serializeContext->DisableRemoveReflection(); PatchedObjectField::Reflect(m_serializeContext.get()); } const PatchedObjectField initialObjectV3; PatchedObjectField *patchedObjectV3 = testPatch.Apply(&initialObjectV3, m_serializeContext.get()); ASSERT_NE(nullptr, patchedObjectV3); EXPECT_TRUE(patchedObjectV3->m_rootBoolField); // Clean up original ObjectFieldConverterV1 object delete testObject.m_baseInnerObjectPolymorphic; // Clean up patched ObjectFieldConverterV1 object delete patchedObjectV3->m_baseInnerObjectPolymorphic; delete patchedObjectV3; m_serializeContext->EnableRemoveReflection(); ObjectWithNumericFieldV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterDerivedV1::Reflect(m_serializeContext.get()); PatchedObjectField::Reflect(m_serializeContext.get()); m_serializeContext->DisableRemoveReflection(); } } inline namespace RootLevelDataContainerFieldConverter { // ObjectFieldConverterReplaceMemberDataConverterV2, uses the same TypeId as ObjectFieldConverterV1 // for to test the FieldConverter for an IDataConverter class ObjectFieldConverterReplaceMemberDataConverterV2 { using ClassType = ObjectFieldConverterReplaceMemberDataConverterV2; public: AZ_CLASS_ALLOCATOR(ClassType, SystemAllocator, 0); AZ_TYPE_INFO(ClassType, "{5722C4E4-25DE-48C5-BC89-0EE9D38DF433}"); static AZStd::array ConvertStringVectorToStringArray(const AZStd::vector& value) { AZStd::array result; size_t elementsToCopy = AZStd::min(result.size(), value.size()); for (size_t valueIndex = 0; valueIndex < elementsToCopy; ++valueIndex) { result[valueIndex] = value[valueIndex]; } return result; } static void Reflect(AZ::ReflectContext* reflectContext) { if (auto serializeContext = azrtti_cast(reflectContext)) { // Because containers are implicitly reflected when used as part of a class reflection // the ObjectFieldConverterV1 AZStd::vector class is not reflected // if the ObjectFieldConverterV1 did not reflect serializeContext->RegisterGenericType>(); serializeContext->Class() ->Version(2) ->Field("RootStringField", &ClassType::m_rootStringField) ->Field("RootStringArray", &ClassType::m_rootStringArray) ->TypeChange("RootStringVector", 1, 2, AZStd::function(const AZStd::vector&)>(&ConvertStringVectorToStringArray)) ->NameChange(1, 2, "RootStringVector", "RootStringArray") ->Field("RootInnerObjectValue", &ClassType::m_rootInnerObject) ->Field("RootInnerObjectPointer", &ClassType::m_baseInnerObjectPolymorphic) ; } } //! AZStd::string uses IDataSerializer for Serialization. //! This is to test Field Converters for patched element that are directly on the patched class AZStd::string m_rootStringField; //! AZStd::array uses IDataContainer for Serialization. The inner AZStd::string class uses IDataSerializer for serialization //! This is to test Field Converters for patched element that are directly on the patched class AZStd::array m_rootStringArray; //! AZStd::vector uses IDataContainer for Serialization. The inner AZStd::string class uses IDataSerializer for serialization //! This is to test Field Converters for patched element that are directly on the patched class InnerObjectFieldConverterV1 m_rootInnerObject{}; InnerObjectFieldConverterV1* m_baseInnerObjectPolymorphic{}; }; // ObjectFieldConverterReplaceMemberDataConverterV3, uses the same TypeId as ObjectFieldConverterV1 // for to test the FieldConverter that skips a level class ObjectFieldConverterReplaceMemberDataConverterV3 { using ClassType = ObjectFieldConverterReplaceMemberDataConverterV3; public: AZ_CLASS_ALLOCATOR(ClassType, SystemAllocator, 0); AZ_TYPE_INFO(ClassType, "{5722C4E4-25DE-48C5-BC89-0EE9D38DF433}"); static AZStd::list ConvertStringVectorToStringList(const AZStd::vector& value) { AZStd::list result(value.begin(), value.end()); return result; } static AZStd::list ConvertStringArrayToStringList(const AZStd::array& value) { AZStd::list result(value.begin(), value.end()); return result; } static void Reflect(AZ::ReflectContext* reflectContext) { if (auto serializeContext = azrtti_cast(reflectContext)) { serializeContext->Class() ->Version(3) ->Field("RootStringField", &ClassType::m_rootStringField) ->Field("RootStringList", &ClassType::m_rootStringList) // The TypeChange and NameChange converters are interleaved purposefully to make sure that the order of declaration // of the converters doesn't affect the conversion result ->TypeChange("RootStringVector", 1, 3, AZStd::function(const AZStd::vector&)>(&ConvertStringVectorToStringList)) ->NameChange(1, 3, "RootStringVector", "RootStringList") ->NameChange(2, 3, "RootStringArray", "RootStringList") ->TypeChange("RootStringArray", 2, 3, AZStd::function(const AZStd::array&)>(&ConvertStringArrayToStringList)) ->Field("RootInnerObjectValue", &ClassType::m_rootInnerObject) ->Field("RootInnerObjectPointer", &ClassType::m_baseInnerObjectPolymorphic) ; } } //! AZStd::string uses IDataSerializer for Serialization. //! This is to test Field Converters for patched element that are directly on the patched class AZStd::string m_rootStringField; //! AZStd::vector uses IDataContainer for Serialization. The inner AZStd::string class uses IDataSerializer for serialization //! This is to test Field Converters for patched element that are directly on the patched class AZStd::list m_rootStringList; //! AZStd::vector uses IDataContainer for Serialization. The inner AZStd::string class uses IDataSerializer for serialization //! This is to test Field Converters for patched element that are directly on the patched class InnerObjectFieldConverterV1 m_rootInnerObject{}; InnerObjectFieldConverterV1* m_baseInnerObjectPolymorphic{}; }; TEST_F(PatchingTest, ObjectFieldConverter_ChangeRootDataContainerField_ConvertsFromV1ToV2_Successfully) { using OriginalObjectField = ObjectFieldConverterV1; using PatchedObjectField = ObjectFieldConverterReplaceMemberDataConverterV2; ObjectWithNumericFieldV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterDerivedV1::Reflect(m_serializeContext.get()); OriginalObjectField::Reflect(m_serializeContext.get()); OriginalObjectField testObject; // Change the defaults of elements on the ObjectFieldConverterV1 object testObject.m_rootStringField = "Test1"; testObject.m_rootStringVector.emplace_back("Test2"); testObject.m_rootInnerObject.m_stringField = "InnerTest1"; testObject.m_rootInnerObject.m_stringVector.emplace_back("InnerTest2"); auto derivedInnerObject = aznew InnerObjectFieldConverterDerivedV1; derivedInnerObject->m_stringField = "DerivedTest1"; derivedInnerObject->m_stringVector.emplace_back("DerivedTest2"); derivedInnerObject->m_objectWithNumericField.m_value = 1; testObject.m_baseInnerObjectPolymorphic = derivedInnerObject; // Create DataPatch using ObjectFieldConverterV1 DataPatch testPatch; const OriginalObjectField initialObjectV1; testPatch.Create(&initialObjectV1, &testObject, {}, {}, m_serializeContext.get()); // Now unreflect the ObjectFieldConverterV1 and reflect ObjectFieldConverterReplaceMemberDataSerializerV2 { m_serializeContext->EnableRemoveReflection(); OriginalObjectField::Reflect(m_serializeContext.get()); m_serializeContext->DisableRemoveReflection(); PatchedObjectField::Reflect(m_serializeContext.get()); } const PatchedObjectField initialObjectV2; PatchedObjectField *patchedObjectV2 = testPatch.Apply(&initialObjectV2, m_serializeContext.get()); ASSERT_NE(nullptr, patchedObjectV2); EXPECT_EQ("Test2", patchedObjectV2->m_rootStringArray.front()); // Clean up original ObjectFieldConverterV1 object delete testObject.m_baseInnerObjectPolymorphic; // Clean up patched ObjectFieldConverterV1 object delete patchedObjectV2->m_baseInnerObjectPolymorphic; delete patchedObjectV2; m_serializeContext->EnableRemoveReflection(); ObjectWithNumericFieldV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterDerivedV1::Reflect(m_serializeContext.get()); PatchedObjectField::Reflect(m_serializeContext.get()); m_serializeContext->DisableRemoveReflection(); } TEST_F(PatchingTest, ObjectFieldConverter_ChangeRootDataContainerField_ConvertsFromV2ToV3_Successfully) { using OriginalObjectField = ObjectFieldConverterReplaceMemberDataConverterV2; using PatchedObjectField = ObjectFieldConverterReplaceMemberDataConverterV3; ObjectWithNumericFieldV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterDerivedV1::Reflect(m_serializeContext.get()); OriginalObjectField::Reflect(m_serializeContext.get()); OriginalObjectField testObject; // Change the defaults of elements on the ObjectFieldConverterV1 object testObject.m_rootStringField = "Test1"; testObject.m_rootStringArray[0] = "BigTest"; testObject.m_rootStringArray[3] = "SuperTest"; testObject.m_rootInnerObject.m_stringField = "InnerTest1"; testObject.m_rootInnerObject.m_stringVector.emplace_back("InnerTest2"); auto derivedInnerObject = aznew InnerObjectFieldConverterDerivedV1; derivedInnerObject->m_stringField = "DerivedTest1"; derivedInnerObject->m_stringVector.emplace_back("DerivedTest2"); derivedInnerObject->m_objectWithNumericField.m_value = 1; testObject.m_baseInnerObjectPolymorphic = derivedInnerObject; // Create DataPatch using ObjectFieldConverterV1 DataPatch testPatch; const OriginalObjectField initialObjectV1; testPatch.Create(&initialObjectV1, &testObject, {}, {}, m_serializeContext.get()); // Now unreflect the ObjectFieldConverterV1 and reflect ObjectFieldConverterReplaceMemberDataSerializerV2 { m_serializeContext->EnableRemoveReflection(); OriginalObjectField::Reflect(m_serializeContext.get()); m_serializeContext->DisableRemoveReflection(); PatchedObjectField::Reflect(m_serializeContext.get()); } const PatchedObjectField initialObjectV3; PatchedObjectField *patchedObjectV3 = testPatch.Apply(&initialObjectV3, m_serializeContext.get()); ASSERT_NE(nullptr, patchedObjectV3); ASSERT_EQ(2, patchedObjectV3->m_rootStringList.size()); auto patchListIter = patchedObjectV3->m_rootStringList.begin(); EXPECT_EQ("BigTest", *patchListIter++); EXPECT_EQ("SuperTest", *patchListIter++); // Clean up original ObjectFieldConverterV1 object delete testObject.m_baseInnerObjectPolymorphic; // Clean up patched ObjectFieldConverterV1 object delete patchedObjectV3->m_baseInnerObjectPolymorphic; delete patchedObjectV3; m_serializeContext->EnableRemoveReflection(); ObjectWithNumericFieldV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterDerivedV1::Reflect(m_serializeContext.get()); PatchedObjectField::Reflect(m_serializeContext.get()); m_serializeContext->DisableRemoveReflection(); } TEST_F(PatchingTest, ObjectFieldConverter_ChangeRootDataConverterField_ConvertsFromV1ToV3_Successfully) { using OriginalObjectField = ObjectFieldConverterV1; using PatchedObjectField = ObjectFieldConverterReplaceMemberDataConverterV3; ObjectWithNumericFieldV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterDerivedV1::Reflect(m_serializeContext.get()); OriginalObjectField::Reflect(m_serializeContext.get()); OriginalObjectField testObject; // Change the defaults of elements on the ObjectFieldConverterV1 object testObject.m_rootStringField = "StringV1"; testObject.m_rootStringVector.emplace_back("Test2"); testObject.m_rootInnerObject.m_stringField = "InnerTest1"; testObject.m_rootInnerObject.m_stringVector.emplace_back("InnerTest2"); auto derivedInnerObject = aznew InnerObjectFieldConverterDerivedV1; derivedInnerObject->m_stringField = "DerivedTest1"; derivedInnerObject->m_stringVector.emplace_back("DerivedTest2"); derivedInnerObject->m_objectWithNumericField.m_value = 1; testObject.m_baseInnerObjectPolymorphic = derivedInnerObject; // Create DataPatch using ObjectFieldConverterV1 DataPatch testPatch; const OriginalObjectField initialObjectV1; testPatch.Create(&initialObjectV1, &testObject, {}, {}, m_serializeContext.get()); // Now unreflect the ObjectFieldConverterV1 and reflect ObjectFieldConverterReplaceMemberDataSerializerV2 { m_serializeContext->EnableRemoveReflection(); OriginalObjectField::Reflect(m_serializeContext.get()); m_serializeContext->DisableRemoveReflection(); PatchedObjectField::Reflect(m_serializeContext.get()); } const PatchedObjectField initialObjectV3; PatchedObjectField *patchedObjectV3 = testPatch.Apply(&initialObjectV3, m_serializeContext.get()); ASSERT_NE(nullptr, patchedObjectV3); ASSERT_EQ(1, patchedObjectV3->m_rootStringList.size()); EXPECT_EQ("Test2", patchedObjectV3->m_rootStringList.front()); // Clean up original ObjectFieldConverterV1 object delete testObject.m_baseInnerObjectPolymorphic; // Clean up patched ObjectFieldConverterV1 object delete patchedObjectV3->m_baseInnerObjectPolymorphic; delete patchedObjectV3; m_serializeContext->EnableRemoveReflection(); ObjectWithNumericFieldV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterV1::Reflect(m_serializeContext.get()); InnerObjectFieldConverterDerivedV1::Reflect(m_serializeContext.get()); PatchedObjectField::Reflect(m_serializeContext.get()); m_serializeContext->DisableRemoveReflection(); } } struct ObjectWithAnyAndBool { AZ_CLASS_ALLOCATOR(ObjectWithAnyAndBool, SystemAllocator, 0); AZ_TYPE_INFO(ObjectWithAnyAndBool, "{266FD5C6-39AE-482F-99B7-DA2A1AFE1EA9}"); static void Reflect(AZ::ReflectContext* reflectContext) { if (auto serializeContext = azrtti_cast(reflectContext)) { serializeContext->Class() ->Version(1) ->Field("AnyValue", &ObjectWithAnyAndBool::m_any) ->Field("BoolValue", &ObjectWithAnyAndBool::m_bool); } } AZStd::any m_any; bool m_bool; }; TEST_F(PatchingTest, DataPatchingObjectWithAnyPreservesAnyData) { ObjectWithAnyAndBool::Reflect(m_serializeContext.get()); ObjectWithAnyAndBool firstObject; firstObject.m_any = AZStd::make_any(false); firstObject.m_bool = false; ObjectWithAnyAndBool secondObject; secondObject.m_any = AZStd::make_any(false); secondObject.m_bool = true; DataPatch testPatch; testPatch.Create(&firstObject, &secondObject, {}, {}, m_serializeContext.get()); ObjectWithAnyAndBool* finalObject = testPatch.Apply(&firstObject, m_serializeContext.get()); ASSERT_FALSE(finalObject->m_any.empty()); delete finalObject; m_serializeContext->EnableRemoveReflection(); ObjectWithAnyAndBool::Reflect(m_serializeContext.get()); m_serializeContext->DisableRemoveReflection(); } } }