/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace AZ; using namespace AZ::IO; using namespace AZ::Debug; namespace UnitTest { class TestComponent : public AZ::Component { public: AZ_COMPONENT(TestComponent, "{94D5C952-FD65-4997-B517-F36003F8018A}"); struct SubData { AZ_TYPE_INFO(SubData, "{A0165FCA-A311-4FED-B36A-DC5FD2AF2857}"); AZ_CLASS_ALLOCATOR(SubData, AZ::SystemAllocator, 0); SubData() {} SubData(int v) : m_int(v) {} ~SubData() = default; int m_int = 0; }; class SerializationEvents : public AZ::SerializeContext::IEventHandler { void OnReadBegin(void* classPtr) override { TestComponent* component = reinterpret_cast(classPtr); component->m_serializeOnReadBegin++; } void OnReadEnd(void* classPtr) override { TestComponent* component = reinterpret_cast(classPtr); component->m_serializeOnReadEnd++; } void OnWriteBegin(void* classPtr) override { TestComponent* component = reinterpret_cast(classPtr); component->m_serializeOnWriteBegin++; } void OnWriteEnd(void* classPtr) override { TestComponent* component = reinterpret_cast(classPtr); component->m_serializeOnWriteEnd++; } }; TestComponent() = default; ~TestComponent() override { for (SubData* data : m_pointerContainer) { delete data; } } static void Reflect(AZ::ReflectContext* context) { if (auto* serializeContext = azrtti_cast(context)) { serializeContext->Class() ->Version(1) ->Field("Int", &SubData::m_int) ; serializeContext->Class() ->EventHandler() ->Version(1) ->Field("Float", &TestComponent::m_float) ->Field("String", &TestComponent::m_string) ->Field("NormalContainer", &TestComponent::m_normalContainer) ->Field("PointerContainer", &TestComponent::m_pointerContainer) ->Field("SubData", &TestComponent::m_subData) ; if (AZ::EditContext* edit = serializeContext->GetEditContext()) { edit->Class("Test Component", "A test component") ->DataElement(0, &TestComponent::m_float, "Float Field", "A float field") ->DataElement(0, &TestComponent::m_string, "String Field", "A string field") ->DataElement(0, &TestComponent::m_normalContainer, "Normal Container", "A container") ->DataElement(0, &TestComponent::m_pointerContainer, "Pointer Container", "A container") ->DataElement(0, &TestComponent::m_subData, "Struct Field", "A sub data type") ; edit->Class("Test Component", "A test component") ->DataElement(0, &SubData::m_int, "Int Field", "An int") ; } } } void Activate() override { } void Deactivate() override { } float m_float = 0.f; AZStd::string m_string; AZStd::vector m_normalContainer; AZStd::vector m_pointerContainer; SubData m_subData; size_t m_serializeOnReadBegin = 0; size_t m_serializeOnReadEnd = 0; size_t m_serializeOnWriteBegin = 0; size_t m_serializeOnWriteEnd = 0; }; bool operator==(const TestComponent::SubData& lhs, const TestComponent::SubData& rhs) { return lhs.m_int == rhs.m_int; } /** * InstanceDataHierarchyBasicTest */ class InstanceDataHierarchyBasicTest : public AllocatorsFixture { public: InstanceDataHierarchyBasicTest() { } ~InstanceDataHierarchyBasicTest() { } void run() { using namespace AzToolsFramework; AZ::SerializeContext serializeContext; serializeContext.CreateEditContext(); Entity::Reflect(&serializeContext); TestComponent::Reflect(&serializeContext); // Test building of hierarchies, and copying of data from testEntity1 to testEntity2-> { AZStd::unique_ptr testEntity1(new AZ::Entity()); testEntity1->CreateComponent(); AZStd::unique_ptr testEntity2(serializeContext.CloneObject(testEntity1.get())); AZ_TEST_ASSERT(testEntity1->FindComponent()->m_serializeOnReadBegin == 1); AZ_TEST_ASSERT(testEntity1->FindComponent()->m_serializeOnReadEnd == 1); AZ_TEST_ASSERT(testEntity2->FindComponent()->m_serializeOnWriteBegin == 1); AZ_TEST_ASSERT(testEntity2->FindComponent()->m_serializeOnWriteEnd == 1); testEntity1->FindComponent()->m_float = 1.f; testEntity1->FindComponent()->m_normalContainer.push_back(TestComponent::SubData(1)); testEntity1->FindComponent()->m_normalContainer.push_back(TestComponent::SubData(2)); testEntity1->FindComponent()->m_pointerContainer.push_back(aznew TestComponent::SubData(1)); testEntity1->FindComponent()->m_pointerContainer.push_back(aznew TestComponent::SubData(2)); // First entity has more entries, so we'll be adding elements to testEntity2-> testEntity2->FindComponent()->m_float = 2.f; testEntity2->FindComponent()->m_normalContainer.push_back(TestComponent::SubData(1)); testEntity2->FindComponent()->m_pointerContainer.push_back(aznew TestComponent::SubData(1)); InstanceDataHierarchy idh1; idh1.AddRootInstance(testEntity1.get()); idh1.Build(&serializeContext, 0); AZ_TEST_ASSERT(testEntity1->FindComponent()->m_serializeOnReadBegin == 2); AZ_TEST_ASSERT(testEntity1->FindComponent()->m_serializeOnReadEnd == 2); InstanceDataHierarchy idh2; idh2.AddRootInstance(testEntity2.get()); idh2.Build(&serializeContext, 0); AZ_TEST_ASSERT(testEntity2->FindComponent()->m_serializeOnReadBegin == 1); AZ_TEST_ASSERT(testEntity2->FindComponent()->m_serializeOnReadEnd == 1); // Verify IDH structure. InstanceDataNode* root1 = idh1.GetRootNode(); AZ_TEST_ASSERT(root1); InstanceDataNode* root2 = idh2.GetRootNode(); AZ_TEST_ASSERT(root2); auto secondChildIter = root1->GetChildren().begin(); AZStd::advance(secondChildIter, 1); InstanceDataNode::Address addr = secondChildIter->ComputeAddress(); AZ_TEST_ASSERT(!addr.empty()); InstanceDataNode* foundIn2 = idh2.FindNodeByAddress(addr); AZ_TEST_ASSERT(foundIn2); // Find the TestComponent in entity1's IDH. AZStd::stack nodeStack; nodeStack.push(root1); InstanceDataNode* componentNode1 = nullptr; while (!nodeStack.empty()) { InstanceDataNode* node = nodeStack.top(); nodeStack.pop(); if (node->GetClassMetadata()->m_typeId == AZ::AzTypeInfo::Uuid()) { componentNode1 = node; break; } for (InstanceDataNode& child : node->GetChildren()) { nodeStack.push(&child); } } // Verify we found the component node in both hierarchies. AZ_TEST_ASSERT(componentNode1); addr = componentNode1->ComputeAddress(); foundIn2 = idh2.FindNodeByAddress(addr); AZ_TEST_ASSERT(foundIn2); //// Try copying data from entity 1 to entity 2. bool result = InstanceDataHierarchy::CopyInstanceData(componentNode1, foundIn2, &serializeContext); AZ_TEST_ASSERT(result); AZ_TEST_ASSERT(testEntity1->FindComponent()->m_serializeOnReadBegin == 2); AZ_TEST_ASSERT(testEntity1->FindComponent()->m_serializeOnReadEnd == 2); AZ_TEST_ASSERT(testEntity2->FindComponent()->m_serializeOnWriteBegin == 2); AZ_TEST_ASSERT(testEntity2->FindComponent()->m_serializeOnWriteEnd == 2); AZ_TEST_ASSERT(testEntity2->FindComponent()->m_normalContainer.size() == 2); AZ_TEST_ASSERT(testEntity2->FindComponent()->m_pointerContainer.size() == 2); AZ_TEST_ASSERT(testEntity2->FindComponent()->m_float == 1.f); } // Test removal of container elements during instance data copying. { AZStd::unique_ptr testEntity1(new AZ::Entity()); testEntity1->CreateComponent(); AZStd::unique_ptr testEntity2(serializeContext.CloneObject(testEntity1.get())); // First entity has more in container 1, fewer in container 2 as compared to second entity. testEntity1->FindComponent()->m_normalContainer.push_back(TestComponent::SubData(1)); testEntity1->FindComponent()->m_normalContainer.push_back(TestComponent::SubData(2)); testEntity1->FindComponent()->m_pointerContainer.push_back(aznew TestComponent::SubData(1)); testEntity2->FindComponent()->m_normalContainer.push_back(TestComponent::SubData(1)); testEntity2->FindComponent()->m_pointerContainer.push_back(aznew TestComponent::SubData(1)); testEntity2->FindComponent()->m_pointerContainer.push_back(aznew TestComponent::SubData(2)); // Change a field. testEntity2->FindComponent()->m_float = 2.f; InstanceDataHierarchy idh1; idh1.AddRootInstance(testEntity1.get()); idh1.Build(&serializeContext, 0); InstanceDataHierarchy idh2; idh2.AddRootInstance(testEntity2.get()); idh2.Build(&serializeContext, 0); InstanceDataNode* root1 = idh1.GetRootNode(); // Find the TestComponent in entity1's IDH. AZStd::stack nodeStack; nodeStack.push(root1); InstanceDataNode* componentNode1 = nullptr; while (!nodeStack.empty()) { InstanceDataNode* node = nodeStack.top(); nodeStack.pop(); if (node->GetClassMetadata()->m_typeId == AZ::AzTypeInfo::Uuid()) { componentNode1 = node; break; } for (InstanceDataNode& child : node->GetChildren()) { nodeStack.push(&child); } } // Verify we found the component node in both hierarchies. AZ_TEST_ASSERT(componentNode1); InstanceDataNode::Address addr = componentNode1->ComputeAddress(); InstanceDataNode* foundIn2 = idh2.FindNodeByAddress(addr); AZ_TEST_ASSERT(foundIn2); // Do a comparison test { size_t newNodes = 0; size_t removedNodes = 0; size_t changedNodes = 0; InstanceDataHierarchy::CompareHierarchies(componentNode1, foundIn2, &InstanceDataHierarchy::DefaultValueComparisonFunction, &serializeContext, // New node [&](InstanceDataNode* targetNode, AZStd::vector& data) { (void)targetNode; (void)data; ++newNodes; }, // Removed node (container element). [&](const InstanceDataNode* sourceNode, InstanceDataNode* targetNodeParent) { (void)sourceNode; (void)targetNodeParent; ++removedNodes; }, // Changed node [&](const InstanceDataNode* sourceNode, InstanceDataNode* targetNode, AZStd::vector& sourceData, AZStd::vector& targetData) { (void)sourceNode; (void)targetNode; (void)sourceData; (void)targetData; ++changedNodes; } ); AZ_TEST_ASSERT(newNodes == 2); // 2 because child nodes of new nodes are now also flagged as new AZ_TEST_ASSERT(removedNodes == 1); AZ_TEST_ASSERT(changedNodes == 1); } //// Try copying data from entity 1 to entity 2. bool result = InstanceDataHierarchy::CopyInstanceData(componentNode1, foundIn2, &serializeContext); AZ_TEST_ASSERT(result); AZ_TEST_ASSERT(testEntity2->FindComponent()->m_normalContainer.size() == 2); AZ_TEST_ASSERT(testEntity2->FindComponent()->m_pointerContainer.size() == 1); } // Test FindNodeByPartialAddress functionality and Read/Write of InstanceDataNode { const AZStd::string testString = "this is a test"; const float testFloat = 123.0f; const int testInt = 7; const TestComponent::SubData testSubData(testInt); const AZStd::vector testNormalContainer{ TestComponent::SubData(1), TestComponent::SubData(2), TestComponent::SubData(3) }; // create a test component with some initial values AZStd::unique_ptr testComponent(new TestComponent); testComponent.get()->m_float = testFloat; testComponent.get()->m_string = testString; testComponent.get()->m_normalContainer = testNormalContainer; testComponent.get()->m_subData.m_int = testInt; // create an InstanceDataHierarchy for the test component InstanceDataHierarchy idhTestComponent; idhTestComponent.AddRootInstance(testComponent.get()); idhTestComponent.Build(&serializeContext, 0); // create some partial addresses to search for fields in InstanceDataHierarchy // note: reflection serialization context values are used for lookup (crcs stored) // if a more specific address is required, start from field and work up to structures/components etc // (see addrSubDataInt below as an example) InstanceDataNode::Address addrFloat = { AZ_CRC("Float") }; InstanceDataNode::Address addrString = { AZ_CRC("String") }; InstanceDataNode::Address addrNormalContainer = { AZ_CRC("NormalContainer") }; InstanceDataNode::Address addrSubData = { AZ_CRC("SubData") }; InstanceDataNode::Address addrSubDataInt = { AZ_CRC("Int"), AZ_CRC("SubData") }; // find InstanceDataNodes using partial address InstanceDataNode* foundFloat = idhTestComponent.FindNodeByPartialAddress(addrFloat); InstanceDataNode* foundString = idhTestComponent.FindNodeByPartialAddress(addrString); InstanceDataNode* foundNormalContainer = idhTestComponent.FindNodeByPartialAddress(addrNormalContainer); InstanceDataNode* foundSubData = idhTestComponent.FindNodeByPartialAddress(addrSubData); InstanceDataNode* foundSubDataInt = idhTestComponent.FindNodeByPartialAddress(addrSubDataInt); // ensure each has been returned successfully AZ_TEST_ASSERT(foundFloat); AZ_TEST_ASSERT(foundString); AZ_TEST_ASSERT(foundNormalContainer); AZ_TEST_ASSERT(foundSubData); AZ_TEST_ASSERT(foundSubDataInt); // check a case where we know the address is incorrect and we will not find an InstanceDataNode InstanceDataNode::Address addrInvalid = { AZ_CRC("INVALID") }; InstanceDataNode* foundInvalid = idhTestComponent.FindNodeByPartialAddress(addrInvalid); AZ_TEST_ASSERT(foundInvalid == nullptr); /////////////////////////////////////////////////////////////////////////////// // test the values read from the InstanceDataNodes are the same as the ones our TestComponent were constructed with float readTestFloat; foundFloat->Read(readTestFloat); AZ_TEST_ASSERT(readTestFloat == testFloat); AZStd::string readTestString; foundString->Read(readTestString); AZ_TEST_ASSERT(readTestString == testString); int readTestInt; foundSubDataInt->Read(readTestInt); AZ_TEST_ASSERT(readTestInt == testInt); TestComponent::SubData readTestSubData; foundSubData->Read(readTestSubData); AZ_TEST_ASSERT(readTestSubData == testSubData); AZStd::vector readTestNormalContainer; foundNormalContainer->Read(readTestNormalContainer); AZ_TEST_ASSERT(readTestNormalContainer == testNormalContainer); // create some new test values to write to the InstanceDataNode const AZStd::string newTestString = "this string has been updated!"; const float newTestFloat = 456.0f; const int newTestInt = 94; const TestComponent::SubData newTestSubData(newTestInt); const AZStd::vector newTestNormalContainer{ TestComponent::SubData(20), TestComponent::SubData(40), TestComponent::SubData(60) }; // actually write the values to each InstanceDataNode foundFloat->Write(newTestFloat); foundString->Write(newTestString); foundSubData->Write(newTestSubData); foundNormalContainer->Write(newTestNormalContainer); // read the values back to make sure the are the same as the newly set values AZStd::string updatedTestString; foundString->Read(updatedTestString); AZ_TEST_ASSERT(updatedTestString == newTestString); float updatedTestFloat; foundFloat->Read(updatedTestFloat); AZ_TEST_ASSERT(updatedTestFloat == newTestFloat); TestComponent::SubData updatedTestSubData; foundSubData->Read(updatedTestSubData); AZ_TEST_ASSERT(updatedTestSubData == newTestSubData); AZStd::vector updatedNormalContainer; foundNormalContainer->Read(updatedNormalContainer); AZ_TEST_ASSERT(updatedNormalContainer == newTestNormalContainer); } } }; static AZ::u8 s_persistentIdCounter = 0; class InstanceDataHierarchyCopyContainerChangesTest : public AllocatorsFixture { public: InstanceDataHierarchyCopyContainerChangesTest() { } ~InstanceDataHierarchyCopyContainerChangesTest() { } class StructInner { public: AZ_TYPE_INFO(StructInner, "{4BFA2A4F-8568-43AA-941C-8361DBA13CBB}"); AZ::u8 m_persistentId; AZ::u32 m_value; StructInner() { m_value = 1; m_persistentId = ++s_persistentIdCounter; } static void Reflect(AZ::SerializeContext& context) { context.Class()-> PersistentId([](const void* instance) -> u64 { return static_cast(reinterpret_cast(instance)->m_persistentId); })-> Field("Id", &StructInner::m_persistentId)-> Field("Value", &StructInner::m_value) ; } }; class StructOuter { public: AZ_TYPE_INFO(StructInner, "{FEDCED26-8D5A-41CB-BA97-AB687CF51FC6}"); AZStd::vector m_vector; StructOuter() { } static void Reflect(AZ::SerializeContext& context) { context.Class()-> Field("Vector", &StructOuter::m_vector) ; } }; void DoCopy(StructOuter& source, StructOuter& target, AZ::SerializeContext& ctx) { AzToolsFramework::InstanceDataHierarchy sourceHier; sourceHier.AddRootInstance(&source, AZ::AzTypeInfo::Uuid()); sourceHier.Build(&ctx, AZ::SerializeContext::ENUM_ACCESS_FOR_READ); AzToolsFramework::InstanceDataHierarchy targetHier; targetHier.AddRootInstance(&target, AZ::AzTypeInfo::Uuid()); targetHier.Build(&ctx, AZ::SerializeContext::ENUM_ACCESS_FOR_READ); AzToolsFramework::InstanceDataHierarchy::CopyInstanceData(&sourceHier, &targetHier, &ctx); } void VerifyMatch(StructOuter& source, StructOuter& target) { AZ_TEST_ASSERT(source.m_vector.size() == target.m_vector.size()); // Make sure that matching elements have the same data (we're using persistent Ids, so order can be whatever). for (auto& sourceElement : source.m_vector) { for (auto& targetElement : target.m_vector) { if (targetElement.m_persistentId == sourceElement.m_persistentId) { AZ_TEST_ASSERT(targetElement.m_value == sourceElement.m_value); break; } } } } void run() { using namespace AzToolsFramework; AZ::SerializeContext serializeContext; serializeContext.CreateEditContext(); StructInner::Reflect(serializeContext); StructOuter::Reflect(serializeContext); StructOuter outerSource; StructOuter outerTarget; StructOuter originalSource; originalSource.m_vector.emplace_back(); originalSource.m_vector.emplace_back(); originalSource.m_vector.emplace_back(); { outerSource = originalSource; DoCopy(outerSource, outerTarget, serializeContext); AZ_TEST_ASSERT(outerTarget.m_vector.size() == 3); } { outerSource = originalSource; outerTarget = outerSource; // Pluck from the start of the array so elements get shifted. // Also modify something in the last element so it's written to the target. // This verifies that removals are applied safely alongside data changes. outerSource.m_vector.erase(outerSource.m_vector.begin()); outerSource.m_vector.begin()->m_value = 2; DoCopy(outerSource, outerTarget, serializeContext); VerifyMatch(outerSource, outerTarget); } { outerSource = originalSource; outerTarget = outerSource; // Remove an element from the target and SHRINK the array to fit so it's // guaranteed to grow when the missing element is copied from the source. // This verifies that additions are being applied safely alongside data changes. outerTarget.m_vector.erase(outerTarget.m_vector.begin()); outerTarget.m_vector.set_capacity(outerTarget.m_vector.size()); // Force grow on insert outerSource.m_vector.back().m_value = 5; DoCopy(outerSource, outerTarget, serializeContext); VerifyMatch(outerSource, outerTarget); } { outerSource = originalSource; outerTarget = outerSource; // Add elements to the source. // Add an element to the target. // Change a different element. // This tests removals, additions, and changes occurring together, with net growth in the target container. outerSource.m_vector.emplace_back(); outerSource.m_vector.emplace_back(); outerTarget.m_vector.emplace_back(); outerTarget.m_vector.set_capacity(outerTarget.m_vector.size()); // Force grow on insert outerTarget.m_vector.begin()->m_value = 10; DoCopy(outerSource, outerTarget, serializeContext); VerifyMatch(outerSource, outerTarget); } } }; enum class TestEnum { Value1 = 0x01, Value2 = 0x02, Value3 = 0xFF, }; } namespace AZ { AZ_TYPE_INFO_SPECIALIZE(UnitTest::TestEnum, "{52DBDCC6-0829-4602-A650-E6FC32AFC5F2}"); } namespace UnitTest { class InstanceDataHierarchyEnumContainerTest : public AllocatorsFixture { public: class EnumContainer { public: AZ_TYPE_INFO(EnumContainer, "{7F9EED53-7587-4616-B4A7-10B3AF95475E}"); AZ_CLASS_ALLOCATOR(EnumContainer, AZ::SystemAllocator, 0); TestEnum m_enum; AZStd::vector m_enumVector; static void Reflect(AZ::SerializeContext& context) { context.Class() ->Field("Enum", &EnumContainer::m_enum) ->Field("EnumVector", &EnumContainer::m_enumVector) ; if (EditContext* edit = context.GetEditContext()) { edit->Enum("TestEnum", "No Description") ->Value("Value1", UnitTest::TestEnum::Value1) ->Value("Value2", UnitTest::TestEnum::Value2) ->Value("Value3", UnitTest::TestEnum::Value3) ; edit->Class("Enum Container", "Test container that has an external enum") ->DataElement(0, &EnumContainer::m_enum, "Enum Field", "An enum value") ->DataElement(0, &EnumContainer::m_enumVector, "Enum Vector Field", "A vector of enum values") ; } } }; void run() { using namespace AzToolsFramework; AZ::SerializeContext serializeContext; serializeContext.CreateEditContext(); EnumContainer::Reflect(serializeContext); EnumContainer ec; ec.m_enumVector.emplace_back(UnitTest::TestEnum::Value3); InstanceDataHierarchy idh; idh.AddRootInstance(&ec, azrtti_typeid()); idh.Build(&serializeContext, 0); InstanceDataNode* enumNode = idh.FindNodeByPartialAddress({ AZ_CRC("Enum") }); InstanceDataNode* enumVectorNode = idh.FindNodeByPartialAddress({ AZ_CRC("EnumVector") }); ASSERT_NE(enumNode, nullptr); ASSERT_NE(enumVectorNode, nullptr); auto getEnumData = [&ec](const AzToolsFramework::InstanceDataNode& node) -> Uuid { Uuid id; auto attribute = node.GetElementMetadata()->FindAttribute(AZ_CRC("EnumType")); auto attributeData = azrtti_cast*>(attribute); if (attributeData) { id = attributeData->Get(&ec); } return id; }; EXPECT_EQ(getEnumData(*enumNode), RttiTypeId()); const auto& vectorEntries = enumVectorNode->GetChildren(); ASSERT_EQ(vectorEntries.size(), 1); EXPECT_EQ(getEnumData(*vectorEntries.begin()), RttiTypeId()); } }; class InstanceDataHierarchyKeyedContainerTest : public AllocatorsFixture { public: class CustomKeyWithoutStringRepresentation { public: AZ_TYPE_INFO(CustomKeyWithoutStringRepresentation, "{54E838DE-1A8D-4BBA-BD3A-D41886C439A9}"); AZ_CLASS_ALLOCATOR(CustomKeyWithoutStringRepresentation, AZ::SystemAllocator, 0); int m_value = 0; int operator<(const CustomKeyWithoutStringRepresentation& other) const { return m_value < other.m_value; } }; class CustomKeyWithStringRepresentation { public: AZ_TYPE_INFO(CustomKeyWithStringRepresentation, "{51F7FB74-2991-4CC9-850A-8D5AA0732282}"); AZ_CLASS_ALLOCATOR(CustomKeyWithStringRepresentation, AZ::SystemAllocator, 0); static const char* KeyPrefix() { return "CustomKey"; } int m_value = 0; int operator<(const CustomKeyWithStringRepresentation& other) const { return m_value < other.m_value; } AZStd::string ToString() const { return AZStd::string::format("%s %i", KeyPrefix(), m_value); } }; class KeyedContainer { public: AZ_TYPE_INFO(KeyedContainer, "{53A7416F-2D84-4256-97B0-BE4B6EF6DBAF}"); AZ_CLASS_ALLOCATOR(KeyedContainer, AZ::SystemAllocator, 0); AZStd::map m_map; AZStd::unordered_map, int> m_unorderedMap; AZStd::set m_set; AZStd::unordered_set m_unorderedSet; AZStd::unordered_multimap m_multiMap; AZStd::unordered_map> m_nestedMap; AZStd::map m_uncollapsableMap; AZStd::map m_collapsableMap; static void Reflect(AZ::SerializeContext& context) { context.Class() ->Field("value", &CustomKeyWithoutStringRepresentation::m_value); context.Class() ->Field("value", &CustomKeyWithStringRepresentation::m_value); context.Class() ->Field("map", &KeyedContainer::m_map) ->Field("unorderedMap", &KeyedContainer::m_unorderedMap) ->Field("set", &KeyedContainer::m_set) ->Field("unorderedSet", &KeyedContainer::m_unorderedSet) ->Field("multiMap", &KeyedContainer::m_multiMap) ->Field("nestedMap", &KeyedContainer::m_nestedMap) ->Field("uncollapsableMap", &KeyedContainer::m_uncollapsableMap) ->Field("collapsableMap", &KeyedContainer::m_collapsableMap); if (auto editContext = context.GetEditContext()) { editContext->Class("CustomKeyWithStringRepresentation", "") ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::ConciseEditorStringRepresentation, &CustomKeyWithStringRepresentation::ToString); } } }; struct KeyTestData { virtual void InsertAndVerifyKeys(AZ::SerializeContext::IDataContainer* container, void* key, void* instance, const AZ::SerializeContext::ClassElement* classElement) const = 0; virtual AZ::Uuid ExpectedKeyType() const = 0; virtual size_t NumberOfKeys() const = 0; virtual ~KeyTestData() {} }; template struct TypedKeyTestData : public KeyTestData { AZStd::vector keysToInsert; TypedKeyTestData(std::initializer_list keys) : keysToInsert(keys) { } void InsertAndVerifyKeys(AZ::SerializeContext::IDataContainer* container, void* key, void* instance, const AZ::SerializeContext::ClassElement* classElement) const { T* keyContainer = reinterpret_cast(key); for (const T& keyToInsert : keysToInsert) { *keyContainer = keyToInsert; void* element = container->ReserveElement(instance, classElement); auto associativeInterface = container->GetAssociativeContainerInterface(); associativeInterface->SetElementKey(element, key); container->StoreElement(instance, element); auto lookupKey = associativeInterface->GetElementByKey(instance, classElement, (void*)(&keyToInsert)); EXPECT_NE(lookupKey, nullptr); } } AZ::Uuid ExpectedKeyType() const override { return azrtti_typeid>(); } size_t NumberOfKeys() const override { return keysToInsert.size(); } static AZStd::unique_ptr> Create(std::initializer_list keys) { return AZStd::make_unique>(keys); } }; void run() { using namespace AzToolsFramework; AZ::SerializeContext serializeContext; serializeContext.CreateEditContext(); KeyedContainer::Reflect(serializeContext); KeyedContainer kc; InstanceDataHierarchy idh; idh.AddRootInstance(&kc, azrtti_typeid()); idh.Build(&serializeContext, 0); AZStd::unordered_map> keyTestData; keyTestData[AZ_CRC("map")] = TypedKeyTestData::Create({"A", "B", "lorem ipsum"}); keyTestData[AZ_CRC("unorderedMap")] = TypedKeyTestData>::Create({ {5, 1.0}, {5, -2.0} }); keyTestData[AZ_CRC("set")] = TypedKeyTestData::Create({2, 4, -255, 999}); keyTestData[AZ_CRC("unorderedSet")] = TypedKeyTestData::Create({500000, 9, 0, 42, 42}); keyTestData[AZ_CRC("multiMap")] = TypedKeyTestData::Create({-1, 2, -3, 4, -5, 6}); keyTestData[AZ_CRC("nestedMap")] = TypedKeyTestData::Create({1, 10, 100, 1000}); keyTestData[AZ_CRC("uncollapsableMap")] = TypedKeyTestData::Create({{0}, {1}}); keyTestData[AZ_CRC("collapsableMap")] = TypedKeyTestData::Create({{0}, {1}}); auto insertKeysIntoContainer = [&serializeContext](AzToolsFramework::InstanceDataNode& node, KeyTestData* keysToInsert) { const AZ::SerializeContext::ClassElement* element = node.GetElementMetadata(); AZ::SerializeContext::IDataContainer* container = node.GetClassMetadata()->m_container; ASSERT_NE(element, nullptr); ASSERT_NE(container, nullptr); const AZ::SerializeContext::ClassElement* containerClassElement = container->GetElement(container->GetDefaultElementNameCrc()); auto associativeInterface = container->GetAssociativeContainerInterface(); ASSERT_NE(associativeInterface, nullptr); auto key = associativeInterface->CreateKey(); auto attribute = containerClassElement ->FindAttribute(AZ_CRC("KeyType")); auto attributeData = azrtti_cast*>(attribute); ASSERT_NE(attributeData, nullptr); auto keyId = attributeData->Get(node.FirstInstance()); ASSERT_EQ(keyId, keysToInsert->ExpectedKeyType()); // Ensure we can build an InstanceDataHierarchy at runtime from the container's KeyType InstanceDataHierarchy idh2; idh2.AddRootInstance(key.get(), keyId); idh2.Build(&serializeContext, 0); auto children = idh2.GetChildren(); EXPECT_EQ(children.size(), 1); keysToInsert->InsertAndVerifyKeys(container, key.get(), node.FirstInstance(), element); }; for (InstanceDataNode& node : idh.GetChildren()) { const AZ::SerializeContext::ClassElement* element = node.GetElementMetadata(); auto insertIterator = keyTestData.find(element->m_nameCrc); ASSERT_NE(insertIterator, keyTestData.end()); auto keysToInsert = insertIterator->second.get(); insertKeysIntoContainer(node, keysToInsert); } auto nestedKeys = TypedKeyTestData::Create({2, 4, 8, 16}); idh.Build(&serializeContext, 0); for (InstanceDataNode& node : idh.GetChildren()) { const AZ::SerializeContext::ClassElement* element = node.GetElementMetadata(); if (element->m_nameCrc == AZ_CRC("nestedMap")) { auto children = node.GetChildren(); // We should have entries for each inserted key in the nested map EXPECT_EQ(children.size(), keyTestData[AZ_CRC("nestedMap")]->NumberOfKeys()); for (AzToolsFramework::InstanceDataNode& child : children) { insertKeysIntoContainer(child.GetChildren().back(), nestedKeys.get()); } } else if (element->m_nameCrc == AZ_CRC("collapsableMap")) { auto children = node.GetChildren(); EXPECT_GT(children.size(), 0); for (AzToolsFramework::InstanceDataNode& child : children) { // Ensure we're getting keys with the correct prefix based on the ConciseEditorStringRepresentation AZStd::string name = child.GetElementEditMetadata()->m_name; EXPECT_NE(name.find(CustomKeyWithStringRepresentation::KeyPrefix()), AZStd::string::npos); } } else if (element->m_nameCrc == AZ_CRC("uncollapsableMap")) { auto children = node.GetChildren(); EXPECT_GT(children.size(), 0); for (AzToolsFramework::InstanceDataNode& child : children) { auto keyValueChildren = child.GetChildren(); EXPECT_EQ(keyValueChildren.size(), 2); auto keyValueChildrenIterator = keyValueChildren.begin(); auto keyNode = *keyValueChildrenIterator; ++keyValueChildrenIterator; auto valueNode = *keyValueChildrenIterator; // Ensure key/value pairs that can't be collapsed get labels based on type EXPECT_EQ(AZ::Crc32(keyNode.GetElementEditMetadata()->m_name), AZ_CRC("Key")); EXPECT_EQ(AZ::Crc32(valueNode.GetElementEditMetadata()->m_name), AZ_CRC("Value")); } } } // Ensure IgnoreKeyValuePairs is respected idh.SetBuildFlags(InstanceDataHierarchy::Flags::IgnoreKeyValuePairs); idh.Build(&serializeContext, 0); for (InstanceDataNode& node : idh.GetChildren()) { const AZ::SerializeContext::ClassElement* element = node.GetElementMetadata(); if (element->m_nameCrc == AZ_CRC("map") || element->m_nameCrc == AZ_CRC("unorderedMap") || element->m_nameCrc == AZ_CRC("nestedMap")) { for (InstanceDataNode& pair : node.GetChildren()) { EXPECT_EQ(pair.GetChildren().size(), 2); } } } } }; class InstanceDataHierarchyCompareAssociativeContainerTest : public AllocatorsFixture { public: class Container { public: AZ_TYPE_INFO(Container, "{9920B5BD-F21C-4353-9449-9C3FD38E50FC}"); AZ_CLASS_ALLOCATOR(Container, AZ::SystemAllocator, 0); AZStd::unordered_map m_map; static void Reflect(AZ::SerializeContext& context) { context.Class() ->Field("map", &Container::m_map); } }; void run() { using namespace AzToolsFramework; AZ::AllocatorInstance::Create(); AZ::SerializeContext serializeContext; Container::Reflect(serializeContext); Container c1; c1.m_map = { {"A", 1}, {"B", 2}, {"C", 3} }; Container c2; c2.m_map = { {"C", 1}, {"A", 2}, {"B", 3} }; Container c3; c3.m_map = { {"A", 2}, {"D", 3} }; auto testComparison = [&](Container& baseInstance, Container& compareInstance, AZStd::unordered_set expectedAdds, AZStd::unordered_set expectedRemoves, AZStd::unordered_set expectedChanges) { InstanceDataHierarchy idhBase; idhBase.AddRootInstance(&baseInstance, azrtti_typeid()); idhBase.Build(&serializeContext, 0); InstanceDataHierarchy idhCompare; idhCompare.AddRootInstance(&compareInstance, azrtti_typeid()); idhCompare.Build(&serializeContext, 0); AZStd::unordered_set actualAdds; AZStd::unordered_set actualRemoves; AZStd::unordered_set actualChanges; auto newNodeCB = [&](InstanceDataNode* newNode, AZStd::vector&) { actualAdds.insert(newNode->GetElementEditMetadata()->m_name); }; auto removedNodeCB = [&](const InstanceDataNode* sourceNode, InstanceDataNode*) { actualRemoves.insert(sourceNode->GetElementEditMetadata()->m_name); }; auto changedNodeCB = [&](const InstanceDataNode* sourceNode, const InstanceDataNode*, AZStd::vector&, AZStd::vector&) { actualChanges.insert(sourceNode->GetParent()->GetElementEditMetadata()->m_name); }; InstanceDataHierarchy::CompareHierarchies(&idhBase, &idhCompare, &InstanceDataHierarchy::DefaultValueComparisonFunction, &serializeContext, newNodeCB, removedNodeCB, changedNodeCB ); EXPECT_EQ(expectedAdds, actualAdds); EXPECT_EQ(expectedRemoves, actualRemoves); EXPECT_EQ(expectedChanges, actualChanges); }; Container cCopy = c1; testComparison(c1, cCopy, {}, {}, {}); testComparison(c1, c3, {"D", "[0]", "[1]"}, {"B", "C"}, {"A"}); testComparison(c3, c1, {"B", "C", "[0]", "[1]"}, {"D"}, {"A"}); testComparison(c1, c2, {}, {}, {"A", "B", "C"}); AZ::AllocatorInstance::Destroy(); } }; class InstanceDataHierarchyElementTest : public AllocatorsFixture { public: class UIElementContainer { public: AZ_TYPE_INFO(UIElementContainer, "{83B7BDFD-8B60-4C52-B7C5-BF3C824620F5}"); AZ_CLASS_ALLOCATOR(UIElementContainer, AZ::SystemAllocator, 0); int m_data; static void Reflect(AZ::SerializeContext& context) { context.Class() ->Field("data", &UIElementContainer::m_data); if (auto editContext = context.GetEditContext()) { editContext->Class("Test", "") ->UIElement("TestHandler", "UIElement") ->DataElement(0, &UIElementContainer::m_data) ->UIElement(AZ_CRC("TestHandler2"), "UIElement2") ; } } }; void run() { using namespace AzToolsFramework; AZ::SerializeContext serializeContext; serializeContext.CreateEditContext(); UIElementContainer::Reflect(serializeContext); UIElementContainer test; InstanceDataHierarchy idh; idh.AddRootInstance(&test, azrtti_typeid()); idh.Build(&serializeContext, 0); auto children = idh.GetChildren(); ASSERT_EQ(children.size(), 3); auto it = children.begin(); Crc32 uiHandler = 0; EXPECT_EQ(it->ReadAttribute(AZ::Edit::UIHandlers::Handler, uiHandler), true); EXPECT_EQ(uiHandler, AZ_CRC("TestHandler")); EXPECT_EQ(it->GetElementMetadata()->m_name, "UIElement"); EXPECT_EQ(it->GetElementMetadata()->m_nameCrc, AZ_CRC("UIElement")); uiHandler = 0; ++it; ++it; EXPECT_EQ(it->ReadAttribute(AZ::Edit::UIHandlers::Handler, uiHandler), true); EXPECT_EQ(uiHandler, AZ_CRC("TestHandler2")); EXPECT_EQ(it->GetElementMetadata()->m_name, "UIElement2"); EXPECT_EQ(it->GetElementMetadata()->m_nameCrc, AZ_CRC("UIElement2")); } }; class InstanceDataHierarchyAggregateInstanceTest : public AllocatorsFixture { public: class AggregatedContainer { public: AZ_TYPE_INFO(AggregatedContainer, "{42E09F38-2D26-4FED-9901-06003A030ED5}"); AZ_CLASS_ALLOCATOR(AggregatedContainer, AZ::SystemAllocator, 0); int m_aggregated; int m_notAggregated; static void Reflect(AZ::SerializeContext& context) { context.Class() ->Field("aggregatedDataElement", &AggregatedContainer::m_aggregated) ->Field("notAggregatedDataElement", &AggregatedContainer::m_notAggregated) ; if (auto editContext = context.GetEditContext()) { // By default, DataElements accept multi-edit and UIElements do not editContext->Class("Test", "") ->DataElement(0, &AggregatedContainer::m_aggregated) ->DataElement(0, &AggregatedContainer::m_notAggregated) ->Attribute(AZ::Edit::Attributes::AcceptsMultiEdit, false) ->UIElement("TestHandler", "aggregatedUIElement") ->Attribute(AZ::Edit::Attributes::AcceptsMultiEdit, true) ->UIElement(AZ_CRC("TestHandler2"), "notAggregatedUIElement") ; } } }; void run() { using namespace AzToolsFramework; AZ::SerializeContext serializeContext; serializeContext.CreateEditContext(); AggregatedContainer::Reflect(serializeContext); InstanceDataHierarchy idh; AZStd::list containers; for (int i = 0; i < 5; ++i) { containers.push_back(); AggregatedContainer& container = containers.back(); idh.AddRootInstance(&container, azrtti_typeid()); idh.Build(&serializeContext, 0); auto children = idh.GetChildren(); // If we have multiple instances, the two non-aggregating elements should go away ASSERT_EQ(children.size(), i == 0 ? 4 : 2); auto it = children.begin(); EXPECT_EQ(it->GetElementMetadata()->m_name, "aggregatedDataElement"); ++it; if (i == 0) { EXPECT_EQ(it->GetElementMetadata()->m_name, "notAggregatedDataElement"); ++it; } EXPECT_EQ(it->GetElementMetadata()->m_name, "aggregatedUIElement"); ++it; if (i == 0) { EXPECT_EQ(it->GetElementMetadata()->m_name, "notAggregatedUIElement"); ++it; } } } }; TEST_F(InstanceDataHierarchyBasicTest, Test) { run(); } TEST_F(InstanceDataHierarchyCopyContainerChangesTest, Test) { run(); } TEST_F(InstanceDataHierarchyEnumContainerTest, Test) { run(); } TEST_F(InstanceDataHierarchyKeyedContainerTest, Test) { run(); } TEST_F(InstanceDataHierarchyKeyedContainerTest, RemovingMultipleItemsFromContainerDoesNotCrash) { using TestMap = AZStd::unordered_map; TestMap testMap; AZStd::initializer_list> valuesToInsert{ {1, 0}, {2, 0}, {3, 0}, {4, 0}, {5, 0}, {6, 0}, {7, 0}, {8, 0}, {9, 0} }; AZ::GenericClassInfo* mapGenericClassInfo = AZ::SerializeGenericTypeInfo::GetGenericInfo(); AZ::SerializeContext::ClassData* mapClassData = mapGenericClassInfo->GetClassData(); ASSERT_NE(nullptr, mapClassData); AZ::SerializeContext::IDataContainer* mapDataContainer = mapClassData->m_container; ASSERT_NE(nullptr, mapDataContainer); auto associativeInterface = mapDataContainer->GetAssociativeContainerInterface(); AZ::SerializeContext::ClassElement classElement; AZ::SerializeContext::DataElement dataElement; dataElement.m_nameCrc = mapDataContainer->GetDefaultElementNameCrc(); EXPECT_TRUE(mapDataContainer->GetElement(classElement, dataElement)); AZStd::vector keyRemovalContainer; keyRemovalContainer.reserve(valuesToInsert.size()); for (const AZStd::pair& valueToInsert : valuesToInsert) { void* newElement = mapDataContainer->ReserveElement(&testMap, &classElement); *reinterpret_cast(newElement) = valueToInsert; mapDataContainer->StoreElement(&testMap, newElement); keyRemovalContainer.push_back(valueToInsert.first); } EXPECT_EQ(valuesToInsert.size(), testMap.size()); for (const AZStd::pair& testValue : valuesToInsert) { // Make sure all elements within initializer_list is in the map void* lookupValue = associativeInterface->GetElementByKey(&testMap, &classElement, &testValue.first); EXPECT_NE(nullptr, lookupValue); } // Shuffle the keys around and attempt to remove the keys using IDataContainer::RemoveElement SerializeContext serializeContext; const uint32_t rngSeed = std::random_device{}(); std::mt19937 mtTwisterRng(rngSeed); std::shuffle(keyRemovalContainer.begin(), keyRemovalContainer.end(), mtTwisterRng); for (double key : keyRemovalContainer) { void* valueToRemove = associativeInterface->GetElementByKey(&testMap, &classElement, &key); EXPECT_TRUE(mapDataContainer->RemoveElement(&testMap, valueToRemove, &serializeContext)); } EXPECT_EQ(0, mapDataContainer->Size(&testMap)); } TEST_F(InstanceDataHierarchyCompareAssociativeContainerTest, TestComparingAssociativeContainers) { run(); } TEST_F(InstanceDataHierarchyElementTest, TestLayingOutUIAndDataElements) { run(); } TEST_F(InstanceDataHierarchyAggregateInstanceTest, TestRespectingAggregateInstanceVisibility) { run(); } } // namespace UnitTest