/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include namespace UnitTest { class MockRenderNode : public IRenderNode { public: void ReleaseNode(bool) override { } EERType GetRenderNodeType() override { return eERType_NotRenderNode; } const char* GetEntityClassName(void) const override { return "Mock"; } const char* GetName(void) const override { return "Mock"; } Vec3 GetPos(bool) const override { return Vec3(0.0f); } void Render(const SRendParams&, const SRenderingPassInfo& passInfo) override {} void SetMaterial(_smart_ptr pMat) override {} _smart_ptr GetMaterial(Vec3* pHitPos = NULL) override { return nullptr; } _smart_ptr GetMaterialOverride() override { return nullptr; } float GetMaxViewDist() override { return 0.0f; } void GetMemoryUsage(ICrySizer* pSizer) const override {} const AABB GetBBox() const override { return AABB(Vec3(0.0f), Vec3(0.0f)); } void SetBBox(const AABB& WSBBox) override { } void FillBBox(AABB& aabb) override {} void OffsetPosition(const Vec3& delta) override {} }; // Mock VegetationSystemComponent is needed to reflect only the LegacyVegetationInstanceSpawner. class MockLegacyVegetationInstanceVegetationSystemComponent : public AZ::Component { public: AZ_COMPONENT(MockLegacyVegetationInstanceVegetationSystemComponent, "{DEE0C740-40E1-49F7-84DB-4603FBAD8A87}", AZ::Component); void Activate() override {} void Deactivate() override {} static void Reflect(AZ::ReflectContext* reflect) { Vegetation::InstanceSpawner::Reflect(reflect); Vegetation::LegacyVegetationInstanceSpawner::Reflect(reflect); // We also register EmptyInstanceSpawner so that we can test the Descriptor "type changed" methods // appropriately. Since the default for Descriptor is to create a LegacyVegetationInstanceSpawner, // we need to change it to something else and back again to truly test the "type changed" method. Vegetation::EmptyInstanceSpawner::Reflect(reflect); } static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) { provided.push_back(AZ_CRC("VegetationSystemService", 0xa2322728)); } }; // To test Legacy Vegetation spawning, we need to mock up enough of the asset management system and the mesh // asset handling to pretend like we're loading/unloading meshes successfully. class LegacyVegetationInstanceSpawnerTests : public VegetationComponentTests , public Vegetation::DescriptorNotificationBus::Handler , public StatInstGroupEventBus::Handler , public AZ::Data::AssetCatalogRequestBus::Handler , public AZ::Data::AssetHandler , public AZ::Data::AssetCatalog { public: void RegisterComponentDescriptors() override { m_app.RegisterComponentDescriptor(MockLegacyVegetationInstanceVegetationSystemComponent::CreateDescriptor()); } void SetUp() override { VegetationComponentTests::SetUp(); // capture prior state. m_savedEnv = gEnv; // override state with our needed test mocks m_data = AZStd::make_unique(); gEnv = &(m_data->m_mockGEnv); m_data->m_mockGEnv.p3DEngine = &(m_data->m_mock3DEngine); m_data->m_mockGEnv.pSystem = &(m_data->m_mockSystem); // Reroute all the mocked calls as necessary EXPECT_CALL(m_data->m_mockSystem, GetI3DEngine()).WillRepeatedly(testing::Return(&(m_data->m_mock3DEngine))); EXPECT_CALL(m_data->m_mock3DEngine, GetIMergedMeshesManager()).WillRepeatedly(testing::Return(&(m_data->m_mockMergedMeshesManager))); EXPECT_CALL(m_data->m_mock3DEngine, SetStatInstGroup(testing::_, testing::_, testing::_)).WillRepeatedly(testing::Return(true)); EXPECT_CALL(m_data->m_mockMergedMeshesManager, AddDynamicInstance(testing::_, testing::_, testing::_)).WillRepeatedly(testing::Return(&(m_data->m_mockRenderNode))); // Create a real Asset Mananger, and point to ourselves as the handler for MeshAsset. AZ::AllocatorInstance::Create(); AZ::AllocatorInstance::Create(); AZ::Data::AssetManager::Descriptor descriptor; descriptor.m_maxWorkerThreads = 1; AZ::Data::AssetManager::Create(descriptor); AZ::Data::AssetManager::Instance().RegisterHandler(this, AZ::AzTypeInfo::Uuid()); AZ::Data::AssetManager::Instance().RegisterCatalog(this, AZ::AzTypeInfo::Uuid()); // Intercept messages for finding assets by name and creating/destroying slices. AZ::Data::AssetCatalogRequestBus::Handler::BusConnect(); // Intercept messages for generating / releasing stat group IDs StatInstGroupEventBus::Handler::BusConnect(); } void TearDown() override { StatInstGroupEventBus::Handler::BusDisconnect(); AZ::Data::AssetCatalogRequestBus::Handler::BusDisconnect(); AZ::Data::AssetManager::Instance().UnregisterCatalog(this); AZ::Data::AssetManager::Instance().UnregisterHandler(this); AZ::Data::AssetManager::Destroy(); AZ::AllocatorInstance::Destroy(); AZ::AllocatorInstance::Destroy(); // Remove the other mocked systems m_data.reset(); // restore the global state that we modified during the test. gEnv = m_savedEnv; 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::LegacyVegetationInstanceSpawner& instanceSpawner, AZ::Data::AssetId assetId, AZStd::string assetPath) { // Save these off for use from our mock AssetCatalogRequestBus m_assetId = assetId; m_assetPath = assetPath; // Mark our mock asset with the correct id and call it ready. reinterpret_cast(&m_meshAsset)->SetId(m_assetId); Vegetation::DescriptorNotificationBus::Handler::BusConnect(&instanceSpawner); // Tell the spawner to use this asset. This will trigger a LoadAssets() call inside of it. instanceSpawner.SetMeshAssetPath(m_assetPath); // Our instance spawner should now have a valid asset reference, // but not be loaded or spawnable yet. EXPECT_FALSE(instanceSpawner.HasEmptyAssetReferences()); EXPECT_FALSE(instanceSpawner.IsLoaded()); EXPECT_FALSE(instanceSpawner.IsSpawnable()); // Since this is going through the real AssetManager, there's a delay while a separate // job thread executes and actually loads our mock mesh 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 Mesh has been created/loaded/destroyed successfully AZ::Data::AssetPtr CreateAsset(const AZ::Data::AssetId& id, const AZ::Data::AssetType& type) override { auto assetPtr = new LmbrCentral::MeshAsset(); assetPtr->m_statObj = new MockStatObj(); return assetPtr; } void DestroyAsset(AZ::Data::AssetPtr ptr) override { delete ptr; } void GetHandledAssetTypes(AZStd::vector& assetTypes) override { assetTypes.push_back(AZ::AzTypeInfo::Uuid()); } bool LoadAssetData(const AZ::Data::Asset& asset, const char* assetPath, const AZ::Data::AssetFilterCB& assetLoadFilterCB) { return true; } // DescriptorNotificationBus // Keep track of whether or not the Spawner successfully loaded the asset and notified listeners void OnDescriptorAssetsLoaded() override { m_numOnLoadedCalls++; } // StatInstGroupEventBus // Minimalist mocks to generate a stat group id for our mock vegetation object StatInstGroupId GenerateStatInstGroupId() override { return 1; } void ReleaseStatInstGroupId(StatInstGroupId statInstGroupId) override {} void ReleaseStatInstGroupIdSet(const AZStd::unordered_set& statInstGroupIdSet) override {} void ReserveStatInstGroupIdRange(StatInstGroupId from, StatInstGroupId to) override {} // 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::Uuid(); assetInfo.m_relativePath = m_assetPath; return assetInfo; } // AssetCatalog // Minimalist mock to pretend like we've loaded a Mesh asset AZ::Data::AssetStreamInfo GetStreamInfoForLoad(const AZ::Data::AssetId& id, const AZ::Data::AssetType& type) override { EXPECT_TRUE(type == AZ::AzTypeInfo::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; LmbrCentral::MeshAsset m_meshAsset; int m_numOnLoadedCalls = 0; private: struct DataMembers { SSystemGlobalEnvironment m_mockGEnv; ::testing::NiceMock m_mock3DEngine; ::testing::NiceMock m_mockSystem; ::testing::NiceMock m_mockMergedMeshesManager; MockRenderNode m_mockRenderNode; }; AZStd::unique_ptr m_data; SSystemGlobalEnvironment* m_savedEnv = nullptr; }; TEST_F(LegacyVegetationInstanceSpawnerTests, BasicInitializationTest) { // Basic test to make sure we can construct / destroy without errors. Vegetation::LegacyVegetationInstanceSpawner instanceSpawner; } TEST_F(LegacyVegetationInstanceSpawnerTests, DefaultSpawnersAreEqual) { // Two different instances of the default LegacyVegetationInstanceSpawner should // be considered data-equivalent. Vegetation::LegacyVegetationInstanceSpawner instanceSpawner1; Vegetation::LegacyVegetationInstanceSpawner instanceSpawner2; EXPECT_TRUE(instanceSpawner1 == instanceSpawner2); } // [LY-118267] Any test using CreateAndSetMockAsset can intermittently fail, so disabling them for now. TEST_F(LegacyVegetationInstanceSpawnerTests, DISABLED_DifferentSpawnersAreNotEqual) { // Two spawners with different data should *not* be data-equivalent. Vegetation::LegacyVegetationInstanceSpawner instanceSpawner1; Vegetation::LegacyVegetationInstanceSpawner 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] Any test using CreateAndSetMockAsset can intermittently fail, so disabling them for now. TEST_F(LegacyVegetationInstanceSpawnerTests, DISABLED_LoadAndUnloadAssets) { // The spawner should successfully load/unload assets without errors. Vegetation::LegacyVegetationInstanceSpawner instanceSpawner; // Our instance spawner should be empty before we set the assets. EXPECT_TRUE(instanceSpawner.HasEmptyAssetReferences()); // This will create and load an asset. CreateAndSetMockAsset(instanceSpawner, AZ::Uuid::CreateRandom(), "test"); // Unload, then it should stop being loaded and spawnable. Vegetation::DescriptorNotificationBus::Handler::BusConnect(&instanceSpawner); instanceSpawner.UnloadAssets(); EXPECT_FALSE(instanceSpawner.IsLoaded()); EXPECT_FALSE(instanceSpawner.IsSpawnable()); Vegetation::DescriptorNotificationBus::Handler::BusDisconnect(); } // [LY-118267] This test intermittently fails (potentially only with uber builds) with the following backrtrace: // dev\code\cryengine\crycommon\Cry_Vector3.h(124): error: this->IsValid() // Temporarily disabling this test until the root cause can be found. TEST_F(LegacyVegetationInstanceSpawnerTests, DISABLED_CreateAndDestroyInstance) { // The spawner should successfully create and destroy an instance without errors. Vegetation::LegacyVegetationInstanceSpawner instanceSpawner; Vegetation::DescriptorNotificationBus::Handler::BusConnect(&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(); instanceSpawner.UnloadAssets(); Vegetation::DescriptorNotificationBus::Handler::BusDisconnect(); } TEST_F(LegacyVegetationInstanceSpawnerTests, SpawnerRegisteredWithDescriptor) { // Validate that the Descriptor successfully gets LegacyVegetationInstanceSpawner registered with it, // as long as InstanceSpawner and LegacyVegetationInstanceSpawner have been reflected. MockLegacyVegetationInstanceVegetationSystemComponent* component = nullptr; auto entity = CreateEntity(&component); Vegetation::Descriptor descriptor; descriptor.RefreshSpawnerTypeList(); auto spawnerTypes = descriptor.GetSpawnerTypeList(); // Our unit test harness reflects both Empty and Legacy Vegetation spawners. // They should come back in a deterministic alpha-sorted order. EXPECT_TRUE(spawnerTypes.size() == 2); EXPECT_TRUE(spawnerTypes[0].first == Vegetation::EmptyInstanceSpawner::RTTI_Type()); EXPECT_TRUE(spawnerTypes[1].first == Vegetation::LegacyVegetationInstanceSpawner::RTTI_Type()); } TEST_F(LegacyVegetationInstanceSpawnerTests, DescriptorCreatesCorrectSpawner) { // Validate that the Descriptor successfully creates a new LegacyVegetationInstanceSpawner as the default // and when we change the spawner type on the Descriptor. MockLegacyVegetationInstanceVegetationSystemComponent* component = nullptr; auto entity = CreateEntity(&component); // We expect the Descriptor to start off with a Legacy Vegetation spawner, so verify it created one initially. Vegetation::Descriptor descriptor; EXPECT_TRUE(azrtti_typeid(*(descriptor.GetInstanceSpawner())) == Vegetation::LegacyVegetationInstanceSpawner::RTTI_Type()); descriptor.RefreshSpawnerTypeList(); // Change it to Empty spawner to "cleanse the palette" descriptor.m_spawnerType = Vegetation::EmptyInstanceSpawner::RTTI_Type(); descriptor.SpawnerTypeChanged(); EXPECT_TRUE(azrtti_typeid(*(descriptor.GetInstanceSpawner())) == Vegetation::EmptyInstanceSpawner::RTTI_Type()); // Change it back to Legacy Vegetation to verify that changing the type successfully creates one. descriptor.m_spawnerType = Vegetation::LegacyVegetationInstanceSpawner::RTTI_Type(); descriptor.SpawnerTypeChanged(); EXPECT_TRUE(azrtti_typeid(*(descriptor.GetInstanceSpawner())) == Vegetation::LegacyVegetationInstanceSpawner::RTTI_Type()); } }