SliceStabilityTestFramework.cpp 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <Tests/SliceStabilityTests/SliceStabilityTestFramework.h>
  9. #include <AzCore/Serialization/Utils.h>
  10. #include <AzCore/Asset/AssetManager.h>
  11. #include <AzCore/Slice/SliceAsset.h>
  12. #include <AzCore/UserSettings/UserSettingsComponent.h>
  13. #include <AzFramework/Asset/AssetCatalogBus.h>
  14. #include <AzToolsFramework/ToolsComponents/TransformComponent.h>
  15. #include <AzToolsFramework/Slice/SliceUtilities.h>
  16. #include <AzToolsFramework/Asset/AssetSystemComponent.h>
  17. #include <AzToolsFramework/Entity/EditorEntityContextComponent.h>
  18. #include <AzToolsFramework/Entity/EditorEntityHelpers.h>
  19. #include <AzToolsFramework/Entity/EditorEntitySortComponent.h>
  20. namespace UnitTest
  21. {
  22. void SliceStabilityTest::SetUpEditorFixtureImpl()
  23. {
  24. auto* app = GetApplication();
  25. ASSERT_TRUE(app);
  26. // Get the serialize context to reflect our types and set our validator's serialize context
  27. AZ::SerializeContext* serializeContext = app->GetSerializeContext();
  28. m_validator.SetSerializeContext(serializeContext);
  29. app->RegisterComponentDescriptor(EntityReferenceComponent::CreateDescriptor());
  30. // Grab the system entity from the component application
  31. AZ::Entity* systemEntity = app->FindEntity(AZ::SystemEntityId);
  32. // Deactivate the AssetSystemComponent
  33. // We will be implementing the AssetSystemRequestBus and want to avoid Ebus connection conflicts
  34. AzToolsFramework::AssetSystem::AssetSystemComponent* assetSystemComponent = systemEntity->FindComponent<AzToolsFramework::AssetSystem::AssetSystemComponent>();
  35. assetSystemComponent->Deactivate();
  36. AzToolsFramework::AssetSystemRequestBus::Handler::BusConnect();
  37. AzToolsFramework::EditorRequestBus::Handler::BusConnect();
  38. AzToolsFramework::SliceEditorEntityOwnershipServiceNotificationBus::Handler::BusConnect();
  39. AZ::UserSettingsComponentRequestBus::Broadcast(&AZ::UserSettingsComponentRequests::DisableSaveOnFinalize);
  40. // Cache the existing file io instance and build our mock file io
  41. m_priorFileIO = AZ::IO::FileIOBase::GetInstance();
  42. m_fileIOMock = AZStd::make_unique<testing::NiceMock<AZ::IO::MockFileIOBase>>();
  43. // Swap out current file io instance for our mock
  44. AZ::IO::FileIOBase::SetInstance(nullptr);
  45. AZ::IO::FileIOBase::SetInstance(m_fileIOMock.get());
  46. // Setup the default returns for our mock file io calls
  47. AZ::IO::MockFileIOBase::InstallDefaultReturns(*m_fileIOMock.get());
  48. // For write we set the default of the 4th param (bytesWritten) to 1
  49. // otherwise slice transaction errors out during the mock write for writing the default 0 bytes
  50. ON_CALL(*m_fileIOMock.get(), Write(testing::_, testing::_, testing::_, testing::_))
  51. .WillByDefault(
  52. testing::DoAll(
  53. testing::SetArgPointee<3>(1),
  54. testing::Return(AZ::IO::Result(AZ::IO::ResultCode::Success))));
  55. ON_CALL(*m_fileIOMock.get(), GetAlias(testing::_))
  56. .WillByDefault(
  57. testing::Return(""));
  58. ON_CALL(*m_fileIOMock.get(), Rename(testing::_, testing::_))
  59. .WillByDefault(
  60. testing::Return(AZ::IO::Result(AZ::IO::ResultCode::Success)));
  61. }
  62. void SliceStabilityTest::TearDownEditorFixtureImpl()
  63. {
  64. // Get the system entity from the component application
  65. AZ::Entity* systemEntity = nullptr;
  66. AZ::ComponentApplicationBus::BroadcastResult(systemEntity, &AZ::ComponentApplicationBus::Events::FindEntity, AZ::SystemEntityId);
  67. // Deactivate the EditorEntityContextComponent
  68. // This triggers the entity context to destroy its root slice asset which destroys all entities, slice instances, and meta data entities
  69. AzToolsFramework::EditorEntityContextComponent* editorEntityContext = systemEntity->FindComponent<AzToolsFramework::EditorEntityContextComponent>();
  70. editorEntityContext->Deactivate();
  71. // Restore our original file io instance
  72. AZ::IO::FileIOBase::SetInstance(nullptr);
  73. AZ::IO::FileIOBase::SetInstance(m_priorFileIO);
  74. AzToolsFramework::EditorRequestBus::Handler::BusDisconnect();
  75. AzToolsFramework::AssetSystemRequestBus::Handler::BusDisconnect();
  76. AzToolsFramework::SliceEditorEntityOwnershipServiceNotificationBus::Handler::BusDisconnect();
  77. }
  78. AZ::EntityId SliceStabilityTest::CreateEditorEntity(const char* entityName, AzToolsFramework::EntityIdList& entityList, const AZ::EntityId& parentId /*= AZ::EntityId()*/)
  79. {
  80. // Start by creating and registering a new loose entity with the editor entity context
  81. // This call also adds required components onto the entity
  82. AZ::EntityId newEntityId;
  83. AzToolsFramework::EditorEntityContextRequestBus::BroadcastResult(
  84. newEntityId, &AzToolsFramework::EditorEntityContextRequestBus::Events::CreateNewEditorEntity, entityName);
  85. AZ::Entity* newEntity = AzToolsFramework::GetEntityById(newEntityId);
  86. // If newEntity is nullptr still then there was a failure in the above EBus call and we cannot proceed
  87. if (!newEntity)
  88. {
  89. return AZ::EntityId();
  90. }
  91. // Add to our entities container
  92. entityList.emplace_back(newEntity->GetId());
  93. // Get the new entity's transform component
  94. AzToolsFramework::Components::TransformComponent* entityTransform =
  95. newEntity->FindComponent<AzToolsFramework::Components::TransformComponent>();
  96. // If new entity has no Transform component then there was a failure in the create entity call
  97. // and the application of required components
  98. if (!entityTransform)
  99. {
  100. AzToolsFramework::EditorEntityContextRequestBus::Broadcast(&AzToolsFramework::EditorEntityContextRequestBus::Events::DestroyEditorEntity, newEntity->GetId());
  101. return AZ::EntityId();
  102. }
  103. // If supplied set the parent of the new entity
  104. if (parentId.IsValid())
  105. {
  106. entityTransform->SetParent(parentId);
  107. }
  108. // Set the new entity's transform to non zero values
  109. // This helps validate in comparison tests that the transform values of created entities persist during slice operations
  110. entityTransform->SetLocalUniformScale(5);
  111. entityTransform->SetLocalRotation(AZ::Vector3RadToDeg(AZ::Vector3(90, 90, 90)));
  112. entityTransform->SetLocalTranslation(AZ::Vector3(100, 100, 100));
  113. return entityList.back();
  114. }
  115. AZ::Data::AssetId SliceStabilityTest::CreateSlice(AZStd::string sliceAssetName, AzToolsFramework::EntityIdList entityList, AZ::SliceComponent::SliceInstanceAddress& sliceAddress)
  116. {
  117. // Fabricate a new asset id for this slice and set its sub id to the SliceAsset sub id
  118. m_newSliceId = AZ::Uuid::CreateRandom();
  119. m_newSliceId.m_subId = AZ::SliceAsset::GetAssetSubId();
  120. // Init the sliceAddress to invalid
  121. sliceAddress = AZ::SliceComponent::SliceInstanceAddress();
  122. // The relative slice asset path will be used in registering the slice with the asset catalog
  123. // It will show up in debugging and is useful for tracking multiple slice assets in a test
  124. // Since we are mocking file io m_relativeSourceAssetRoot is purely cosmetic
  125. AZStd::string relativeSliceAssetPath = m_relativeSourceAssetRoot + sliceAssetName;
  126. // Call MakeNewSlice and deactivate all prompts for user input
  127. // 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
  128. // inheritSlices: whether to inherit slice ancestry of added instance entities or make a new slice with no ancestry
  129. // setAsDynamic: whether to mark the slice asset as dynamic
  130. // acceptDefaultPath: whether to prompt the user for a path save location or to proceed with the generated one
  131. // 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
  132. // defaultGenerateSharedRoot: whether to generate a shared root if one or more added entities do not share the same root
  133. // silenceWarningPopups: disables QT warning popups from being generated, we can still rely on the return of MakeNewSlice for error handling
  134. bool sliceCreateSuccess = AzToolsFramework::SliceUtilities::MakeNewSlice(AzToolsFramework::EntityIdSet(entityList.begin(), entityList.end()),
  135. relativeSliceAssetPath.c_str(),
  136. true /*inheritSlices*/,
  137. false /*setAsDynamic*/,
  138. true /*acceptDefaultPath*/,
  139. true /*defaultMoveExternalRefs*/,
  140. true /*defaultGenerateSharedRoot*/,
  141. true /*silenceWarningPopups*/);
  142. if (sliceCreateSuccess)
  143. {
  144. // Setup the mock asset info for our new slice
  145. AZ::Data::AssetInfo newSliceInfo;
  146. newSliceInfo.m_assetId = m_newSliceId;
  147. newSliceInfo.m_relativePath = relativeSliceAssetPath;
  148. newSliceInfo.m_assetType = azrtti_typeid<AZ::SliceAsset>();
  149. newSliceInfo.m_sizeBytes = 1;
  150. // Register the asset with the asset catalog
  151. // This mocks the asset load pipeline that triggers the OnCatalogAssetAdded event
  152. // OnCatalogAssetAdded triggers the final steps of the create slice flow by building the first slice instance out of the added entities
  153. AZ::Data::AssetCatalogRequestBus::Broadcast(&AZ::Data::AssetCatalogRequestBus::Events::RegisterAsset, m_newSliceId, newSliceInfo);
  154. }
  155. else
  156. {
  157. return AZ::Uuid::CreateNull();
  158. }
  159. // Acquire the slice instance address the added entities were promoted into
  160. AzFramework::SliceEntityRequestBus::EventResult(sliceAddress, *entityList.begin(),
  161. &AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
  162. // Validate the slice instance
  163. if (!sliceAddress.IsValid())
  164. {
  165. return AZ::Uuid::CreateNull();
  166. }
  167. // Validate the new slice asset id matches our generated asset id
  168. AZ::Data::AssetId createdSliceId = sliceAddress.GetReference()->GetSliceAsset().GetId();
  169. if (m_newSliceId != createdSliceId)
  170. {
  171. // Return invalid id as error
  172. createdSliceId = AZ::Uuid::CreateNull();
  173. }
  174. // Reset our newSliceId so it's invalid for any OnSliceInstantiated calls
  175. m_newSliceId = AZ::Uuid::CreateNull();
  176. return createdSliceId;
  177. }
  178. bool SliceStabilityTest::PushEntitiesToSlice(AZ::SliceComponent::SliceInstanceAddress& sliceInstanceAddress, const AzToolsFramework::EntityIdList& entitiesToPush)
  179. {
  180. // Nothing to push
  181. if (entitiesToPush.empty())
  182. {
  183. return true;
  184. }
  185. // Cannot push to an invalid slice
  186. if (!sliceInstanceAddress.IsValid())
  187. {
  188. return false;
  189. }
  190. // Copy the slice instance id
  191. // The internal instance of the slicecomponent we push to will be destroyed
  192. // We will use this id to validate that the new instance maps to the same id after the push
  193. AZ::SliceComponent::SliceInstance* sliceInstance = sliceInstanceAddress.GetInstance();
  194. AZ::SliceComponent::SliceInstanceId sliceInstanceId = sliceInstance->GetId();
  195. // Get the currently instantiated entities in this slice instance
  196. const AZ::SliceComponent::EntityList& sliceInstanceInstantiatedEntities = sliceInstance->GetInstantiated() ? sliceInstance->GetInstantiated()->m_entities : AZ::SliceComponent::EntityList();
  197. // Acquire the slice instance's asset and start the push slice transaction
  198. const AZ::Data::Asset<AZ::SliceAsset> sliceAsset = sliceInstanceAddress.GetReference()->GetSliceAsset();
  199. AzToolsFramework::SliceUtilities::SliceTransaction::TransactionPtr transaction = AzToolsFramework::SliceUtilities::SliceTransaction::BeginSlicePush(sliceAsset);
  200. // Since a slice push causes the current instance to re-instantiate all added entities will be remade in the new instance
  201. // We will be deleting the existing entities being added as they will be replaced in this manner
  202. AzToolsFramework::EntityIdList entitiesToRemove;
  203. for (const AZ::EntityId& entityToPush : entitiesToPush)
  204. {
  205. AzToolsFramework::SliceUtilities::SliceTransaction::Result result;
  206. // If the entity already exists in the slice then we will update it
  207. if (FindEntityInList(entityToPush, sliceInstanceInstantiatedEntities))
  208. {
  209. result = transaction->UpdateEntity(entityToPush);
  210. }
  211. else
  212. {
  213. // Otherwise we add it to the slice transaction
  214. // and mark the entity for delete since it will be replaced
  215. result = transaction->AddEntity(entityToPush);
  216. entitiesToRemove.emplace_back(entityToPush);
  217. }
  218. if (!result.IsSuccess())
  219. {
  220. return false;
  221. }
  222. }
  223. // This asset mocks the reloaded temp asset that would trigger the ReloadAssetFromData call after a slice push
  224. AZ::Data::Asset<AZ::SliceAsset> slicePushResultClone;
  225. AzToolsFramework::SliceUtilities::SliceTransaction::PostSaveCallback postSaveCallback =
  226. [&sliceAsset, &slicePushResultClone](AzToolsFramework::SliceUtilities::SliceTransaction::TransactionPtr transaction, const char* fullSourcePath, const AzToolsFramework::SliceUtilities::SliceTransaction::SliceAssetPtr& asset) -> void
  227. {
  228. // SlicePostPushCallback updates the slice component that owns our instance's reference (usually the root slice component of the entity context)
  229. // 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)
  230. // 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
  231. AzToolsFramework::SliceUtilities::SlicePostPushCallback(transaction, fullSourcePath, asset);
  232. // Clone our slice asset so that our temp has the same asset id
  233. slicePushResultClone = { sliceAsset.Get()->Clone(), AZ::Data::AssetLoadBehavior::Default };
  234. // Move the transaction's asset data into our temp
  235. // the transaction's asset data is what would be saved to disk and reloaded into our temp
  236. slicePushResultClone.Get()->SetData(asset.Get()->GetEntity(), asset.Get()->GetComponent());
  237. asset.Get()->SetData(nullptr, nullptr, false);
  238. };
  239. // Commit our queued entity adds and updates to be pushed to our slice asset and set our pre and post commit callbacks
  240. const AzToolsFramework::SliceUtilities::SliceTransaction::Result result = transaction->Commit(
  241. "NotAValidAssetPath",
  242. AzToolsFramework::SliceUtilities::SlicePreSaveCallbackForWorldEntities,
  243. postSaveCallback);
  244. if (!result.IsSuccess())
  245. {
  246. return false;
  247. }
  248. // Send the reload event that will trigger the owning slice component to re-instantiate its data with what was "written" to disk
  249. // This replaces our deleted entities with their versions pushed to the slice and rebuilds our slice instance to contain those entities
  250. // 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
  251. AZ::Data::AssetManager::Instance().ReloadAssetFromData(slicePushResultClone);
  252. // Acquire the root slice
  253. AZ::SliceComponent* rootSlice = nullptr;
  254. AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::BroadcastResult(rootSlice,
  255. &AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::Events::GetEditorRootSlice);
  256. if (!rootSlice)
  257. {
  258. return false;
  259. }
  260. // Find the owning slice instance of one of the entities we added
  261. // This instance should contain all entities prior to the push plus the pushed entities
  262. // We need to update the slice instance here since the instantiated entities in the original instance have been destroyed and re-allocated
  263. // 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
  264. sliceInstanceAddress = rootSlice->FindSlice(*entitiesToPush.begin());
  265. // The instance should be valid and its instance id should match our original instance before the asset reload
  266. if (!sliceInstanceAddress.IsValid() ||
  267. (sliceInstanceAddress.GetInstance()->GetId() != sliceInstanceId))
  268. {
  269. return false;
  270. }
  271. return true;
  272. }
  273. AZ::SliceComponent::SliceInstanceAddress SliceStabilityTest::InstantiateEditorSlice(AZ::Data::AssetId sliceAssetId, AzToolsFramework::EntityIdList& entityList, const AZ::EntityId& parent /*= AZ::EntityId()*/)
  274. {
  275. // Make sure we've created this asset before trying to instantiate it
  276. auto findIt = m_createdSlices.find(sliceAssetId);
  277. if (findIt == m_createdSlices.end())
  278. {
  279. return AZ::SliceComponent::SliceInstanceAddress();
  280. }
  281. // Cache how many instances of this asset exist currently
  282. size_t currentInstanceCount = findIt->second.size();
  283. // Acquire the SliceAsset
  284. AZ::Data::Asset<AZ::SliceAsset> asset = AZ::Data::AssetManager::Instance().FindOrCreateAsset<AZ::SliceAsset>(sliceAssetId, AZ::Data::AssetLoadBehavior::Default);
  285. if (asset.GetStatus() != AZ::Data::AssetData::AssetStatus::NotLoaded)
  286. {
  287. asset.BlockUntilLoadComplete();
  288. }
  289. if (!asset)
  290. {
  291. return AZ::SliceComponent::SliceInstanceAddress();
  292. }
  293. // Instantiate a new slice instance into the editor from the slice asset
  294. AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::BroadcastResult(m_ticket,
  295. &AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::Events::InstantiateEditorSlice,
  296. asset, AZ::Transform::CreateIdentity());
  297. // InstantiateEditorSlice queued the actual instantiation logic onto the tick bus queued events
  298. // Execute the tickbus queue to complete the instantiation
  299. // This should trigger our OnSliceInstantiated callback
  300. AZ::TickBus::ExecuteQueuedEvents();
  301. // Validate that our instances under this asset have grown by 1
  302. // This confirms that OnSliceInstantiated was called during ExecuteQueuedEvents
  303. if (findIt->second.size() != (currentInstanceCount + 1))
  304. {
  305. return AZ::SliceComponent::SliceInstanceAddress();
  306. }
  307. // OnSliceInstantiated has updated the instance list for this asset
  308. // Acquire it now and check if it's valid
  309. AZ::SliceComponent::SliceInstanceAddress& newInstanceAddress = findIt->second.back();
  310. if (!newInstanceAddress.IsValid())
  311. {
  312. return AZ::SliceComponent::SliceInstanceAddress();
  313. }
  314. // Get the root entity of our new instance and check if it's valid
  315. AZ::EntityId sliceInstanceRoot;
  316. AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult(sliceInstanceRoot, &AzToolsFramework::ToolsApplicationRequestBus::Events::GetRootEntityIdOfSliceInstance, newInstanceAddress);
  317. if (!sliceInstanceRoot.IsValid())
  318. {
  319. return AZ::SliceComponent::SliceInstanceAddress();
  320. }
  321. // If a parent was provided then make it the parent of our new slice instance
  322. if (parent.IsValid())
  323. {
  324. AZ::TransformBus::Event(sliceInstanceRoot, &AZ::TransformBus::Events::SetParent, parent);
  325. }
  326. // Reset our ticket
  327. m_ticket = AzFramework::SliceInstantiationTicket();
  328. // For each of the new instances instantiated entities
  329. // Add them to our live entity id list
  330. const AZ::SliceComponent::EntityList& instanceEntities = newInstanceAddress.GetInstance()->GetInstantiated()->m_entities;
  331. for (const AZ::Entity* instanceEntity : instanceEntities)
  332. {
  333. if (instanceEntity)
  334. {
  335. entityList.emplace_back(instanceEntity->GetId());
  336. }
  337. }
  338. // Return the new instance
  339. return newInstanceAddress;
  340. }
  341. void SliceStabilityTest::ReparentEntity(AZ::EntityId& entity, const AZ::EntityId& newParent)
  342. {
  343. if (AzToolsFramework::SliceUtilities::IsReparentNonTrivial(entity, newParent))
  344. {
  345. AzToolsFramework::SliceUtilities::ReparentNonTrivialSliceInstanceHierarchy(entity, newParent);
  346. }
  347. else
  348. {
  349. AZ::TransformBus::Event(entity, &AZ::TransformBus::Events::SetParent, newParent);
  350. }
  351. }
  352. // A helper to find an entity within an entity list
  353. // Used to determine whether to update or push an entity to slice
  354. // As well as to sort our comparison captures in tests
  355. AZ::Entity* SliceStabilityTest::FindEntityInList(const AZ::EntityId& entityId, const AZ::SliceComponent::EntityList& entityList)
  356. {
  357. auto findIt = AZStd::find_if(entityList.begin(), entityList.end(),
  358. [&entityId](AZ::Entity* entity) -> bool
  359. {
  360. if (entity && entity->GetId() == entityId)
  361. {
  362. return true;
  363. }
  364. return false;
  365. });
  366. if (findIt != entityList.end())
  367. {
  368. return *findIt;
  369. }
  370. return nullptr;
  371. }
  372. // Wrapper around finding an entity in the Editor Root Slice
  373. AZ::Entity* SliceStabilityTest::FindEntityInEditor(const AZ::EntityId& entityId)
  374. {
  375. AZ::SliceComponent* editorRootSlice = nullptr;
  376. AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::BroadcastResult(editorRootSlice,
  377. &AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::Events::GetEditorRootSlice);
  378. if (!editorRootSlice)
  379. {
  380. return nullptr;
  381. }
  382. return editorRootSlice->FindEntity(entityId);
  383. }
  384. /*
  385. * EditorEntityContextNotificationBus
  386. */
  387. void SliceStabilityTest::OnSliceInstantiated(const AZ::Data::AssetId& sliceAssetId, AZ::SliceComponent::SliceInstanceAddress& sliceAddress, const AzFramework::SliceInstantiationTicket& ticket)
  388. {
  389. if (!sliceAssetId.IsValid())
  390. {
  391. EXPECT_TRUE(sliceAssetId.IsValid());
  392. return;
  393. }
  394. // We instantiate slices in 2 manners
  395. // The first is creating a new slice asset and in this case we have no ticket to check against so check the asset id
  396. // The other is we instantiated an instance from an existing asset and we have a ticket to compare against
  397. if (ticket == m_ticket || sliceAssetId == m_newSliceId)
  398. {
  399. m_createdSlices[sliceAssetId].emplace_back(sliceAddress);
  400. m_ticket = AzFramework::SliceInstantiationTicket();
  401. }
  402. }
  403. void SliceStabilityTest::OnSliceInstantiationFailed(const AZ::Data::AssetId& sliceAssetId, const AzFramework::SliceInstantiationTicket& ticket)
  404. {
  405. // This should never occur for an instantiation we're responsible for
  406. EXPECT_FALSE(ticket == m_ticket || sliceAssetId == m_newSliceId);
  407. }
  408. /*
  409. * EditorRequestBus
  410. */
  411. void SliceStabilityTest::CreateEditorRepresentation(AZ::Entity* entity)
  412. {
  413. if (!entity)
  414. {
  415. EXPECT_TRUE(entity);
  416. return;
  417. }
  418. // CreateEditorEntity triggers this event so we add required components here
  419. AzToolsFramework::EditorEntityContextRequestBus::Broadcast(&AzToolsFramework::EditorEntityContextRequestBus::Events::AddRequiredComponents, *entity);
  420. }
  421. /*
  422. * AssetSystemRequestBus
  423. */
  424. bool SliceStabilityTest::GetSourceInfoBySourcePath(const char* sourcePath, AZ::Data::AssetInfo& assetInfo, [[maybe_unused]] AZStd::string& watchFolder)
  425. {
  426. // Mock stub for GetSourceInfoBySourcePath
  427. // This call is invoked during Create Slice to predict the asset id of the new slice before it gets processed
  428. assetInfo.m_relativePath = sourcePath;
  429. assetInfo.m_assetId = m_newSliceId;
  430. return true;
  431. }
  432. SliceStabilityTest::SliceOperationValidator::SliceOperationValidator() :
  433. m_serializeContext(nullptr)
  434. {
  435. }
  436. SliceStabilityTest::SliceOperationValidator::~SliceOperationValidator()
  437. {
  438. // Destroy any entities within our capture and clear our capture list
  439. Reset();
  440. }
  441. void SliceStabilityTest::SliceOperationValidator::SetSerializeContext(AZ::SerializeContext* serializeContext)
  442. {
  443. m_serializeContext = serializeContext;
  444. }
  445. bool SliceStabilityTest::SliceOperationValidator::Capture(const AzToolsFramework::EntityIdList& entitiesToCapture)
  446. {
  447. // We either haven't released our current capture or were given nothing to capture or we weren't activated
  448. if (!m_entityStateCapture.empty() || entitiesToCapture.empty() || !m_serializeContext)
  449. {
  450. return false;
  451. }
  452. // Validate that all entities to capture are real entities in the Editor Entity Context
  453. // Place their Entity* in a temp list to clone
  454. AZ::SliceComponent::EntityList captureList;
  455. for (const AZ::EntityId& entityId : entitiesToCapture)
  456. {
  457. AZ::Entity* entity = FindEntityInEditor(entityId);
  458. if (!entity)
  459. {
  460. return false;
  461. }
  462. captureList.emplace_back(entity);
  463. }
  464. // Clone the entities
  465. // The clones should not be active within the entity context and are safe from our slice operations
  466. m_serializeContext->CloneObjectInplace(m_entityStateCapture, &captureList);
  467. // Success if the clone completed and matches the size of the input
  468. return m_entityStateCapture.size() == entitiesToCapture.size();
  469. }
  470. bool SliceStabilityTest::SliceOperationValidator::Compare(const AZ::SliceComponent::SliceInstanceAddress& instanceToCompare)
  471. {
  472. // We've either captured nothing or our instance to compare has no instantiated entities
  473. if (m_entityStateCapture.empty() || !instanceToCompare.IsValid() || !instanceToCompare.GetInstance()->GetInstantiated())
  474. {
  475. return false;
  476. }
  477. // Get the instantiated list of entities and early out if the entity count doesn't match out capture
  478. AZ::SliceComponent::EntityList instanceEntityList = instanceToCompare.GetInstance()->GetInstantiated()->m_entities;
  479. if (instanceEntityList.size() != m_entityStateCapture.size())
  480. {
  481. return false;
  482. }
  483. // Since slice instantiation can alter the order of entities against the original input we need to sort our capture to match
  484. // We do not care if the order of entities is different, only that both sets of entities are identical
  485. // SortCapture will early out if a comparison entity cannot be found in our capture
  486. if (!SortCapture(instanceEntityList))
  487. {
  488. return false;
  489. }
  490. // Build a data patch between our sorted capture and the instantiated comparison entities
  491. // This will diff every reflected element within both entity lists including: Entity Ids, Parent/Child Hierarchies, Component IDs, Component properties, etc.
  492. AZ::DataPatch patch;
  493. bool result = patch.Create(&m_entityStateCapture, &instanceEntityList, AZ::DataPatch::FlagsMap(), AZ::DataPatch::FlagsMap(), m_serializeContext);
  494. // If the patch has any delta between the two then they do not match
  495. return result & !patch.IsData();
  496. }
  497. bool SliceStabilityTest::SliceOperationValidator::SortCapture(const AzToolsFramework::EntityList& orderToMatch)
  498. {
  499. // Since slice instantiation can alter the order of entities against the original input we need to sort our capture to match
  500. // We do not care if the order of entities is different, only that both sets of entities are identical
  501. // SortCapture will early out if a comparison entity cannot be found in our capture
  502. AzToolsFramework::EntityList sortedCapture;
  503. for (const AZ::Entity* entity : orderToMatch)
  504. {
  505. // If an entity is ever nullptr early out
  506. if (!entity)
  507. {
  508. return false;
  509. }
  510. // Try and find the entity within our capture state, early out if we can't find it
  511. AZ::Entity* foundCaptureEntity = FindEntityInList(entity->GetId(), m_entityStateCapture);
  512. if (!foundCaptureEntity)
  513. {
  514. return false;
  515. }
  516. // Place the found entity into our temp
  517. // This builds a sequence of entities that match our orderToMatch list
  518. sortedCapture.emplace_back(foundCaptureEntity);
  519. }
  520. // Update our capture
  521. m_entityStateCapture = sortedCapture;
  522. return true;
  523. }
  524. void SliceStabilityTest::SliceOperationValidator::Reset()
  525. {
  526. // Since our entity capture is made of clones we need to delete them
  527. for (AZ::Entity* capturedEntity : m_entityStateCapture)
  528. {
  529. EXPECT_NE(capturedEntity, nullptr);
  530. delete capturedEntity;
  531. }
  532. m_entityStateCapture.clear();
  533. }
  534. void SliceStabilityTest::EntityReferenceComponent::Reflect(AZ::ReflectContext* reflection)
  535. {
  536. AZ::SerializeContext* serializeContext = AZ::RttiCast<AZ::SerializeContext*>(reflection);
  537. if (serializeContext)
  538. {
  539. serializeContext->Class<EntityReferenceComponent, AzToolsFramework::Components::EditorComponentBase>()->
  540. Field("EntityReference", &EntityReferenceComponent::m_entityReference);
  541. }
  542. }
  543. // Sanity check test to confirm validator will catch differences
  544. TEST_F(SliceStabilityTest, ValidatorCompare_DifferenceInObjects_DifferenceDetected_FT)
  545. {
  546. AUTO_RESULT_IF_SETTING_TRUE(UnitTest::prefabSystemSetting, true)
  547. // Generate a root entity
  548. AzToolsFramework::EntityIdList liveEntityIds;
  549. AZ::EntityId rootEntityId = CreateEditorEntity("Root", liveEntityIds);
  550. ASSERT_TRUE(rootEntityId.IsValid());
  551. // Capture entity state
  552. EXPECT_TRUE(m_validator.Capture(liveEntityIds));
  553. // Create a slice from the root entity
  554. AZ::SliceComponent::SliceInstanceAddress sliceInstanceAddress;
  555. AZ::Data::AssetId newSliceAssetId = CreateSlice("NewSlice", liveEntityIds, sliceInstanceAddress);
  556. ASSERT_TRUE(newSliceAssetId.IsValid());
  557. // Compare generated slice instance to initial capture state
  558. EXPECT_TRUE(m_validator.Compare(sliceInstanceAddress));
  559. // Make a second instance of our new slice
  560. // This instance should have a unique entity id for its root entity
  561. AzToolsFramework::EntityIdList newInstanceEntities;
  562. AZ::SliceComponent::SliceInstanceAddress newInstanceAddress = InstantiateEditorSlice(newSliceAssetId, newInstanceEntities);
  563. ASSERT_TRUE(newInstanceAddress.IsValid());
  564. // Validate that our first instance has a single valid entity
  565. ASSERT_TRUE(sliceInstanceAddress.IsValid());
  566. ASSERT_TRUE(sliceInstanceAddress.GetInstance()->GetInstantiated());
  567. ASSERT_EQ(sliceInstanceAddress.GetInstance()->GetInstantiated()->m_entities.size(), 1);
  568. ASSERT_TRUE(sliceInstanceAddress.GetInstance()->GetInstantiated()->m_entities[0]);
  569. // Validate that our first instance's entity has rootEntityId as its EntityID
  570. EXPECT_EQ(sliceInstanceAddress.GetInstance()->GetInstantiated()->m_entities[0]->GetId(), rootEntityId);
  571. // Validate that our second instance has a single valid entity
  572. ASSERT_TRUE(newInstanceAddress.IsValid());
  573. ASSERT_TRUE(newInstanceAddress.GetInstance()->GetInstantiated());
  574. ASSERT_EQ(newInstanceAddress.GetInstance()->GetInstantiated()->m_entities.size(), 1);
  575. ASSERT_TRUE(newInstanceAddress.GetInstance()->GetInstantiated()->m_entities[0]);
  576. // Validate that our two instances have different EntityIDs for their root entities
  577. EXPECT_NE(sliceInstanceAddress.GetInstance()->GetInstantiated()->m_entities[0]->GetId(), newInstanceAddress.GetInstance()->GetInstantiated()->m_entities[0]->GetId());
  578. // Compare the new instance against the inital capture
  579. // We expect the compare to fail since there is a difference in entity ids
  580. EXPECT_FALSE(m_validator.Compare(newInstanceAddress));
  581. }
  582. }