Files
o3de/Code/Framework/AzToolsFramework/Tests/InstanceDataHierarchy.cpp
T
moraaar 9e0b8c564d Fixed AzToolsFramework tests (#2887)
* Fixed AzToolsFramework unit tests.

Signed-off-by: moraaar <moraaar@amazon.com>

* Include missing header.

Signed-off-by: moraaar <moraaar@amazon.com>

* Using util's class to generate temp directory, instead of qt.

Signed-off-by: moraaar <moraaar@amazon.com>

* Added empty line

Signed-off-by: moraaar <moraaar@amazon.com>

* Fixed warning in MessageTest fixture that CacheProjectRootFolder was not set

Signed-off-by: moraaar <moraaar@amazon.com>

* Additional checks in CreateDefaultEditorEntity helper function.

Signed-off-by: moraaar <moraaar@amazon.com>

* Updated the AzToolsFrameworkTest logic to set the project cache path

The Project Cache Path and Project Path is set through the CommandLine functionality of the ComponentApplication.
This allows those Project Cache Path and Project Path to be set within the Settings Registry during the ComponentApplication constructor

Removed the explicitly calls to delete the temporary directory and fixed the ScopedTemporaryDirectory class to recursively delete the temporary directory

Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>

* Setup correctly @assets@ alias for PlatformAddressedAssetCatalogManagerTest and AssetSeedManagerTest fixtures.

- These 2 test fixtures need to manually set the @asset@ alias to not include the platform at the end (which it does by default), because they are looping over platforms in their setup.
- Also initializing pointers to nullptr, so if setup fail in the future the teardown doesn't crash trying to delete garbage.

Signed-off-by: moraaar <moraaar@amazon.com>

Co-authored-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>
2021-08-06 11:07:15 -05:00

1571 lines
65 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/Component/Entity.h>
#include <AzCore/Component/Component.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/containers/stack.h>
#include <AzCore/std/containers/map.h>
#include <AzCore/std/containers/unordered_map.h>
#include <AzCore/std/containers/set.h>
#include <AzCore/std/containers/unordered_set.h>
#include <AzToolsFramework/UI/PropertyEditor/InstanceDataHierarchy.h>
#include <AzToolsFramework/UI/PropertyEditor/PropertyEditorAPI.h>
#include <AzCore/Serialization/ObjectStream.h>
#include <AzCore/Serialization/Utils.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <random>
#include <QDebug>
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<TestComponent*>(classPtr);
component->m_serializeOnReadBegin++;
}
void OnReadEnd(void* classPtr) override
{
TestComponent* component = reinterpret_cast<TestComponent*>(classPtr);
component->m_serializeOnReadEnd++;
}
void OnWriteBegin(void* classPtr) override
{
TestComponent* component = reinterpret_cast<TestComponent*>(classPtr);
component->m_serializeOnWriteBegin++;
}
void OnWriteEnd(void* classPtr) override
{
TestComponent* component = reinterpret_cast<TestComponent*>(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<AZ::SerializeContext*>(context))
{
serializeContext->Class<SubData>()
->Version(1)
->Field("Int", &SubData::m_int)
;
serializeContext->Class<TestComponent, AZ::Component>()
->EventHandler<SerializationEvents>()
->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<TestComponent>("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<SubData>("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<SubData> m_normalContainer;
AZStd::vector<SubData*> 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<AZ::Entity> testEntity1(new AZ::Entity());
testEntity1->CreateComponent<TestComponent>();
AZStd::unique_ptr<AZ::Entity> testEntity2(serializeContext.CloneObject(testEntity1.get()));
AZ_TEST_ASSERT(testEntity1->FindComponent<TestComponent>()->m_serializeOnReadBegin == 1);
AZ_TEST_ASSERT(testEntity1->FindComponent<TestComponent>()->m_serializeOnReadEnd == 1);
AZ_TEST_ASSERT(testEntity2->FindComponent<TestComponent>()->m_serializeOnWriteBegin == 1);
AZ_TEST_ASSERT(testEntity2->FindComponent<TestComponent>()->m_serializeOnWriteEnd == 1);
testEntity1->FindComponent<TestComponent>()->m_float = 1.f;
testEntity1->FindComponent<TestComponent>()->m_normalContainer.push_back(TestComponent::SubData(1));
testEntity1->FindComponent<TestComponent>()->m_normalContainer.push_back(TestComponent::SubData(2));
testEntity1->FindComponent<TestComponent>()->m_pointerContainer.push_back(aznew TestComponent::SubData(1));
testEntity1->FindComponent<TestComponent>()->m_pointerContainer.push_back(aznew TestComponent::SubData(2));
// First entity has more entries, so we'll be adding elements to testEntity2->
testEntity2->FindComponent<TestComponent>()->m_float = 2.f;
testEntity2->FindComponent<TestComponent>()->m_normalContainer.push_back(TestComponent::SubData(1));
testEntity2->FindComponent<TestComponent>()->m_pointerContainer.push_back(aznew TestComponent::SubData(1));
InstanceDataHierarchy idh1;
idh1.AddRootInstance(testEntity1.get());
idh1.Build(&serializeContext, 0);
AZ_TEST_ASSERT(testEntity1->FindComponent<TestComponent>()->m_serializeOnReadBegin == 2);
AZ_TEST_ASSERT(testEntity1->FindComponent<TestComponent>()->m_serializeOnReadEnd == 2);
InstanceDataHierarchy idh2;
idh2.AddRootInstance(testEntity2.get());
idh2.Build(&serializeContext, 0);
AZ_TEST_ASSERT(testEntity2->FindComponent<TestComponent>()->m_serializeOnReadBegin == 1);
AZ_TEST_ASSERT(testEntity2->FindComponent<TestComponent>()->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<InstanceDataNode*> nodeStack;
nodeStack.push(root1);
InstanceDataNode* componentNode1 = nullptr;
while (!nodeStack.empty())
{
InstanceDataNode* node = nodeStack.top();
nodeStack.pop();
if (node->GetClassMetadata()->m_typeId == AZ::AzTypeInfo<TestComponent>::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<TestComponent>()->m_serializeOnReadBegin == 2);
AZ_TEST_ASSERT(testEntity1->FindComponent<TestComponent>()->m_serializeOnReadEnd == 2);
AZ_TEST_ASSERT(testEntity2->FindComponent<TestComponent>()->m_serializeOnWriteBegin == 2);
AZ_TEST_ASSERT(testEntity2->FindComponent<TestComponent>()->m_serializeOnWriteEnd == 2);
AZ_TEST_ASSERT(testEntity2->FindComponent<TestComponent>()->m_normalContainer.size() == 2);
AZ_TEST_ASSERT(testEntity2->FindComponent<TestComponent>()->m_pointerContainer.size() == 2);
AZ_TEST_ASSERT(testEntity2->FindComponent<TestComponent>()->m_float == 1.f);
}
// Test removal of container elements during instance data copying.
{
AZStd::unique_ptr<AZ::Entity> testEntity1(new AZ::Entity());
testEntity1->CreateComponent<TestComponent>();
AZStd::unique_ptr<AZ::Entity> testEntity2(serializeContext.CloneObject(testEntity1.get()));
// First entity has more in container 1, fewer in container 2 as compared to second entity.
testEntity1->FindComponent<TestComponent>()->m_normalContainer.push_back(TestComponent::SubData(1));
testEntity1->FindComponent<TestComponent>()->m_normalContainer.push_back(TestComponent::SubData(2));
testEntity1->FindComponent<TestComponent>()->m_pointerContainer.push_back(aznew TestComponent::SubData(1));
testEntity2->FindComponent<TestComponent>()->m_normalContainer.push_back(TestComponent::SubData(1));
testEntity2->FindComponent<TestComponent>()->m_pointerContainer.push_back(aznew TestComponent::SubData(1));
testEntity2->FindComponent<TestComponent>()->m_pointerContainer.push_back(aznew TestComponent::SubData(2));
// Change a field.
testEntity2->FindComponent<TestComponent>()->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<InstanceDataNode*> nodeStack;
nodeStack.push(root1);
InstanceDataNode* componentNode1 = nullptr;
while (!nodeStack.empty())
{
InstanceDataNode* node = nodeStack.top();
nodeStack.pop();
if (node->GetClassMetadata()->m_typeId == AZ::AzTypeInfo<TestComponent>::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<AZ::u8>& 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<AZ::u8>& sourceData, AZStd::vector<AZ::u8>& 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<TestComponent>()->m_normalContainer.size() == 2);
AZ_TEST_ASSERT(testEntity2->FindComponent<TestComponent>()->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<TestComponent::SubData> testNormalContainer{ TestComponent::SubData(1), TestComponent::SubData(2), TestComponent::SubData(3) };
// create a test component with some initial values
AZStd::unique_ptr<TestComponent> 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<TestComponent::SubData> 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<TestComponent::SubData> 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<TestComponent::SubData> 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<StructInner>()->
PersistentId([](const void* instance) -> u64 { return static_cast<u64>(reinterpret_cast<const StructInner*>(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<StructInner> m_vector;
StructOuter()
{
}
static void Reflect(AZ::SerializeContext& context)
{
context.Class<StructOuter>()->
Field("Vector", &StructOuter::m_vector)
;
}
};
void DoCopy(StructOuter& source, StructOuter& target, AZ::SerializeContext& ctx)
{
AzToolsFramework::InstanceDataHierarchy sourceHier;
sourceHier.AddRootInstance(&source, AZ::AzTypeInfo<StructOuter>::Uuid());
sourceHier.Build(&ctx, AZ::SerializeContext::ENUM_ACCESS_FOR_READ);
AzToolsFramework::InstanceDataHierarchy targetHier;
targetHier.AddRootInstance(&target, AZ::AzTypeInfo<StructOuter>::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<TestEnum> m_enumVector;
static void Reflect(AZ::SerializeContext& context)
{
context.Class<EnumContainer>()
->Field("Enum", &EnumContainer::m_enum)
->Field("EnumVector", &EnumContainer::m_enumVector)
;
if (EditContext* edit = context.GetEditContext())
{
edit->Enum<UnitTest::TestEnum>("TestEnum", "No Description")
->Value("Value1", UnitTest::TestEnum::Value1)
->Value("Value2", UnitTest::TestEnum::Value2)
->Value("Value3", UnitTest::TestEnum::Value3)
;
edit->Class<EnumContainer>("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<EnumContainer>());
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<AttributeData<AZ::TypeId>*>(attribute);
if (attributeData)
{
id = attributeData->Get(&ec);
}
return id;
};
EXPECT_EQ(getEnumData(*enumNode), RttiTypeId<UnitTest::TestEnum>());
const auto& vectorEntries = enumVectorNode->GetChildren();
ASSERT_EQ(vectorEntries.size(), 1);
EXPECT_EQ(getEnumData(*vectorEntries.begin()), RttiTypeId<UnitTest::TestEnum>());
}
};
class GroupTestComponent : public AZ::Component
{
public:
AZ_COMPONENT(GroupTestComponent, "{C088C81D-D59D-43F1-85F8-B2E591BABA36}")
GroupTestComponent() = default;
struct SubData
{
AZ_TYPE_INFO(SubData, "{983316B5-17C0-476E-9CEB-CA749B3ABE5D}");
AZ_CLASS_ALLOCATOR(SubData, AZ::SystemAllocator, 0);
SubData() {}
explicit SubData(int v) : m_int(v) {}
explicit SubData(bool b) : m_bool(b) {}
explicit SubData(float f) : m_float(f) {}
~SubData() = default;
float m_float = 0.f;
int m_int = 0;
bool m_bool = true;
};
static void Reflect(AZ::ReflectContext* context)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<SubData>()
->Version(1)
->Field("SubInt", &SubData::m_int)
->Field("SubToggle", &SubData::m_bool)
->Field("SubFloat", &SubData::m_float)
;
serializeContext->Class<GroupTestComponent, AZ::Component>()
->Version(1)
->Field("Float", &GroupTestComponent::m_float)
->Field("GroupToggle", &GroupTestComponent::m_groupToggle)
->Field("GroupFloat", &GroupTestComponent::m_groupFloat)
->Field("ToggleGroupInt", &GroupTestComponent::m_toggleGroupInt)
->Field("SubDataNormal", &GroupTestComponent::m_subGroupForNormal)
->Field("SubDataToggle", &GroupTestComponent::m_subGroupForToggle)
;
if (AZ::EditContext* edit = serializeContext->GetEditContext())
{
edit->Class<GroupTestComponent>("Group Test Component", "Testing normal groups and toggle groups")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->DataElement(0, &GroupTestComponent::m_float, "Float Field", "A float field")
->ClassElement(AZ::Edit::ClassElements::Group, "Normal Group")
->DataElement(0, &GroupTestComponent::m_groupFloat, "Float Field", "A float field")
->DataElement(0, &GroupTestComponent::m_subGroupForNormal, "Struct Field", "A sub data type")
->GroupElementToggle("Group Toggle", &GroupTestComponent::m_groupToggle)
->DataElement(0, &GroupTestComponent::m_toggleGroupInt, "Normal Integer", "An Integer")
->DataElement(0, &GroupTestComponent::m_subGroupForToggle, "Struct Field", "A sub data type")
;
edit->Class<SubData>("SubGroup Test Component", "Testing nested normal groups and toggle groups")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->ClassElement(AZ::Edit::ClassElements::Group, "Normal SubGroup")
->DataElement(0, &SubData::m_int, "SubGroup Int Field", "An int")
->GroupElementToggle("SubGroup Toggle", &SubData::m_bool)
->DataElement(0, &SubData::m_float, "SubGroup Float Field", "An int")
;
}
}
}
void Activate() override
{
}
void Deactivate() override
{
}
float m_float = 0.f;
float m_groupFloat = 0.f;
int m_toggleGroupInt = 0;
AZStd::string m_string;
bool m_groupToggle = false;
SubData m_subGroupForNormal;
SubData m_subGroupForToggle;
};
class InstanceDataHierarchyGroupTestFixture : public AllocatorsFixture
{
public:
InstanceDataHierarchyGroupTestFixture() = default;
AZStd::unique_ptr<SerializeContext> m_serializeContext;
AZStd::unique_ptr<AZ::Entity> testEntity1;
AzToolsFramework::InstanceDataHierarchy* instanceDataHierarchy;
AzToolsFramework::InstanceDataNode* componentNode1 = nullptr;
void SetUp() override
{
AllocatorsFixture::SetUp();
using AzToolsFramework::InstanceDataHierarchy;
using AzToolsFramework::InstanceDataNode;
AZ::AllocatorInstance<AZ::PoolAllocator>::Create();
m_serializeContext.reset(aznew AZ::SerializeContext());
m_serializeContext.get()->CreateEditContext();
Entity::Reflect(m_serializeContext.get());
GroupTestComponent::Reflect(m_serializeContext.get());
testEntity1.reset(new AZ::Entity());
testEntity1->CreateComponent<GroupTestComponent>();
instanceDataHierarchy = aznew InstanceDataHierarchy();
instanceDataHierarchy->AddRootInstance(testEntity1.get());
instanceDataHierarchy->Build(m_serializeContext.get(), 0);
// Adding the nodes to a node stack
auto rootNode = instanceDataHierarchy->GetRootNode();
AZStd::stack<InstanceDataNode*> nodeStack;
nodeStack.push(rootNode);
while (!nodeStack.empty())
{
InstanceDataNode* node = nodeStack.top();
nodeStack.pop();
if (node->GetClassMetadata()->m_typeId == AZ::AzTypeInfo<GroupTestComponent>::Uuid())
{
componentNode1 = node;
break;
}
for (InstanceDataNode& child : node->GetChildren())
{
nodeStack.push(&child);
}
}
}
void TearDown() override
{
m_serializeContext.reset();
testEntity1.reset();
delete instanceDataHierarchy;
AZ::AllocatorInstance<AZ::PoolAllocator>::Destroy();
AllocatorsFixture::TearDown();
}
};
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<AZStd::string, float> m_map;
AZStd::unordered_map<AZStd::pair<int, double>, int> m_unorderedMap;
AZStd::set<int> m_set;
AZStd::unordered_set<AZ::u64> m_unorderedSet;
AZStd::unordered_multimap<int, AZStd::string> m_multiMap;
AZStd::unordered_map<int, AZStd::unordered_map<int, int>> m_nestedMap;
AZStd::map<CustomKeyWithoutStringRepresentation, int> m_uncollapsableMap;
AZStd::map<CustomKeyWithStringRepresentation, int> m_collapsableMap;
static void Reflect(AZ::SerializeContext& context)
{
context.Class<CustomKeyWithoutStringRepresentation>()
->Field("value", &CustomKeyWithoutStringRepresentation::m_value);
context.Class<CustomKeyWithStringRepresentation>()
->Field("value", &CustomKeyWithStringRepresentation::m_value);
context.Class<KeyedContainer>()
->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>("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 <class T>
struct TypedKeyTestData : public KeyTestData
{
AZStd::vector<T> keysToInsert;
TypedKeyTestData(std::initializer_list<T> keys)
: keysToInsert(keys)
{
}
void InsertAndVerifyKeys(AZ::SerializeContext::IDataContainer* container, void* key, void* instance, const AZ::SerializeContext::ClassElement* classElement) const
{
T* keyContainer = reinterpret_cast<T*>(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<AZ::Internal::RValueToLValueWrapper<T>>();
}
size_t NumberOfKeys() const override
{
return keysToInsert.size();
}
static AZStd::unique_ptr<TypedKeyTestData<T>> Create(std::initializer_list<T> keys)
{
return AZStd::make_unique<TypedKeyTestData<T>>(keys);
}
};
void run()
{
using namespace AzToolsFramework;
AZ::SerializeContext serializeContext;
serializeContext.CreateEditContext();
KeyedContainer::Reflect(serializeContext);
KeyedContainer kc;
InstanceDataHierarchy idh;
idh.AddRootInstance(&kc, azrtti_typeid<KeyedContainer>());
idh.Build(&serializeContext, 0);
AZStd::unordered_map<AZ::u32, AZStd::unique_ptr<KeyTestData>> keyTestData;
keyTestData[AZ_CRC("map")] = TypedKeyTestData<AZStd::string>::Create({"A", "B", "lorem ipsum"});
keyTestData[AZ_CRC("unorderedMap")] = TypedKeyTestData<AZStd::pair<int, double>>::Create({ {5, 1.0}, {5, -2.0} });
keyTestData[AZ_CRC("set")] = TypedKeyTestData<int>::Create({2, 4, -255, 999});
keyTestData[AZ_CRC("unorderedSet")] = TypedKeyTestData<AZ::u64>::Create({500000, 9, 0, 42, 42});
keyTestData[AZ_CRC("multiMap")] = TypedKeyTestData<int>::Create({-1, 2, -3, 4, -5, 6});
keyTestData[AZ_CRC("nestedMap")] = TypedKeyTestData<int>::Create({1, 10, 100, 1000});
keyTestData[AZ_CRC("uncollapsableMap")] = TypedKeyTestData<CustomKeyWithoutStringRepresentation>::Create({{0}, {1}});
keyTestData[AZ_CRC("collapsableMap")] = TypedKeyTestData<CustomKeyWithStringRepresentation>::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<AttributeData<AZ::TypeId>*>(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<int>::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<CustomKeyWithoutStringRepresentation>"));
EXPECT_EQ(AZ::Crc32(valueNode.GetElementEditMetadata()->m_name), AZ_CRC("Value<int>"));
}
}
}
// 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<AZStd::string, int> m_map;
static void Reflect(AZ::SerializeContext& context)
{
context.Class<Container>()
->Field("map", &Container::m_map);
}
};
void run()
{
using namespace AzToolsFramework;
AZ::AllocatorInstance<AZ::PoolAllocator>::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<AZStd::string> expectedAdds,
AZStd::unordered_set<AZStd::string> expectedRemoves,
AZStd::unordered_set<AZStd::string> expectedChanges)
{
InstanceDataHierarchy idhBase;
idhBase.AddRootInstance(&baseInstance, azrtti_typeid<Container>());
idhBase.Build(&serializeContext, 0);
InstanceDataHierarchy idhCompare;
idhCompare.AddRootInstance(&compareInstance, azrtti_typeid<Container>());
idhCompare.Build(&serializeContext, 0);
AZStd::unordered_set<AZStd::string> actualAdds;
AZStd::unordered_set<AZStd::string> actualRemoves;
AZStd::unordered_set<AZStd::string> actualChanges;
auto newNodeCB = [&](InstanceDataNode* newNode, AZStd::vector<AZ::u8>&)
{
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<AZ::u8>&, AZStd::vector<AZ::u8>&)
{
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<AZ::PoolAllocator>::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<UIElementContainer>()
->Field("data", &UIElementContainer::m_data);
if (auto editContext = context.GetEditContext())
{
editContext->Class<UIElementContainer>("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<UIElementContainer>());
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_STREQ(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_STREQ(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<AggregatedContainer>()
->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<AggregatedContainer>("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<AggregatedContainer> containers;
for (int i = 0; i < 5; ++i)
{
containers.push_back();
AggregatedContainer& container = containers.back();
idh.AddRootInstance(&container, azrtti_typeid<AggregatedContainer>());
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_STREQ(it->GetElementMetadata()->m_name, "aggregatedDataElement");
++it;
if (i == 0)
{
EXPECT_STREQ(it->GetElementMetadata()->m_name, "notAggregatedDataElement");
++it;
}
EXPECT_STREQ(it->GetElementMetadata()->m_name, "aggregatedUIElement");
++it;
if (i == 0)
{
EXPECT_STREQ(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<double, double>;
TestMap testMap;
AZStd::initializer_list<AZStd::pair<double, double>> valuesToInsert{ {1, 0}, {2, 0}, {3, 0}, {4, 0}, {5, 0}, {6, 0}, {7, 0}, {8, 0}, {9, 0} };
AZ::GenericClassInfo* mapGenericClassInfo = AZ::SerializeGenericTypeInfo<TestMap>::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<double> keyRemovalContainer;
keyRemovalContainer.reserve(valuesToInsert.size());
for (const AZStd::pair<double, double>& valueToInsert : valuesToInsert)
{
void* newElement = mapDataContainer->ReserveElement(&testMap, &classElement);
*reinterpret_cast<typename TestMap::value_type*>(newElement) = valueToInsert;
mapDataContainer->StoreElement(&testMap, newElement);
keyRemovalContainer.push_back(valueToInsert.first);
}
EXPECT_EQ(valuesToInsert.size(), testMap.size());
for (const AZStd::pair<double, double>& 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();
}
// Test to validate that the only ClassElement::Group nodes are ToggleGroups
TEST_F(InstanceDataHierarchyGroupTestFixture, GroupToggleIsClassElementGroup)
{
using AzToolsFramework::InstanceDataHierarchy;
using AzToolsFramework::InstanceDataNode;
for (auto child : componentNode1->GetChildren())
{
AZStd::string childName(child.GetElementMetadata()->m_name);
if (childName.compare("GroupToggle") == 0)
{
EXPECT_EQ(child.GetElementEditMetadata()->m_elementId, AZ::Edit::ClassElements::Group);
}
if ((childName.compare("SubDataNormal") == 0) || (childName.compare("SubDataToggle") == 0))
{
for (auto subChild : child.GetChildren())
{
childName = subChild.GetElementMetadata()->m_name;
if (childName.compare("SubToggle") == 0)
{
EXPECT_EQ(subChild.GetElementEditMetadata()->m_elementId, AZ::Edit::ClassElements::Group);
}
else
{
EXPECT_NE(subChild.GetElementEditMetadata()->m_elementId, AZ::Edit::ClassElements::Group);
}
}
}
}
}
// Test to ensure that each node has been assigned under the proper group and the group hierarchy is structured correctly
TEST_F(InstanceDataHierarchyGroupTestFixture, ValidatingGroupAndSubGroupHierarchy)
{
using AzToolsFramework::InstanceDataHierarchy;
using AzToolsFramework::InstanceDataNode;
for (auto child : componentNode1->GetChildren())
{
AZStd::string childName(child.GetElementMetadata()->m_name);
if (childName.compare("GroupFloat") == 0)
{
EXPECT_STREQ(child.GetGroupElementMetadata()->m_description, "Normal Group");
}
if (childName.compare("ToggleGroupInt") == 0)
{
EXPECT_STREQ(child.GetGroupElementMetadata()->m_description, "Group Toggle");
}
if ((childName.compare("SubDataNormal") == 0) || (childName.compare("SubDataToggle") == 0))
{
for (auto subChild : child.GetChildren())
{
childName = subChild.GetElementMetadata()->m_name;
if (childName.compare("SubInt") == 0)
{
EXPECT_STREQ(subChild.GetGroupElementMetadata()->m_description, "Normal SubGroup");
}
if (childName.compare("SubFloat") == 0)
{
EXPECT_STREQ(subChild.GetGroupElementMetadata()->m_description, "SubGroup Toggle");
}
}
}
}
}
class InstanceDataHierarchyGroupTestFixtureParameterized
: public InstanceDataHierarchyGroupTestFixture
, public ::testing::WithParamInterface<const char*>
{
};
INSTANTIATE_TEST_CASE_P(
InstanceDataHierarchyGroupTestFixture,
InstanceDataHierarchyGroupTestFixtureParameterized,
::testing::Values("GroupFloat", "GroupToggle", "ToggleGroupInt", "SubInt", "SubToggle", "SubFloat"));
// Test to validate that each node in a group and Subgroup has the correct parent
TEST_P(InstanceDataHierarchyGroupTestFixtureParameterized, ValidatingGroupAndSubGroupParents)
{
using AzToolsFramework::InstanceDataHierarchy;
using AzToolsFramework::InstanceDataNode;
const char* paramName = GetParam();
for (auto child : componentNode1->GetChildren())
{
AZStd::string childName(child.GetElementMetadata()->m_name);
if (childName.compare(paramName) == 0)
{
EXPECT_STREQ(child.GetParent()->GetClassMetadata()->m_name, "GroupTestComponent");
}
if ((childName.compare("SubDataNormal") == 0) || (childName.compare("SubDataToggle") == 0))
{
for (auto subChild : child.GetChildren())
{
childName = subChild.GetElementMetadata()->m_name;
if (childName.compare(paramName) == 0)
{
EXPECT_STREQ(subChild.GetParent()->GetClassMetadata()->m_name, "SubData");
}
}
}
}
}
} // namespace UnitTest