You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Code/Framework/AzToolsFramework/Tests/SliceStabilityTests/SliceStabilityTestFramework...

720 lines
33 KiB
C++

/*
* 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 <Tests/SliceStabilityTests/SliceStabilityTestFramework.h>
#include <AzCore/Serialization/Utils.h>
#include <AzCore/Asset/AssetManager.h>
#include <AzCore/Slice/SliceAsset.h>
#include <AzCore/UserSettings/UserSettingsComponent.h>
#include <AzFramework/Asset/AssetCatalogBus.h>
#include <AzToolsFramework/ToolsComponents/TransformComponent.h>
#include <AzToolsFramework/Slice/SliceUtilities.h>
#include <AzToolsFramework/Asset/AssetSystemComponent.h>
#include <AzToolsFramework/Entity/EditorEntityContextComponent.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
#include <AzToolsFramework/Entity/EditorEntitySortComponent.h>
namespace UnitTest
{
void SliceStabilityTest::SetUpEditorFixtureImpl()
{
auto* app = GetApplication();
ASSERT_TRUE(app);
// Get the serialize context to reflect our types and set our validator's serialize context
AZ::SerializeContext* serializeContext = app->GetSerializeContext();
m_validator.SetSerializeContext(serializeContext);
app->RegisterComponentDescriptor(EntityReferenceComponent::CreateDescriptor());
// Grab the system entity from the component application
AZ::Entity* systemEntity = app->FindEntity(AZ::SystemEntityId);
// Deactivate the AssetSystemComponent
// We will be implementing the AssetSystemRequestBus and want to avoid Ebus connection conflicts
AzToolsFramework::AssetSystem::AssetSystemComponent* assetSystemComponent = systemEntity->FindComponent<AzToolsFramework::AssetSystem::AssetSystemComponent>();
assetSystemComponent->Deactivate();
AzToolsFramework::AssetSystemRequestBus::Handler::BusConnect();
AzToolsFramework::EditorRequestBus::Handler::BusConnect();
AzToolsFramework::SliceEditorEntityOwnershipServiceNotificationBus::Handler::BusConnect();
AZ::UserSettingsComponentRequestBus::Broadcast(&AZ::UserSettingsComponentRequests::DisableSaveOnFinalize);
// Cache the existing file io instance and build our mock file io
m_priorFileIO = AZ::IO::FileIOBase::GetInstance();
m_fileIOMock = AZStd::make_unique<testing::NiceMock<AZ::IO::MockFileIOBase>>();
// Swap out current file io instance for our mock
AZ::IO::FileIOBase::SetInstance(nullptr);
AZ::IO::FileIOBase::SetInstance(m_fileIOMock.get());
// Setup the default returns for our mock file io calls
AZ::IO::MockFileIOBase::InstallDefaultReturns(*m_fileIOMock.get());
// For write we set the default of the 4th param (bytesWritten) to 1
// otherwise slice transaction errors out during the mock write for writing the default 0 bytes
ON_CALL(*m_fileIOMock.get(), Write(testing::_, testing::_, testing::_, testing::_))
.WillByDefault(
testing::DoAll(
testing::SetArgPointee<3>(1),
testing::Return(AZ::IO::Result(AZ::IO::ResultCode::Success))));
ON_CALL(*m_fileIOMock.get(), GetAlias(testing::_))
.WillByDefault(
testing::Return(""));
ON_CALL(*m_fileIOMock.get(), Rename(testing::_, testing::_))
.WillByDefault(
testing::Return(AZ::IO::Result(AZ::IO::ResultCode::Success)));
}
void SliceStabilityTest::TearDownEditorFixtureImpl()
{
// Get the system entity from the component application
AZ::Entity* systemEntity = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(systemEntity, &AZ::ComponentApplicationBus::Events::FindEntity, AZ::SystemEntityId);
// Deactivate the EditorEntityContextComponent
// This triggers the entity context to destroy its root slice asset which destroys all entities, slice instances, and meta data entities
AzToolsFramework::EditorEntityContextComponent* editorEntityContext = systemEntity->FindComponent<AzToolsFramework::EditorEntityContextComponent>();
editorEntityContext->Deactivate();
// Restore our original file io instance
AZ::IO::FileIOBase::SetInstance(nullptr);
AZ::IO::FileIOBase::SetInstance(m_priorFileIO);
AzToolsFramework::EditorRequestBus::Handler::BusDisconnect();
AzToolsFramework::AssetSystemRequestBus::Handler::BusDisconnect();
AzToolsFramework::SliceEditorEntityOwnershipServiceNotificationBus::Handler::BusDisconnect();
}
AZ::EntityId SliceStabilityTest::CreateEditorEntity(const char* entityName, AzToolsFramework::EntityIdList& entityList, const AZ::EntityId& parentId /*= AZ::EntityId()*/)
{
// Start by creating and registering a new loose entity with the editor entity context
// This call also adds required components onto the entity
AZ::EntityId newEntityId;
AzToolsFramework::EditorEntityContextRequestBus::BroadcastResult(
newEntityId, &AzToolsFramework::EditorEntityContextRequestBus::Events::CreateNewEditorEntity, entityName);
AZ::Entity* newEntity = AzToolsFramework::GetEntityById(newEntityId);
// If newEntity is nullptr still then there was a failure in the above EBus call and we cannot proceed
if (!newEntity)
{
return AZ::EntityId();
}
// Add to our entities container
entityList.emplace_back(newEntity->GetId());
// Get the new entity's transform component
AzToolsFramework::Components::TransformComponent* entityTransform =
newEntity->FindComponent<AzToolsFramework::Components::TransformComponent>();
// If new entity has no Transform component then there was a failure in the create entity call
// and the application of required components
if (!entityTransform)
{
AzToolsFramework::EditorEntityContextRequestBus::Broadcast(&AzToolsFramework::EditorEntityContextRequestBus::Events::DestroyEditorEntity, newEntity->GetId());
return AZ::EntityId();
}
// If supplied set the parent of the new entity
if (parentId.IsValid())
{
entityTransform->SetParent(parentId);
}
// Set the new entity's transform to non zero values
// This helps validate in comparison tests that the transform values of created entities persist during slice operations
entityTransform->SetLocalUniformScale(5);
entityTransform->SetLocalRotation(AZ::Vector3RadToDeg(AZ::Vector3(90, 90, 90)));
entityTransform->SetLocalTranslation(AZ::Vector3(100, 100, 100));
return entityList.back();
}
AZ::Data::AssetId SliceStabilityTest::CreateSlice(AZStd::string sliceAssetName, AzToolsFramework::EntityIdList entityList, AZ::SliceComponent::SliceInstanceAddress& sliceAddress)
{
// Fabricate a new asset id for this slice and set its sub id to the SliceAsset sub id
m_newSliceId = AZ::Uuid::CreateRandom();
m_newSliceId.m_subId = AZ::SliceAsset::GetAssetSubId();
// Init the sliceAddress to invalid
sliceAddress = AZ::SliceComponent::SliceInstanceAddress();
// The relative slice asset path will be used in registering the slice with the asset catalog
// It will show up in debugging and is useful for tracking multiple slice assets in a test
// Since we are mocking file io m_relativeSourceAssetRoot is purely cosmetic
AZStd::string relativeSliceAssetPath = m_relativeSourceAssetRoot + sliceAssetName;
// Call MakeNewSlice and deactivate all prompts for user input
// Since MakeNewSlice is tightly joined to QT dialogs and popups we default all decisions and silence all popups so we can run tests without user input
// inheritSlices: whether to inherit slice ancestry of added instance entities or make a new slice with no ancestry
// setAsDynamic: whether to mark the slice asset as dynamic
// acceptDefaultPath: whether to prompt the user for a path save location or to proceed with the generated one
// defaultMoveExternalRefs: whether to prompt the user on if external entity references found in added entities get added to the created slice or do this automatically
// defaultGenerateSharedRoot: whether to generate a shared root if one or more added entities do not share the same root
// silenceWarningPopups: disables QT warning popups from being generated, we can still rely on the return of MakeNewSlice for error handling
bool sliceCreateSuccess = AzToolsFramework::SliceUtilities::MakeNewSlice(AzToolsFramework::EntityIdSet(entityList.begin(), entityList.end()),
relativeSliceAssetPath.c_str(),
true /*inheritSlices*/,
false /*setAsDynamic*/,
true /*acceptDefaultPath*/,
true /*defaultMoveExternalRefs*/,
true /*defaultGenerateSharedRoot*/,
true /*silenceWarningPopups*/);
if (sliceCreateSuccess)
{
// Setup the mock asset info for our new slice
AZ::Data::AssetInfo newSliceInfo;
newSliceInfo.m_assetId = m_newSliceId;
newSliceInfo.m_relativePath = relativeSliceAssetPath;
newSliceInfo.m_assetType = azrtti_typeid<AZ::SliceAsset>();
newSliceInfo.m_sizeBytes = 1;
// Register the asset with the asset catalog
// This mocks the asset load pipeline that triggers the OnCatalogAssetAdded event
// OnCatalogAssetAdded triggers the final steps of the create slice flow by building the first slice instance out of the added entities
AZ::Data::AssetCatalogRequestBus::Broadcast(&AZ::Data::AssetCatalogRequestBus::Events::RegisterAsset, m_newSliceId, newSliceInfo);
}
else
{
return AZ::Uuid::CreateNull();
}
// Acquire the slice instance address the added entities were promoted into
AzFramework::SliceEntityRequestBus::EventResult(sliceAddress, *entityList.begin(),
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
// Validate the slice instance
if (!sliceAddress.IsValid())
{
return AZ::Uuid::CreateNull();
}
// Validate the new slice asset id matches our generated asset id
AZ::Data::AssetId createdSliceId = sliceAddress.GetReference()->GetSliceAsset().GetId();
if (m_newSliceId != createdSliceId)
{
// Return invalid id as error
createdSliceId = AZ::Uuid::CreateNull();
}
// Reset our newSliceId so it's invalid for any OnSliceInstantiated calls
m_newSliceId = AZ::Uuid::CreateNull();
return createdSliceId;
}
bool SliceStabilityTest::PushEntitiesToSlice(AZ::SliceComponent::SliceInstanceAddress& sliceInstanceAddress, const AzToolsFramework::EntityIdList& entitiesToPush)
{
// Nothing to push
if (entitiesToPush.empty())
{
return true;
}
// Cannot push to an invalid slice
if (!sliceInstanceAddress.IsValid())
{
return false;
}
// Copy the slice instance id
// The internal instance of the slicecomponent we push to will be destroyed
// We will use this id to validate that the new instance maps to the same id after the push
AZ::SliceComponent::SliceInstance* sliceInstance = sliceInstanceAddress.GetInstance();
AZ::SliceComponent::SliceInstanceId sliceInstanceId = sliceInstance->GetId();
// Get the currently instantiated entities in this slice instance
const AZ::SliceComponent::EntityList& sliceInstanceInstantiatedEntities = sliceInstance->GetInstantiated() ? sliceInstance->GetInstantiated()->m_entities : AZ::SliceComponent::EntityList();
// Acquire the slice instance's asset and start the push slice transaction
const AZ::Data::Asset<AZ::SliceAsset> sliceAsset = sliceInstanceAddress.GetReference()->GetSliceAsset();
AzToolsFramework::SliceUtilities::SliceTransaction::TransactionPtr transaction = AzToolsFramework::SliceUtilities::SliceTransaction::BeginSlicePush(sliceAsset);
// Since a slice push causes the current instance to re-instantiate all added entities will be remade in the new instance
// We will be deleting the existing entities being added as they will be replaced in this manner
AzToolsFramework::EntityIdList entitiesToRemove;
for (const AZ::EntityId& entityToPush : entitiesToPush)
{
AzToolsFramework::SliceUtilities::SliceTransaction::Result result;
// If the entity already exists in the slice then we will update it
if (FindEntityInList(entityToPush, sliceInstanceInstantiatedEntities))
{
result = transaction->UpdateEntity(entityToPush);
}
else
{
// Otherwise we add it to the slice transaction
// and mark the entity for delete since it will be replaced
result = transaction->AddEntity(entityToPush);
entitiesToRemove.emplace_back(entityToPush);
}
if (!result.IsSuccess())
{
return false;
}
}
// This asset mocks the reloaded temp asset that would trigger the ReloadAssetFromData call after a slice push
AZ::Data::Asset<AZ::SliceAsset> slicePushResultClone;
AzToolsFramework::SliceUtilities::SliceTransaction::PostSaveCallback postSaveCallback =
[&sliceAsset, &slicePushResultClone](AzToolsFramework::SliceUtilities::SliceTransaction::TransactionPtr transaction, const char* fullSourcePath, const AzToolsFramework::SliceUtilities::SliceTransaction::SliceAssetPtr& asset) -> void
{
// SlicePostPushCallback updates the slice component that owns our instance's reference (usually the root slice component of the entity context)
// the update is to make a mapping of the existing entity id (about to be deleted) with the asset entity id (about to be instantiated and replace the existing)
// this sets the replacement entity back to its original id so that external references to that entity do not break by it not having the same id
AzToolsFramework::SliceUtilities::SlicePostPushCallback(transaction, fullSourcePath, asset);
// Clone our slice asset so that our temp has the same asset id
slicePushResultClone = { sliceAsset.Get()->Clone(), AZ::Data::AssetLoadBehavior::Default };
// Move the transaction's asset data into our temp
// the transaction's asset data is what would be saved to disk and reloaded into our temp
slicePushResultClone.Get()->SetData(asset.Get()->GetEntity(), asset.Get()->GetComponent());
asset.Get()->SetData(nullptr, nullptr, false);
};
// Commit our queued entity adds and updates to be pushed to our slice asset and set our pre and post commit callbacks
const AzToolsFramework::SliceUtilities::SliceTransaction::Result result = transaction->Commit(
"NotAValidAssetPath",
AzToolsFramework::SliceUtilities::SlicePreSaveCallbackForWorldEntities,
postSaveCallback);
if (!result.IsSuccess())
{
return false;
}
// Send the reload event that will trigger the owning slice component to re-instantiate its data with what was "written" to disk
// This replaces our deleted entities with their versions pushed to the slice and rebuilds our slice instance to contain those entities
// Because of the mapping we did in the post commit callback they will be re-mapped back to their original ids during the instantiation process
AZ::Data::AssetManager::Instance().ReloadAssetFromData(slicePushResultClone);
// Acquire the root slice
AZ::SliceComponent* rootSlice = nullptr;
AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::BroadcastResult(rootSlice,
&AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::Events::GetEditorRootSlice);
if (!rootSlice)
{
return false;
}
// Find the owning slice instance of one of the entities we added
// This instance should contain all entities prior to the push plus the pushed entities
// We need to update the slice instance here since the instantiated entities in the original instance have been destroyed and re-allocated
// The data and ids should be the same but the SliceInstance* and SliceReference* of the input instance address are invalid and need to be updated
sliceInstanceAddress = rootSlice->FindSlice(*entitiesToPush.begin());
// The instance should be valid and its instance id should match our original instance before the asset reload
if (!sliceInstanceAddress.IsValid() ||
(sliceInstanceAddress.GetInstance()->GetId() != sliceInstanceId))
{
return false;
}
return true;
}
AZ::SliceComponent::SliceInstanceAddress SliceStabilityTest::InstantiateEditorSlice(AZ::Data::AssetId sliceAssetId, AzToolsFramework::EntityIdList& entityList, const AZ::EntityId& parent /*= AZ::EntityId()*/)
{
// Make sure we've created this asset before trying to instantiate it
auto findIt = m_createdSlices.find(sliceAssetId);
if (findIt == m_createdSlices.end())
{
return AZ::SliceComponent::SliceInstanceAddress();
}
// Cache how many instances of this asset exist currently
size_t currentInstanceCount = findIt->second.size();
// Acquire the SliceAsset
AZ::Data::Asset<AZ::SliceAsset> asset = AZ::Data::AssetManager::Instance().FindOrCreateAsset<AZ::SliceAsset>(sliceAssetId, AZ::Data::AssetLoadBehavior::Default);
if (asset.GetStatus() != AZ::Data::AssetData::AssetStatus::NotLoaded)
{
asset.BlockUntilLoadComplete();
}
if (!asset)
{
return AZ::SliceComponent::SliceInstanceAddress();
}
// Instantiate a new slice instance into the editor from the slice asset
AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::BroadcastResult(m_ticket,
&AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::Events::InstantiateEditorSlice,
asset, AZ::Transform::CreateIdentity());
// InstantiateEditorSlice queued the actual instantiation logic onto the tick bus queued events
// Execute the tickbus queue to complete the instantiation
// This should trigger our OnSliceInstantiated callback
AZ::TickBus::ExecuteQueuedEvents();
// Validate that our instances under this asset have grown by 1
// This confirms that OnSliceInstantiated was called during ExecuteQueuedEvents
if (findIt->second.size() != (currentInstanceCount + 1))
{
return AZ::SliceComponent::SliceInstanceAddress();
}
// OnSliceInstantiated has updated the instance list for this asset
// Acquire it now and check if it's valid
AZ::SliceComponent::SliceInstanceAddress& newInstanceAddress = findIt->second.back();
if (!newInstanceAddress.IsValid())
{
return AZ::SliceComponent::SliceInstanceAddress();
}
// Get the root entity of our new instance and check if it's valid
AZ::EntityId sliceInstanceRoot;
AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult(sliceInstanceRoot, &AzToolsFramework::ToolsApplicationRequestBus::Events::GetRootEntityIdOfSliceInstance, newInstanceAddress);
if (!sliceInstanceRoot.IsValid())
{
return AZ::SliceComponent::SliceInstanceAddress();
}
// If a parent was provided then make it the parent of our new slice instance
if (parent.IsValid())
{
AZ::TransformBus::Event(sliceInstanceRoot, &AZ::TransformBus::Events::SetParent, parent);
}
// Reset our ticket
m_ticket = AzFramework::SliceInstantiationTicket();
// For each of the new instances instantiated entities
// Add them to our live entity id list
const AZ::SliceComponent::EntityList& instanceEntities = newInstanceAddress.GetInstance()->GetInstantiated()->m_entities;
for (const AZ::Entity* instanceEntity : instanceEntities)
{
if (instanceEntity)
{
entityList.emplace_back(instanceEntity->GetId());
}
}
// Return the new instance
return newInstanceAddress;
}
void SliceStabilityTest::ReparentEntity(AZ::EntityId& entity, const AZ::EntityId& newParent)
{
if (AzToolsFramework::SliceUtilities::IsReparentNonTrivial(entity, newParent))
{
AzToolsFramework::SliceUtilities::ReparentNonTrivialSliceInstanceHierarchy(entity, newParent);
}
else
{
AZ::TransformBus::Event(entity, &AZ::TransformBus::Events::SetParent, newParent);
}
}
// A helper to find an entity within an entity list
// Used to determine whether to update or push an entity to slice
// As well as to sort our comparison captures in tests
AZ::Entity* SliceStabilityTest::FindEntityInList(const AZ::EntityId& entityId, const AZ::SliceComponent::EntityList& entityList)
{
auto findIt = AZStd::find_if(entityList.begin(), entityList.end(),
[&entityId](AZ::Entity* entity) -> bool
{
if (entity && entity->GetId() == entityId)
{
return true;
}
return false;
});
if (findIt != entityList.end())
{
return *findIt;
}
return nullptr;
}
// Wrapper around finding an entity in the Editor Root Slice
AZ::Entity* SliceStabilityTest::FindEntityInEditor(const AZ::EntityId& entityId)
{
AZ::SliceComponent* editorRootSlice = nullptr;
AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::BroadcastResult(editorRootSlice,
&AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::Events::GetEditorRootSlice);
if (!editorRootSlice)
{
return nullptr;
}
return editorRootSlice->FindEntity(entityId);
}
/*
* EditorEntityContextNotificationBus
*/
void SliceStabilityTest::OnSliceInstantiated(const AZ::Data::AssetId& sliceAssetId, AZ::SliceComponent::SliceInstanceAddress& sliceAddress, const AzFramework::SliceInstantiationTicket& ticket)
{
if (!sliceAssetId.IsValid())
{
EXPECT_TRUE(sliceAssetId.IsValid());
return;
}
// We instantiate slices in 2 manners
// The first is creating a new slice asset and in this case we have no ticket to check against so check the asset id
// The other is we instantiated an instance from an existing asset and we have a ticket to compare against
if (ticket == m_ticket || sliceAssetId == m_newSliceId)
{
m_createdSlices[sliceAssetId].emplace_back(sliceAddress);
m_ticket = AzFramework::SliceInstantiationTicket();
}
}
void SliceStabilityTest::OnSliceInstantiationFailed(const AZ::Data::AssetId& sliceAssetId, const AzFramework::SliceInstantiationTicket& ticket)
{
// This should never occur for an instantiation we're responsible for
EXPECT_FALSE(ticket == m_ticket || sliceAssetId == m_newSliceId);
}
/*
* EditorRequestBus
*/
void SliceStabilityTest::CreateEditorRepresentation(AZ::Entity* entity)
{
if (!entity)
{
EXPECT_TRUE(entity);
return;
}
// CreateEditorEntity triggers this event so we add required components here
AzToolsFramework::EditorEntityContextRequestBus::Broadcast(&AzToolsFramework::EditorEntityContextRequestBus::Events::AddRequiredComponents, *entity);
}
/*
* AssetSystemRequestBus
*/
bool SliceStabilityTest::GetSourceInfoBySourcePath(const char* sourcePath, AZ::Data::AssetInfo& assetInfo, [[maybe_unused]] AZStd::string& watchFolder)
{
// Mock stub for GetSourceInfoBySourcePath
// This call is invoked during Create Slice to predict the asset id of the new slice before it gets processed
assetInfo.m_relativePath = sourcePath;
assetInfo.m_assetId = m_newSliceId;
return true;
}
SliceStabilityTest::SliceOperationValidator::SliceOperationValidator() :
m_serializeContext(nullptr)
{
}
SliceStabilityTest::SliceOperationValidator::~SliceOperationValidator()
{
// Destroy any entities within our capture and clear our capture list
Reset();
}
void SliceStabilityTest::SliceOperationValidator::SetSerializeContext(AZ::SerializeContext* serializeContext)
{
m_serializeContext = serializeContext;
}
bool SliceStabilityTest::SliceOperationValidator::Capture(const AzToolsFramework::EntityIdList& entitiesToCapture)
{
// We either haven't released our current capture or were given nothing to capture or we weren't activated
if (!m_entityStateCapture.empty() || entitiesToCapture.empty() || !m_serializeContext)
{
return false;
}
// Validate that all entities to capture are real entities in the Editor Entity Context
// Place their Entity* in a temp list to clone
AZ::SliceComponent::EntityList captureList;
for (const AZ::EntityId& entityId : entitiesToCapture)
{
AZ::Entity* entity = FindEntityInEditor(entityId);
if (!entity)
{
return false;
}
captureList.emplace_back(entity);
}
// Clone the entities
// The clones should not be active within the entity context and are safe from our slice operations
m_serializeContext->CloneObjectInplace(m_entityStateCapture, &captureList);
// Success if the clone completed and matches the size of the input
return m_entityStateCapture.size() == entitiesToCapture.size();
}
bool SliceStabilityTest::SliceOperationValidator::Compare(const AZ::SliceComponent::SliceInstanceAddress& instanceToCompare)
{
// We've either captured nothing or our instance to compare has no instantiated entities
if (m_entityStateCapture.empty() || !instanceToCompare.IsValid() || !instanceToCompare.GetInstance()->GetInstantiated())
{
return false;
}
// Get the instantiated list of entities and early out if the entity count doesn't match out capture
AZ::SliceComponent::EntityList instanceEntityList = instanceToCompare.GetInstance()->GetInstantiated()->m_entities;
if (instanceEntityList.size() != m_entityStateCapture.size())
{
return false;
}
// Since slice instantiation can alter the order of entities against the original input we need to sort our capture to match
// We do not care if the order of entities is different, only that both sets of entities are identical
// SortCapture will early out if a comparison entity cannot be found in our capture
if (!SortCapture(instanceEntityList))
{
return false;
}
// Build a data patch between our sorted capture and the instantiated comparison entities
// This will diff every reflected element within both entity lists including: Entity Ids, Parent/Child Hierarchies, Component IDs, Component properties, etc.
AZ::DataPatch patch;
bool result = patch.Create(&m_entityStateCapture, &instanceEntityList, AZ::DataPatch::FlagsMap(), AZ::DataPatch::FlagsMap(), m_serializeContext);
// If the patch has any delta between the two then they do not match
return result & !patch.IsData();
}
bool SliceStabilityTest::SliceOperationValidator::SortCapture(const AzToolsFramework::EntityList& orderToMatch)
{
// Since slice instantiation can alter the order of entities against the original input we need to sort our capture to match
// We do not care if the order of entities is different, only that both sets of entities are identical
// SortCapture will early out if a comparison entity cannot be found in our capture
AzToolsFramework::EntityList sortedCapture;
for (const AZ::Entity* entity : orderToMatch)
{
// If an entity is ever nullptr early out
if (!entity)
{
return false;
}
// Try and find the entity within our capture state, early out if we can't find it
AZ::Entity* foundCaptureEntity = FindEntityInList(entity->GetId(), m_entityStateCapture);
if (!foundCaptureEntity)
{
return false;
}
// Place the found entity into our temp
// This builds a sequence of entities that match our orderToMatch list
sortedCapture.emplace_back(foundCaptureEntity);
}
// Update our capture
m_entityStateCapture = sortedCapture;
return true;
}
void SliceStabilityTest::SliceOperationValidator::Reset()
{
// Since our entity capture is made of clones we need to delete them
for (AZ::Entity* capturedEntity : m_entityStateCapture)
{
EXPECT_NE(capturedEntity, nullptr);
delete capturedEntity;
}
m_entityStateCapture.clear();
}
void SliceStabilityTest::EntityReferenceComponent::Reflect(AZ::ReflectContext* reflection)
{
AZ::SerializeContext* serializeContext = AZ::RttiCast<AZ::SerializeContext*>(reflection);
if (serializeContext)
{
serializeContext->Class<EntityReferenceComponent, AzToolsFramework::Components::EditorComponentBase>()->
Field("EntityReference", &EntityReferenceComponent::m_entityReference);
}
}
// Sanity check test to confirm validator will catch differences
TEST_F(SliceStabilityTest, ValidatorCompare_DifferenceInObjects_DifferenceDetected_FT)
{
AUTO_RESULT_IF_SETTING_TRUE(UnitTest::prefabSystemSetting, true)
// Generate a root entity
AzToolsFramework::EntityIdList liveEntityIds;
AZ::EntityId rootEntityId = CreateEditorEntity("Root", liveEntityIds);
ASSERT_TRUE(rootEntityId.IsValid());
// Capture entity state
EXPECT_TRUE(m_validator.Capture(liveEntityIds));
// Create a slice from the root entity
AZ::SliceComponent::SliceInstanceAddress sliceInstanceAddress;
AZ::Data::AssetId newSliceAssetId = CreateSlice("NewSlice", liveEntityIds, sliceInstanceAddress);
ASSERT_TRUE(newSliceAssetId.IsValid());
// Compare generated slice instance to initial capture state
EXPECT_TRUE(m_validator.Compare(sliceInstanceAddress));
// Make a second instance of our new slice
// This instance should have a unique entity id for its root entity
AzToolsFramework::EntityIdList newInstanceEntities;
AZ::SliceComponent::SliceInstanceAddress newInstanceAddress = InstantiateEditorSlice(newSliceAssetId, newInstanceEntities);
ASSERT_TRUE(newInstanceAddress.IsValid());
// Validate that our first instance has a single valid entity
ASSERT_TRUE(sliceInstanceAddress.IsValid());
ASSERT_TRUE(sliceInstanceAddress.GetInstance()->GetInstantiated());
ASSERT_EQ(sliceInstanceAddress.GetInstance()->GetInstantiated()->m_entities.size(), 1);
ASSERT_TRUE(sliceInstanceAddress.GetInstance()->GetInstantiated()->m_entities[0]);
// Validate that our first instance's entity has rootEntityId as its EntityID
EXPECT_EQ(sliceInstanceAddress.GetInstance()->GetInstantiated()->m_entities[0]->GetId(), rootEntityId);
// Validate that our second instance has a single valid entity
ASSERT_TRUE(newInstanceAddress.IsValid());
ASSERT_TRUE(newInstanceAddress.GetInstance()->GetInstantiated());
ASSERT_EQ(newInstanceAddress.GetInstance()->GetInstantiated()->m_entities.size(), 1);
ASSERT_TRUE(newInstanceAddress.GetInstance()->GetInstantiated()->m_entities[0]);
// Validate that our two instances have different EntityIDs for their root entities
EXPECT_NE(sliceInstanceAddress.GetInstance()->GetInstantiated()->m_entities[0]->GetId(), newInstanceAddress.GetInstance()->GetInstantiated()->m_entities[0]->GetId());
// Compare the new instance against the inital capture
// We expect the compare to fail since there is a difference in entity ids
EXPECT_FALSE(m_validator.Compare(newInstanceAddress));
}
}