/* * 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 "Vegetation_precompiled.h" #include "VegetationTest.h" #include "VegetationMocks.h" #include <AzCore/Component/Entity.h> #include <AzTest/AzTest.h> #include <AzCore/UnitTest/TestTypes.h> #include <AzCore/Asset/AssetManagerBus.h> #include <AzCore/Asset/AssetManagerComponent.h> #include <AzCore/Asset/AssetManager.h> #include <AzCore/Memory/PoolAllocator.h> #include <AzFramework/Entity/GameEntityContextBus.h> #include <Vegetation/DynamicSliceInstanceSpawner.h> #include <Vegetation/Ebuses/DescriptorNotificationBus.h> namespace UnitTest { // Mock VegetationSystemComponent is needed to reflect only the DynamicSliceInstanceSpawner. class MockDynamicSliceInstanceVegetationSystemComponent : public AZ::Component { public: AZ_COMPONENT(MockDynamicSliceInstanceVegetationSystemComponent, "{41BCCB16-1E27-4B8E-9053-762CC5034F18}", AZ::Component); void Activate() override {} void Deactivate() override {} static void Reflect(AZ::ReflectContext* reflect) { Vegetation::InstanceSpawner::Reflect(reflect); Vegetation::DynamicSliceInstanceSpawner::Reflect(reflect); } static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) { provided.push_back(AZ_CRC("VegetationSystemService", 0xa2322728)); } }; // To test Dynamic Slice spawning, we need to mock up enough of the asset management system and the dynamic slice // asset handling to pretend like we're loading/unloading dynamic slices successfully. class DynamicSliceInstanceSpawnerTests : public VegetationComponentTests , public Vegetation::DescriptorNotificationBus::Handler , public AZ::Data::AssetCatalogRequestBus::Handler , public AZ::Data::AssetHandler , public AZ::Data::AssetCatalog , public AzFramework::GameEntityContextRequestBus::Handler { public: void RegisterComponentDescriptors() override { m_app.RegisterComponentDescriptor(MockDynamicSliceInstanceVegetationSystemComponent::CreateDescriptor()); } void SetUp() override { VegetationComponentTests::SetUp(); // Create a real Asset Mananger, and point to ourselves as the handler for DynamicSliceAsset. AZ::AllocatorInstance<AZ::PoolAllocator>::Create(); AZ::AllocatorInstance<AZ::ThreadPoolAllocator>::Create(); AZ::Data::AssetManager::Descriptor descriptor; descriptor.m_maxWorkerThreads = 1; AZ::Data::AssetManager::Create(descriptor); AZ::Data::AssetManager::Instance().RegisterHandler(this, AZ::AzTypeInfo<AZ::DynamicSliceAsset>::Uuid()); AZ::Data::AssetManager::Instance().RegisterCatalog(this, AZ::AzTypeInfo<AZ::DynamicSliceAsset>::Uuid()); m_app.RegisterComponentDescriptor(AZ::SliceComponent::CreateDescriptor()); // Intercept messages for finding assets by name and creating/destroying slices. AZ::Data::AssetCatalogRequestBus::Handler::BusConnect(); AzFramework::GameEntityContextRequestBus::Handler::BusConnect(); } void TearDown() override { // Give the AssetManager a chance to fire off any lingering events and perform cleanup for any // dynamic slice assets we loaded. AZ::Data::AssetManager::Instance().DispatchEvents(); AzFramework::GameEntityContextRequestBus::Handler::BusDisconnect(); AZ::Data::AssetManager::Instance().UnregisterCatalog(this); AZ::Data::AssetManager::Instance().UnregisterHandler(this); AZ::Data::AssetCatalogRequestBus::Handler::BusDisconnect(); AZ::Data::AssetManager::Destroy(); AZ::AllocatorInstance<AZ::ThreadPoolAllocator>::Destroy(); AZ::AllocatorInstance<AZ::PoolAllocator>::Destroy(); VegetationComponentTests::TearDown(); } // Helper methods: // Set up a mock asset with the given name and id and direct the instance spawner to use it. void CreateAndSetMockAsset(Vegetation::DynamicSliceInstanceSpawner& instanceSpawner, AZ::Data::AssetId assetId, AZStd::string assetPath) { // Save these off for use from our mock AssetCatalogRequestBus m_assetId = assetId; m_assetPath = assetPath; Vegetation::DescriptorNotificationBus::Handler::BusConnect(&instanceSpawner); // Tell the spawner to use this asset. Note that this also triggers a LoadAssets() call // internally. instanceSpawner.SetSliceAssetPath(m_assetPath); // Our instance spawner should now have a valid asset reference. // It may or may not be loaded already by the time we get here, // depending on how quickly the Asset Processor job thread picks it up. EXPECT_FALSE(instanceSpawner.HasEmptyAssetReferences()); // Since the asset load is going through the real AssetManager, there's a delay while a separate // job thread executes and actually loads our mock dynamic slice asset. // If our asset hasn't loaded successfully after 5 seconds, it's unlikely to succeed. // This choice of delay should be *reasonably* safe because it's all CPU-based processing, // no actual I/O occurs as a part of the test. constexpr int sleepMs = 10; constexpr int totalWaitTimeMs = 5000; int numRetries = totalWaitTimeMs / sleepMs; while ((m_numOnLoadedCalls < 1) && (numRetries >= 0)) { AZ::Data::AssetManager::Instance().DispatchEvents(); AZ::SystemTickBus::Broadcast(&AZ::SystemTickBus::Events::OnSystemTick); AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(sleepMs)); numRetries--; } ASSERT_TRUE(m_numOnLoadedCalls == 1); EXPECT_TRUE(instanceSpawner.IsLoaded()); EXPECT_TRUE(instanceSpawner.IsSpawnable()); Vegetation::DescriptorNotificationBus::Handler::BusDisconnect(); } // AssetHandler // Minimalist mocks to look like a Dynamic Slice has been created/loaded/destroyed successfully AZ::Data::AssetPtr CreateAsset(const AZ::Data::AssetId& id, const AZ::Data::AssetType& type) override { AZ::DynamicSliceAsset* sliceAsset = new AZ::DynamicSliceAsset(id); AZ::Entity* mockEntity = new AZ::Entity(); mockEntity->Init(); auto mockComponent = mockEntity->CreateComponent<AZ::SliceComponent>(); mockEntity->Activate(); sliceAsset->SetData(mockEntity, mockComponent); MockAssetData* temp = reinterpret_cast<MockAssetData*>(sliceAsset); temp->SetStatus(AZ::Data::AssetData::AssetStatus::NotLoaded); return sliceAsset; } void DestroyAsset(AZ::Data::AssetPtr ptr) override { delete ptr; } void GetHandledAssetTypes(AZStd::vector<AZ::Data::AssetType>& assetTypes) override { assetTypes.push_back(AZ::AzTypeInfo<AZ::DynamicSliceAsset>::Uuid()); } bool LoadAssetData(const AZ::Data::Asset<AZ::Data::AssetData>& asset, const char* assetPath, const AZ::Data::AssetFilterCB& assetLoadFilterCB) { MockAssetData* temp = reinterpret_cast<MockAssetData*>(asset.GetData()); temp->SetStatus(AZ::Data::AssetData::AssetStatus::Ready); return true; } // DescriptorNotificationBus // Keep track of whether or not the Spawner successfully loaded the asset and notified listeners void OnDescriptorAssetsLoaded() override { m_numOnLoadedCalls++; } // AssetCatalogRequestBus // Minimalist mocks to provide our desired asset path or asset id AZStd::string GetAssetPathById(const AZ::Data::AssetId& /*id*/) override { return m_assetPath; } AZ::Data::AssetId GetAssetIdByPath(const char* /*path*/, const AZ::Data::AssetType& /*typeToRegister*/, bool /*autoRegisterIfNotFound*/) override { return m_assetId; } AZ::Data::AssetInfo GetAssetInfoById(const AZ::Data::AssetId& /*id*/) override { AZ::Data::AssetInfo assetInfo; assetInfo.m_assetId = m_assetId; assetInfo.m_assetType = AZ::AzTypeInfo<AZ::DynamicSliceAsset>::Uuid(); assetInfo.m_relativePath = m_assetPath; return assetInfo; } // GameEntityContextRequestBus // Minimalist mocks to mock out InstantiateDynamicSlice AzFramework::EntityContextId GetGameEntityContextId() override { return AzFramework::EntityContextId(); } AZ::Entity* CreateGameEntity(const char* /*name*/) override { return nullptr; } AzFramework::BehaviorEntity CreateGameEntityForBehaviorContext(const char* /*name*/) override { return AzFramework::BehaviorEntity(); } void AddGameEntity(AZ::Entity* /*entity*/) override {} void DestroyGameEntity(const AZ::EntityId& /*id*/) override {} void DestroyGameEntityAndDescendants(const AZ::EntityId& /*id*/) override {} void ActivateGameEntity(const AZ::EntityId& /*id*/) override {} void DeactivateGameEntity(const AZ::EntityId& /*id*/) override {} bool LoadFromStream(AZ::IO::GenericStream& /*stream*/, bool /*remapIds*/) override { return true; } void ResetGameContext() override {} void MarkEntityForNoActivation(AZ::EntityId /*entityId*/) override {} AZStd::string GetEntityName(const AZ::EntityId&) override { return ""; } // These are the only mocks actually needed for unit testing DynamicSliceInstanceSpawner: void CancelDynamicSliceInstantiation(const AzFramework::SliceInstantiationTicket& /*ticket*/) override {} bool DestroyDynamicSliceByEntity(const AZ::EntityId& /*id*/) override { return true; } AzFramework::SliceInstantiationTicket InstantiateDynamicSlice( const AZ::Data::Asset<AZ::Data::AssetData>& /*sliceAsset*/, const AZ::Transform& /*worldTransform*/, const AZ::IdUtils::Remapper<AZ::EntityId>::IdMapper& /*customIdMapper*/) override { return AzFramework::SliceInstantiationTicket(AzFramework::EntityContextId::Create(), 1); } // AssetCatalog // Minimalist mock to pretend like we've loaded a Dynamic Slice asset AZ::Data::AssetStreamInfo GetStreamInfoForLoad(const AZ::Data::AssetId& id, const AZ::Data::AssetType& type) override { EXPECT_TRUE(type == AZ::AzTypeInfo<AZ::DynamicSliceAsset>::Uuid()); AZ::Data::AssetStreamInfo info; info.m_dataOffset = 0; info.m_streamFlags = AZ::IO::OpenMode::ModeRead; info.m_streamName = m_assetPath; info.m_dataLen = 0; // By setting this to true, we call our custom LoadAssetData() above instead of using actual File IO info.m_isCustomStreamType = true; return info; } AZStd::string m_assetPath; AZ::Data::AssetId m_assetId; int m_numOnLoadedCalls = 0; }; TEST_F(DynamicSliceInstanceSpawnerTests, BasicInitializationTest) { // Basic test to make sure we can construct / destroy without errors. Vegetation::DynamicSliceInstanceSpawner instanceSpawner; } TEST_F(DynamicSliceInstanceSpawnerTests, DefaultSpawnersAreEqual) { // Two different instances of the default DynamicSliceInstanceSpawner should // be considered data-equivalent. Vegetation::DynamicSliceInstanceSpawner instanceSpawner1; Vegetation::DynamicSliceInstanceSpawner instanceSpawner2; EXPECT_TRUE(instanceSpawner1 == instanceSpawner2); } // [LY-117648] This test intermittently fails on automated builds with errors like the following: // d:\ly\workspace\DEV_shelf_build\dev\Code\Framework\AzCore\AzCore/UnitTest/UnitTest.h(161): // error: Asset handler 'AssetHandler' is being removed but there are still 1 active assets being handled by it! // d:\ly\workspace\DEV_shelf_build\dev\Code\Framework\AzCore\AzCore\Asset\AssetManager.cpp(1051) : // error: No handler was registered for this asset[type:0x6dd991e8 id : {535BED6F - 0CFA - 4C53 - 9B2A - 1A531BBB16F6} : 0]! // Until the intermittent failure can be solved, the test is disabled. TEST_F(DynamicSliceInstanceSpawnerTests, DISABLED_DifferentSpawnersAreNotEqual) { // Two spawners with different data should *not* be data-equivalent. Vegetation::DynamicSliceInstanceSpawner instanceSpawner1; Vegetation::DynamicSliceInstanceSpawner instanceSpawner2; // Give the second instance spawner a non-default asset reference. CreateAndSetMockAsset(instanceSpawner2, AZ::Uuid::CreateRandom(), "test"); // The test is written this way because only the == operator is overloaded. EXPECT_TRUE(!(instanceSpawner1 == instanceSpawner2)); } // [LY-118267] This test intermittently fails on automated builds, so disabling temporarily until the root cause // can be identified TEST_F(DynamicSliceInstanceSpawnerTests, DISABLED_LoadAndUnloadAssets) { // The spawner should successfully load/unload assets without errors. Vegetation::DynamicSliceInstanceSpawner instanceSpawner; // Our instance spawner should be empty before we set the assets. EXPECT_TRUE(instanceSpawner.HasEmptyAssetReferences()); // This will test the asset load. CreateAndSetMockAsset(instanceSpawner, AZ::Uuid::CreateRandom(), "test"); // Test the asset unload works too. Vegetation::DescriptorNotificationBus::Handler::BusConnect(&instanceSpawner); instanceSpawner.UnloadAssets(); EXPECT_FALSE(instanceSpawner.IsLoaded()); EXPECT_FALSE(instanceSpawner.IsSpawnable()); Vegetation::DescriptorNotificationBus::Handler::BusDisconnect(); } // [LY-118267] Any test using CreateAndSetMockAsset can intermittently fail, so disabling them for now. TEST_F(DynamicSliceInstanceSpawnerTests, DISABLED_CreateAndDestroyInstance) { // The spawner should successfully create and destroy an instance without errors. Vegetation::DynamicSliceInstanceSpawner instanceSpawner; CreateAndSetMockAsset(instanceSpawner, AZ::Uuid::CreateRandom(), "test"); instanceSpawner.OnRegisterUniqueDescriptor(); Vegetation::InstanceData instanceData; Vegetation::InstancePtr instance = instanceSpawner.CreateInstance(instanceData); EXPECT_TRUE(instance); instanceSpawner.DestroyInstance(0, instance); instanceSpawner.OnReleaseUniqueDescriptor(); } TEST_F(DynamicSliceInstanceSpawnerTests, SpawnerRegisteredWithDescriptor) { // Validate that the Descriptor successfully gets DynamicSliceInstanceSpawner registered with it, // as long as InstanceSpawner and DynamicSliceInstanceSpawner have been reflected. MockDynamicSliceInstanceVegetationSystemComponent* component = nullptr; auto entity = CreateEntity(&component); Vegetation::Descriptor descriptor; descriptor.RefreshSpawnerTypeList(); auto spawnerTypes = descriptor.GetSpawnerTypeList(); EXPECT_TRUE(spawnerTypes.size() == 1); EXPECT_TRUE(spawnerTypes[0].first == Vegetation::DynamicSliceInstanceSpawner::RTTI_Type()); } TEST_F(DynamicSliceInstanceSpawnerTests, DescriptorCreatesCorrectSpawner) { // Validate that the Descriptor successfully creates a new DynamicSliceInstanceSpawner if we change // the spawner type on the Descriptor. MockDynamicSliceInstanceVegetationSystemComponent* component = nullptr; auto entity = CreateEntity(&component); // We expect the Descriptor to start off with a Legacy Vegetation spawner, but then should correctly get an // DynamicSliceInstanceSpawner after we change spawnerType. Vegetation::Descriptor descriptor; EXPECT_TRUE(azrtti_typeid(*(descriptor.GetInstanceSpawner())) != Vegetation::DynamicSliceInstanceSpawner::RTTI_Type()); descriptor.m_spawnerType = Vegetation::DynamicSliceInstanceSpawner::RTTI_Type(); descriptor.RefreshSpawnerTypeList(); descriptor.SpawnerTypeChanged(); EXPECT_TRUE(azrtti_typeid(*(descriptor.GetInstanceSpawner())) == Vegetation::DynamicSliceInstanceSpawner::RTTI_Type()); } }