Files
o3de/Code/Framework/AzToolsFramework/Tests/ComponentAddRemove.cpp
T
lumberyard-employee-dm 5fc4551ac0 [LYN-8041] Enable relocation of the Project Game Release Layout (#5380)
* Enable relocation of the Project Game Release Layout

Relocating the Project Game Release Layout to another directory on the file system failed due to the querying of the engine root failing due to the ComponentApplication::m_engineRoot not using the project path stored in the SettingsRegisry if the engine root cannot be detected

Removed the ApplicationRequestBus GetEngineRoot function.
The ComponentApplicationRequestBus has a function of the same name that returns the same path.

Removed the deprecated GetAppRoot function.
The path it returns has no defined value. It was not the engine root or the project root.
Removed unused CFileUtil and CFileUtil_impl functions that were invoking the ApplicationREquestBus GetEngineRoot function.
On the way to update the functions it was discovered that they aren't called

Added a CalculateBranchToken overload that can populate a fixed_string to avoid heap allocations

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

* Protect against an empty list of artifacts to remove when generating the
engine.pak

Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>
2021-11-09 12:03:52 -06:00

1449 lines
70 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/Outcome/Outcome.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzCore/UserSettings/UserSettingsComponent.h>
#include <AzToolsFramework/API/EntityCompositionNotificationBus.h>
#include <AzToolsFramework/API/EntityCompositionRequestBus.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <AzToolsFramework/Application/ToolsApplication.h>
#include <AzToolsFramework/Entity/EditorEntityActionComponent.h>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
#include <AzToolsFramework/ToolsComponents/EditorComponentBase.h>
#include <AzToolsFramework/ToolsComponents/EditorDisabledCompositionBus.h>
#include <AzToolsFramework/ToolsComponents/EditorPendingCompositionBus.h>
#include <AzToolsFramework/ToolsComponents/EditorPendingCompositionComponent.h>
#include <AzToolsFramework/ToolsComponents/GenericComponentWrapper.h>
namespace UnitTest
{
using namespace AZ;
using namespace AzToolsFramework;
using namespace AZ::Data;
using namespace AZ::IO;
//
// Declaring several clothing-themed components for use in tests.
//
// Shoes require socks
class LeatherBootsComponent
: public AZ::Component
{
public:
AZ_COMPONENT(LeatherBootsComponent, "{C2852908-0FC6-4BF6-9907-E390840F9897}");
void Activate() override {}
void Deactivate() override {}
static void Reflect(AZ::ReflectContext* reflection)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(reflection))
{
serializeContext->Class<LeatherBootsComponent, AZ::Component>();
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
{
editContext->Class<LeatherBootsComponent>("Leather Boots", "")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game"));
}
}
}
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
{
provided.push_back(AZ_CRC("ShoesService", 0xaa20aadf));
}
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
{
incompatible.push_back(AZ_CRC("ShoesService", 0xaa20aadf));
}
static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required)
{
required.push_back(AZ_CRC("SocksService", 0x51e58440));
}
};
// Note that WoolSocksComponent is an "editor component".
// This is just to make sure we are testing with both "editor"
// and "non-editor" components.
class WoolSocksComponent
: public AzToolsFramework::Components::EditorComponentBase
{
public:
AZ_COMPONENT(WoolSocksComponent, "{6436A9A1-701E-4275-AF6F-82F53C7916C8}", EditorComponentBase);
void Activate() override {}
void Deactivate() override {}
static void Reflect(AZ::ReflectContext* reflection)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(reflection))
{
serializeContext->Class<WoolSocksComponent, EditorComponentBase>();
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
{
editContext->Class<WoolSocksComponent>("Wool Socks", "")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game"));
}
}
}
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
{
provided.push_back(AZ_CRC("SocksService", 0x51e58440));
}
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
{
incompatible.push_back(AZ_CRC("SocksService", 0x51e58440));
}
static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& /*required*/)
{
}
};
// Incompatible with socks
class HatesSocksComponent
: public AZ::Component
{
public:
AZ_COMPONENT(HatesSocksComponent, "{D359D446-A172-4854-8EA9-B95073FF5709}");
void Activate() override {}
void Deactivate() override {}
static void Reflect(AZ::ReflectContext* reflection)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(reflection))
{
serializeContext->Class<HatesSocksComponent, AZ::Component>();
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
{
editContext->Class<HatesSocksComponent>("Hates Socks", "")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game"));
}
}
}
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& /*provided*/)
{
}
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
{
incompatible.push_back(AZ_CRC("SocksService", 0x51e58440));
}
static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& /*required*/)
{
}
};
// Pants require underwear
class BlueJeansComponent
: public AZ::Component
{
public:
AZ_COMPONENT(BlueJeansComponent, "{AEA4D69E-F02B-4F6D-A793-8DEE0C0E54E3}");
void Activate() override {}
void Deactivate() override {}
static void Reflect(AZ::ReflectContext* reflection)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(reflection))
{
serializeContext->Class<BlueJeansComponent, AZ::Component>();
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
{
editContext->Class<BlueJeansComponent>("Blue Jeans", "")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game"));
}
}
}
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
{
provided.push_back(AZ_CRC("TrousersService", 0x15edf105));
}
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
{
incompatible.push_back(AZ_CRC("TrousersService", 0x15edf105));
}
static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required)
{
required.push_back(AZ_CRC("UnderwearService", 0x915ec03a));
}
};
// 1 of 2 underwear styles
class WhiteBriefsComponent
: public AZ::Component
{
public:
AZ_COMPONENT(WhiteBriefsComponent, "{8B095E11-082B-4EB1-A119-D1534323C956}");
void Activate() override {}
void Deactivate() override {}
static void Reflect(AZ::ReflectContext* reflection)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(reflection))
{
serializeContext->Class<WhiteBriefsComponent, AZ::Component>();
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
{
editContext->Class<WhiteBriefsComponent>("White Briefs", "")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game"));
}
}
}
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
{
provided.push_back(AZ_CRC("UnderwearService", 0x915ec03a));
}
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
{
incompatible.push_back(AZ_CRC("UnderwearService", 0x915ec03a));
}
static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& /*required*/)
{
}
};
// 2 of 2 underwear styles
class HeartBoxersComponent
: public AZ::Component
{
public:
AZ_COMPONENT(HeartBoxersComponent, "{06071955-CC65-4C32-A4D8-1125D827C10B}");
void Activate() override {}
void Deactivate() override {}
static void Reflect(AZ::ReflectContext* reflection)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(reflection))
{
serializeContext->Class<HeartBoxersComponent, AZ::Component>();
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
{
editContext->Class<HeartBoxersComponent>("Heart Boxers", "")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game"));
}
}
}
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
{
provided.push_back(AZ_CRC("UnderwearService", 0x915ec03a));
}
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
{
incompatible.push_back(AZ_CRC("UnderwearService", 0x915ec03a));
}
static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& /*required*/)
{
}
};
// Requires a belt (but no belt exists)
class KnifeSheathComponent
: public AZ::Component
{
public:
AZ_COMPONENT(KnifeSheathComponent, "{D99C3EF1-592F-4744-9D07-A5F2CE679870}");
void Activate() override {}
void Deactivate() override {}
static void Reflect(AZ::ReflectContext* reflection)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(reflection))
{
serializeContext->Class<KnifeSheathComponent, AZ::Component>();
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
{
editContext->Class<KnifeSheathComponent>("Knife Sheath", "")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game"));
}
}
}
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& /*provided*/)
{
}
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& /*incompatible*/)
{
}
static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required)
{
required.push_back(AZ_CRC("BeltService", 0xba4df957));
}
};
// Count components of given type
template <typename ComponentType>
size_t CountComponentsOnEntity(AZ::Entity* entity)
{
EXPECT_NE(nullptr, entity);
if (!entity)
{
return 0;
}
size_t count = 0;
for (const auto component : entity->GetComponents())
{
if (AzToolsFramework::GetUnderlyingComponentType(*component) == azrtti_typeid<ComponentType>())
{
++count;
}
}
return count;
}
template <typename ComponentType>
size_t CountPendingComponentsOnEntity(AZ::Entity* entity)
{
EXPECT_NE(nullptr, entity);
if (!entity)
{
return 0;
}
AZ::Entity::ComponentArrayType pendingComponents;
AzToolsFramework::EditorPendingCompositionRequestBus::Event(entity->GetId(), &AzToolsFramework::EditorPendingCompositionRequestBus::Events::GetPendingComponents, pendingComponents);
size_t count = 0;
for (const auto pendingComponent : pendingComponents)
{
if (AzToolsFramework::GetUnderlyingComponentType(*pendingComponent) == azrtti_typeid<ComponentType>())
{
++count;
}
}
return count;
}
template <typename ComponentType>
size_t CountDisabledComponentsOnEntity(AZ::Entity* entity)
{
EXPECT_NE(nullptr, entity);
if (!entity)
{
return 0;
}
AZ::Entity::ComponentArrayType disabledComponents;
AzToolsFramework::EditorDisabledCompositionRequestBus::Event(entity->GetId(), &AzToolsFramework::EditorDisabledCompositionRequestBus::Events::GetDisabledComponents, disabledComponents);
size_t count = 0;
for (const auto disabledComponent : disabledComponents)
{
if (AzToolsFramework::GetUnderlyingComponentType(*disabledComponent) == azrtti_typeid<ComponentType>())
{
++count;
}
}
return count;
}
template <typename ComponentType>
AZStd::vector<AZ::Component*> GetComponentsForEntity(AZ::Entity* entity)
{
AZStd::vector<AZ::Component*> components;
AzToolsFramework::GetAllComponentsForEntity(entity, components);
auto itr = AZStd::remove_if(components.begin(), components.end(), [](AZ::Component* component) {return AzToolsFramework::GetUnderlyingComponentType(*component) != azrtti_typeid<ComponentType>();});
components.erase(itr, components.end());
return components;
}
bool CheckAllAreTrue(std::initializer_list<bool> booleans)
{
for (auto boolean : booleans)
{
if (!boolean)
{
return false;
}
}
return true;
}
template <typename... BooleanTypes>
bool CheckAllAreTrue(BooleanTypes... booleans)
{
return CheckAllAreTrue({booleans...});
}
bool DoesComponentListHaveComponent(const AZ::Entity::ComponentArrayType& componentList, const AZ::Uuid& componentType)
{
for (const auto component : componentList)
{
if (AzToolsFramework::GetUnderlyingComponentType(*component) == componentType)
{
return true;
}
}
return false;
}
template <typename... AdditionalValidatedComponentTypes>
struct VerifyAdditionalValidatedComponents
{
static bool OnOutcomeForEntity(const AzToolsFramework::EntityCompositionRequestBus::Events::AddComponentsOutcome& outcome, AZ::Entity* entity)
{
auto iterEntityOutcome = outcome.GetValue().find(entity->GetId());
EXPECT_NE(iterEntityOutcome, outcome.GetValue().end());
return CheckAllAreTrue(DoesComponentListHaveComponent(iterEntityOutcome->second.m_additionalValidatedComponents, azrtti_typeid<AdditionalValidatedComponentTypes>())...);
}
};
template <typename... AddedPendingComponentTypes>
struct VerifyAddedPendingComponents
{
static bool OnOutcomeForEntity(const AzToolsFramework::EntityCompositionRequestBus::Events::AddComponentsOutcome& outcome, AZ::Entity* entity)
{
auto iterEntityOutcome = outcome.GetValue().find(entity->GetId());
EXPECT_NE(iterEntityOutcome, outcome.GetValue().end());
return CheckAllAreTrue(DoesComponentListHaveComponent(iterEntityOutcome->second.m_addedPendingComponents, azrtti_typeid<AddedPendingComponentTypes>())...);
}
};
template <typename... AddedValidComponentTypes>
struct VerifyAddedValidComponents
{
static bool OnOutcomeForEntity(const AzToolsFramework::EntityCompositionRequestBus::Events::AddComponentsOutcome& outcome, AZ::Entity* entity)
{
auto iterEntityOutcome = outcome.GetValue().find(entity->GetId());
EXPECT_NE(iterEntityOutcome, outcome.GetValue().end());
return CheckAllAreTrue(DoesComponentListHaveComponent(iterEntityOutcome->second.m_addedValidComponents, azrtti_typeid<AddedValidComponentTypes>())...);
}
template <typename... AdditionalValidatedComponentTypes>
struct AndAdditionalValidatedComponents
{
static bool OnOutcomeForEntity(const AzToolsFramework::EntityCompositionRequestBus::Events::AddComponentsOutcome& outcome, AZ::Entity* entity)
{
return CheckAllAreTrue(
VerifyAddedValidComponents<AddedValidComponentTypes...>::OnOutcomeForEntity(outcome, entity),
VerifyAdditionalValidatedComponents<AdditionalValidatedComponentTypes...>::OnOutcomeForEntity(outcome, entity)
);
}
};
template <typename... AddedPendingComponentTypes>
struct AndAddedPendingComponents
{
static bool OnOutcomeForEntity(const AzToolsFramework::EntityCompositionRequestBus::Events::AddComponentsOutcome& outcome, AZ::Entity* entity)
{
return CheckAllAreTrue(
VerifyAddedValidComponents<AddedValidComponentTypes...>::OnOutcomeForEntity(outcome, entity),
VerifyAddedPendingComponents<AddedPendingComponentTypes...>::OnOutcomeForEntity(outcome, entity)
);
}
template <typename... AdditionalValidatedComponentTypes>
struct AndAdditionalValidatedComponents
{
static bool OnOutcomeForEntity(const AzToolsFramework::EntityCompositionRequestBus::Events::AddComponentsOutcome& outcome, AZ::Entity* entity)
{
return CheckAllAreTrue(
VerifyAddedValidComponents<AddedValidComponentTypes...>::OnOutcomeForEntity(outcome, entity),
VerifyAddedPendingComponents<AddedPendingComponentTypes...>::OnOutcomeForEntity(outcome, entity),
VerifyAdditionalValidatedComponents<AdditionalValidatedComponentTypes...>::OnOutcomeForEntity(outcome, entity)
);
}
};
};
};
template <typename... InvalidatedComponentTypes>
struct VerifyRemovalInvalidatedComponents
{
static bool OnOutcomeForEntity(const AzToolsFramework::EntityCompositionRequestBus::Events::RemoveComponentsOutcome& outcome, AZ::Entity* entity)
{
auto iterEntityOutcome = outcome.GetValue().find(entity->GetId());
EXPECT_NE(iterEntityOutcome, outcome.GetValue().end());
return CheckAllAreTrue(DoesComponentListHaveComponent(iterEntityOutcome->second.m_invalidatedComponents, azrtti_typeid<InvalidatedComponentTypes>())...);
}
};
template <typename... ValidatedComponentTypes>
struct VerifyRemovalValidatedComponents
{
static bool OnOutcomeForEntity(const AzToolsFramework::EntityCompositionRequestBus::Events::RemoveComponentsOutcome& outcome, AZ::Entity* entity)
{
auto iterEntityOutcome = outcome.GetValue().find(entity->GetId());
EXPECT_NE(iterEntityOutcome, outcome.GetValue().end());
return CheckAllAreTrue(DoesComponentListHaveComponent(iterEntityOutcome->second.m_validatedComponents, azrtti_typeid<ValidatedComponentTypes>())...);
}
template <typename... InvalidatedComponentTypes>
struct AndInvalidatedComponents
{
static bool OnOutcomeForEntity(const AzToolsFramework::EntityCompositionRequestBus::Events::RemoveComponentsOutcome& outcome, AZ::Entity* entity)
{
return CheckAllAreTrue(
VerifyRemovalValidatedComponents<ValidatedComponentTypes...>::OnOutcomeForEntity(outcome, entity),
VerifyRemovalInvalidatedComponents<InvalidatedComponentTypes...>::OnOutcomeForEntity(outcome, entity)
);
}
};
};
class EntityComponentCounter
{
public:
void SetEntity(const AZ::Entity* entity)
{
m_entity = entity;
}
size_t GetCount() const
{
if (!m_entity)
{
return 0;
}
return GetComponentCount() - m_lastResetCount;
}
size_t GetPendingCount() const
{
if (!m_entity)
{
return 0;
}
return GetPendingComponentCount() - m_lastPendingResetCount;
}
size_t GetDisabledCount() const
{
if (!m_entity)
{
return 0;
}
return GetDisabledComponentCount() - m_lastDisabledResetCount;
}
void Reset()
{
m_lastResetCount = GetComponentCount();
m_lastPendingResetCount = GetPendingComponentCount();
m_lastDisabledResetCount = GetDisabledComponentCount();
}
private:
size_t GetComponentCount() const
{
return m_entity->GetComponents().size();
}
size_t GetPendingComponentCount() const
{
AZ::Entity::ComponentArrayType pendingComponents;
AzToolsFramework::EditorPendingCompositionRequestBus::Event(m_entity->GetId(), &AzToolsFramework::EditorPendingCompositionRequestBus::Events::GetPendingComponents, pendingComponents);
return pendingComponents.size();
}
size_t GetDisabledComponentCount() const
{
AZ::Entity::ComponentArrayType disabledComponents;
AzToolsFramework::EditorDisabledCompositionRequestBus::Event(m_entity->GetId(), &AzToolsFramework::EditorDisabledCompositionRequestBus::Events::GetDisabledComponents, disabledComponents);
return disabledComponents.size();
}
size_t m_lastResetCount = 0;
size_t m_lastPendingResetCount = 0;
size_t m_lastDisabledResetCount = 0;
const AZ::Entity* m_entity;
};
class AddComponentsTest
: public AllocatorsTestFixture
{
public:
void SetUp() override
{
AllocatorsTestFixture::SetUp();
AZ::SettingsRegistryInterface* registry = AZ::SettingsRegistry::Get();
auto projectPathKey =
AZ::SettingsRegistryInterface::FixedValueString(AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey) + "/project_path";
AZ::IO::FixedMaxPath enginePath;
registry->Get(enginePath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder);
registry->Set(projectPathKey, (enginePath / "AutomatedTesting").Native());
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(*registry);
AzFramework::Application::Descriptor descriptor;
descriptor.m_enableDrilling = false;
m_app.Start(descriptor);
// Without this, the user settings component would attempt to save on finalize/shutdown. Since the file is
// shared across the whole engine, if multiple tests are run in parallel, the saving could cause a crash
// in the unit tests.
AZ::UserSettingsComponentRequestBus::Broadcast(&AZ::UserSettingsComponentRequests::DisableSaveOnFinalize);
m_app.RegisterComponentDescriptor(LeatherBootsComponent::CreateDescriptor());
m_app.RegisterComponentDescriptor(WoolSocksComponent::CreateDescriptor());
m_app.RegisterComponentDescriptor(HatesSocksComponent::CreateDescriptor());
m_app.RegisterComponentDescriptor(BlueJeansComponent::CreateDescriptor());
m_app.RegisterComponentDescriptor(WhiteBriefsComponent::CreateDescriptor());
m_app.RegisterComponentDescriptor(HeartBoxersComponent::CreateDescriptor());
m_app.RegisterComponentDescriptor(KnifeSheathComponent::CreateDescriptor());
m_entity1 = new AZ::Entity("Entity1");
m_entity1Counter.SetEntity(m_entity1);
AzToolsFramework::EditorEntityContextRequestBus::Broadcast(&AzToolsFramework::EditorEntityContextRequestBus::Events::AddRequiredComponents, *m_entity1);
m_entity1->Init();
m_entity1Counter.Reset();
m_entity2 = new AZ::Entity("Entity2");
m_entity2Counter.SetEntity(m_entity2);
AzToolsFramework::EditorEntityContextRequestBus::Broadcast(&AzToolsFramework::EditorEntityContextRequestBus::Events::AddRequiredComponents, *m_entity2);
m_entity2->Init();
m_entity2Counter.Reset();
}
void TearDown() override
{
m_app.Stop();
AllocatorsTestFixture::TearDown();
}
AzToolsFramework::ToolsApplication m_app;
AZ::Entity* m_entity1 = nullptr;
EntityComponentCounter m_entity1Counter;
AZ::Entity* m_entity2 = nullptr;
EntityComponentCounter m_entity2Counter;
size_t m_initialComponentCount = 0;
};
TEST_F(AddComponentsTest, AddOneComponentToOneEntity)
{
auto outcome = AzToolsFramework::AddComponents<WoolSocksComponent>::ToEntities(m_entity1);
// Verify success
ASSERT_TRUE(outcome.IsSuccess());
// Check that the returned result was what we expected
ASSERT_TRUE(VerifyAddedValidComponents<WoolSocksComponent>::OnOutcomeForEntity(outcome, m_entity1));
// Check that we have the component added as expected
ASSERT_EQ(1, CountComponentsOnEntity<WoolSocksComponent>(m_entity1));
// We do separate count checks in case some other random components were added or something unexpected occurred
ASSERT_EQ(1, m_entity1Counter.GetCount());
// Verify nothing is pending
ASSERT_EQ(0, CountPendingComponentsOnEntity<WoolSocksComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetPendingCount());
// Verify nothing is disabled
ASSERT_EQ(0, CountDisabledComponentsOnEntity<WoolSocksComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetDisabledCount());
auto addComponentsResults = outcome.GetValue()[m_entity1->GetId()];
AzToolsFramework::DisableComponents(addComponentsResults.m_addedValidComponents);
ASSERT_EQ(0, CountComponentsOnEntity<WoolSocksComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetCount());
ASSERT_EQ(0, CountPendingComponentsOnEntity<WoolSocksComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetPendingCount());
ASSERT_EQ(1, CountDisabledComponentsOnEntity<WoolSocksComponent>(m_entity1));
ASSERT_EQ(1, m_entity1Counter.GetDisabledCount());
AzToolsFramework::EnableComponents(addComponentsResults.m_addedValidComponents);
ASSERT_EQ(1, CountComponentsOnEntity<WoolSocksComponent>(m_entity1));
ASSERT_EQ(1, m_entity1Counter.GetCount());
ASSERT_EQ(0, CountPendingComponentsOnEntity<WoolSocksComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetPendingCount());
ASSERT_EQ(0, CountDisabledComponentsOnEntity<WoolSocksComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetDisabledCount());
}
TEST_F(AddComponentsTest, AddOneComponentToMultipleEntities)
{
// have one entity activated, we must ensure that it is still activated after add operation
m_entity1->Activate();
// add a component to both the activated and inactive entities
auto outcome = AzToolsFramework::AddComponents<WoolSocksComponent>::ToEntities(m_entity1, m_entity2);
// Verify outcome
ASSERT_TRUE(outcome.IsSuccess());
ASSERT_TRUE(VerifyAddedValidComponents<WoolSocksComponent>::OnOutcomeForEntity(outcome, m_entity1));
ASSERT_TRUE(VerifyAddedValidComponents<WoolSocksComponent>::OnOutcomeForEntity(outcome, m_entity2));
// Should always be on entity, not pending since services are met
ASSERT_EQ(1, CountComponentsOnEntity<WoolSocksComponent>(m_entity1));
ASSERT_EQ(1, CountComponentsOnEntity<WoolSocksComponent>(m_entity2));
ASSERT_EQ(1, m_entity1Counter.GetCount());
ASSERT_EQ(1, m_entity2Counter.GetCount());
// Nothing is pending
ASSERT_EQ(0, CountPendingComponentsOnEntity<WoolSocksComponent>(m_entity1));
ASSERT_EQ(0, CountPendingComponentsOnEntity<WoolSocksComponent>(m_entity2));
ASSERT_EQ(0, m_entity1Counter.GetPendingCount());
ASSERT_EQ(0, m_entity2Counter.GetPendingCount());
// Nothing is disabled
ASSERT_EQ(0, CountDisabledComponentsOnEntity<WoolSocksComponent>(m_entity1));
ASSERT_EQ(0, CountDisabledComponentsOnEntity<WoolSocksComponent>(m_entity2));
ASSERT_EQ(0, m_entity1Counter.GetDisabledCount());
ASSERT_EQ(0, m_entity2Counter.GetDisabledCount());
// Still in original states
ASSERT_EQ(AZ::Entity::State::Active, m_entity1->GetState());
ASSERT_EQ(AZ::Entity::State::Init, m_entity2->GetState());
}
// Add a component which requires another component
TEST_F(AddComponentsTest, ComponentRequiresService)
{
auto outcome = AzToolsFramework::AddComponents<LeatherBootsComponent>::ToEntities(m_entity1);
// Verify outcome
ASSERT_TRUE(outcome.IsSuccess());
ASSERT_TRUE(VerifyAddedPendingComponents<LeatherBootsComponent>::OnOutcomeForEntity(outcome, m_entity1));
// This will be pending since it is missing a socks service
ASSERT_EQ(0, CountComponentsOnEntity<LeatherBootsComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetCount());
ASSERT_EQ(1, CountPendingComponentsOnEntity<LeatherBootsComponent>(m_entity1));
ASSERT_EQ(1, m_entity1Counter.GetPendingCount());
ASSERT_EQ(0, CountDisabledComponentsOnEntity<LeatherBootsComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetDisabledCount());
// Satisfy the pending component with wool socks
outcome = AzToolsFramework::AddComponents<WoolSocksComponent>::ToEntities(m_entity1);
ASSERT_TRUE(outcome.IsSuccess());
ASSERT_TRUE(VerifyAddedValidComponents<WoolSocksComponent>::AndAdditionalValidatedComponents<LeatherBootsComponent>::OnOutcomeForEntity(outcome, m_entity1));
// Should have both on the entity now and no pending
ASSERT_EQ(1, CountComponentsOnEntity<LeatherBootsComponent>(m_entity1));
ASSERT_EQ(1, CountComponentsOnEntity<WoolSocksComponent>(m_entity1));
ASSERT_EQ(2, m_entity1Counter.GetCount());
ASSERT_EQ(0, CountPendingComponentsOnEntity<LeatherBootsComponent>(m_entity1));
ASSERT_EQ(0, CountPendingComponentsOnEntity<WoolSocksComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetPendingCount());
ASSERT_EQ(0, CountDisabledComponentsOnEntity<LeatherBootsComponent>(m_entity1));
ASSERT_EQ(0, CountDisabledComponentsOnEntity<WoolSocksComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetDisabledCount());
// LeatherBootsComponent should be wrapped in a GenericComponentWrapper
// because it is not an "editor component".
auto* wrapper = m_entity1->FindComponent<AzToolsFramework::Components::GenericComponentWrapper>();
ASSERT_TRUE(wrapper && wrapper->GetTemplate());
ASSERT_EQ(azrtti_typeid<LeatherBootsComponent>(), azrtti_typeid(wrapper->GetTemplate()));
// Try adding a component which requires a service
// that no other component provides.
outcome = AzToolsFramework::AddComponents<KnifeSheathComponent>::ToEntities(m_entity2);
ASSERT_TRUE(outcome.IsSuccess());
ASSERT_TRUE(VerifyAddedPendingComponents<KnifeSheathComponent>::OnOutcomeForEntity(outcome, m_entity2));
// This one will always be pending, never on entity as it will never be satisfied
ASSERT_EQ(0, CountComponentsOnEntity<KnifeSheathComponent>(m_entity2));
ASSERT_EQ(0, m_entity2Counter.GetCount());
ASSERT_EQ(1, CountPendingComponentsOnEntity<KnifeSheathComponent>(m_entity2));
ASSERT_EQ(1, m_entity2Counter.GetPendingCount());
ASSERT_EQ(0, CountDisabledComponentsOnEntity<KnifeSheathComponent>(m_entity2));
ASSERT_EQ(0, m_entity2Counter.GetDisabledCount());
// Check pending status
auto component = outcome.GetValue()[m_entity2->GetId()].m_addedPendingComponents[0];
AzToolsFramework::EntityCompositionRequestBus::Events::PendingComponentInfo pendingComponentInfo;
AzToolsFramework::EntityCompositionRequestBus::BroadcastResult(pendingComponentInfo, &AzToolsFramework::EntityCompositionRequestBus::Events::GetPendingComponentInfo, component);
// Should have one missing service
ASSERT_EQ(pendingComponentInfo.m_missingRequiredServices.size(), 1);
ASSERT_EQ(pendingComponentInfo.m_validComponentsThatAreIncompatible.size(), 0);
ASSERT_EQ(pendingComponentInfo.m_pendingComponentsWithRequiredServices.size(), 0);
// And that missing service should be the BeltService
ASSERT_EQ(pendingComponentInfo.m_missingRequiredServices[0], AZ_CRC("BeltService"));
// Entity 1 should remain untouched
ASSERT_EQ(2, m_entity1Counter.GetCount());
ASSERT_EQ(0, m_entity1Counter.GetPendingCount());
ASSERT_EQ(0, m_entity1Counter.GetDisabledCount());
}
// Add a component (jeans) which requires a service (underwear),
// and there are two viable options (boxers or briefs).
TEST_F(AddComponentsTest, ComponentRequiresServiceWithTwoViableOptions)
{
auto outcome = AzToolsFramework::AddComponents<BlueJeansComponent>::ToEntities(m_entity1);
ASSERT_TRUE(outcome.IsSuccess());
ASSERT_TRUE(VerifyAddedPendingComponents<BlueJeansComponent>::OnOutcomeForEntity(outcome, m_entity1));
ASSERT_EQ(0, CountComponentsOnEntity<BlueJeansComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetCount());
ASSERT_EQ(1, CountPendingComponentsOnEntity<BlueJeansComponent>(m_entity1));
ASSERT_EQ(1, m_entity1Counter.GetPendingCount());
ASSERT_EQ(0, CountDisabledComponentsOnEntity<BlueJeansComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetDisabledCount());
// Check pending status
auto component = outcome.GetValue()[m_entity1->GetId()].m_addedPendingComponents[0];
AzToolsFramework::EntityCompositionRequestBus::Events::PendingComponentInfo pendingComponentInfo;
AzToolsFramework::EntityCompositionRequestBus::BroadcastResult(pendingComponentInfo, &AzToolsFramework::EntityCompositionRequestBus::Events::GetPendingComponentInfo, component);
// Should have one missing service
ASSERT_EQ(pendingComponentInfo.m_missingRequiredServices.size(), 1);
ASSERT_EQ(pendingComponentInfo.m_validComponentsThatAreIncompatible.size(), 0);
ASSERT_EQ(pendingComponentInfo.m_pendingComponentsWithRequiredServices.size(), 0);
// And that missing service should be the "UnderwearService"
ASSERT_EQ(pendingComponentInfo.m_missingRequiredServices[0], AZ_CRC("UnderwearService"));
outcome = AzToolsFramework::AddComponents<WhiteBriefsComponent>::ToEntities(m_entity1);
ASSERT_TRUE(outcome.IsSuccess());
ASSERT_TRUE(VerifyAddedValidComponents<WhiteBriefsComponent>::AndAdditionalValidatedComponents<BlueJeansComponent>::OnOutcomeForEntity(outcome, m_entity1));
// Save this for later checks
auto whiteBriefsComponent = outcome.GetValue()[m_entity1->GetId()].m_addedValidComponents[0];
ASSERT_EQ(1, CountComponentsOnEntity<BlueJeansComponent>(m_entity1));
ASSERT_EQ(1, CountComponentsOnEntity<WhiteBriefsComponent>(m_entity1));
ASSERT_EQ(2, m_entity1Counter.GetCount());
ASSERT_EQ(0, CountPendingComponentsOnEntity<BlueJeansComponent>(m_entity1));
ASSERT_EQ(0, CountPendingComponentsOnEntity<WhiteBriefsComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetPendingCount());
ASSERT_EQ(0, CountDisabledComponentsOnEntity<BlueJeansComponent>(m_entity1));
ASSERT_EQ(0, CountDisabledComponentsOnEntity<WhiteBriefsComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetDisabledCount());
// Now try adding the second kind of underwear
// (it should be pending because entity already has underwear)
outcome = AzToolsFramework::AddComponents<HeartBoxersComponent>::ToEntities(m_entity1);
ASSERT_TRUE(outcome.IsSuccess());
ASSERT_TRUE(VerifyAddedPendingComponents<HeartBoxersComponent>::OnOutcomeForEntity(outcome, m_entity1));
ASSERT_EQ(1, CountComponentsOnEntity<BlueJeansComponent>(m_entity1));
ASSERT_EQ(1, CountComponentsOnEntity<WhiteBriefsComponent>(m_entity1));
ASSERT_EQ(0, CountComponentsOnEntity<HeartBoxersComponent>(m_entity1));
ASSERT_EQ(2, m_entity1Counter.GetCount());
ASSERT_EQ(0, CountPendingComponentsOnEntity<BlueJeansComponent>(m_entity1));
ASSERT_EQ(0, CountPendingComponentsOnEntity<WhiteBriefsComponent>(m_entity1));
ASSERT_EQ(1, CountPendingComponentsOnEntity<HeartBoxersComponent>(m_entity1));
ASSERT_EQ(1, m_entity1Counter.GetPendingCount());
ASSERT_EQ(0, CountDisabledComponentsOnEntity<BlueJeansComponent>(m_entity1));
ASSERT_EQ(0, CountDisabledComponentsOnEntity<WhiteBriefsComponent>(m_entity1));
ASSERT_EQ(0, CountDisabledComponentsOnEntity<HeartBoxersComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetDisabledCount());
// Check pending status
component = outcome.GetValue()[m_entity1->GetId()].m_addedPendingComponents[0];
AzToolsFramework::EntityCompositionRequestBus::BroadcastResult(pendingComponentInfo, &AzToolsFramework::EntityCompositionRequestBus::Events::GetPendingComponentInfo, component);
// Should have one incompatible component
ASSERT_EQ(pendingComponentInfo.m_missingRequiredServices.size(), 0);
ASSERT_EQ(pendingComponentInfo.m_validComponentsThatAreIncompatible.size(), 1);
ASSERT_EQ(pendingComponentInfo.m_pendingComponentsWithRequiredServices.size(), 0);
// And that incompatible component should be the WhiteBriefsComponent from earlier
ASSERT_EQ(pendingComponentInfo.m_validComponentsThatAreIncompatible[0], whiteBriefsComponent);
// disable white briefs component, which should resolve heart briefs, and check container counts
AzToolsFramework::DisableComponents(whiteBriefsComponent);
ASSERT_EQ(1, CountComponentsOnEntity<BlueJeansComponent>(m_entity1));
ASSERT_EQ(0, CountComponentsOnEntity<WhiteBriefsComponent>(m_entity1));
ASSERT_EQ(1, CountComponentsOnEntity<HeartBoxersComponent>(m_entity1));
ASSERT_EQ(2, m_entity1Counter.GetCount());
ASSERT_EQ(0, CountPendingComponentsOnEntity<BlueJeansComponent>(m_entity1));
ASSERT_EQ(0, CountPendingComponentsOnEntity<WhiteBriefsComponent>(m_entity1));
ASSERT_EQ(0, CountPendingComponentsOnEntity<HeartBoxersComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetPendingCount());
ASSERT_EQ(0, CountDisabledComponentsOnEntity<BlueJeansComponent>(m_entity1));
ASSERT_EQ(1, CountDisabledComponentsOnEntity<WhiteBriefsComponent>(m_entity1));
ASSERT_EQ(0, CountDisabledComponentsOnEntity<HeartBoxersComponent>(m_entity1));
ASSERT_EQ(1, m_entity1Counter.GetDisabledCount());
// re-enable white briefs component which is now pending because it's re-added after heart boxers was resolved
AzToolsFramework::EnableComponents(whiteBriefsComponent);
ASSERT_EQ(1, CountComponentsOnEntity<BlueJeansComponent>(m_entity1));
ASSERT_EQ(0, CountComponentsOnEntity<WhiteBriefsComponent>(m_entity1));
ASSERT_EQ(1, CountComponentsOnEntity<HeartBoxersComponent>(m_entity1));
ASSERT_EQ(2, m_entity1Counter.GetCount());
ASSERT_EQ(0, CountPendingComponentsOnEntity<BlueJeansComponent>(m_entity1));
ASSERT_EQ(1, CountPendingComponentsOnEntity<WhiteBriefsComponent>(m_entity1));
ASSERT_EQ(0, CountPendingComponentsOnEntity<HeartBoxersComponent>(m_entity1));
ASSERT_EQ(1, m_entity1Counter.GetPendingCount());
ASSERT_EQ(0, CountDisabledComponentsOnEntity<BlueJeansComponent>(m_entity1));
ASSERT_EQ(0, CountDisabledComponentsOnEntity<WhiteBriefsComponent>(m_entity1));
ASSERT_EQ(0, CountDisabledComponentsOnEntity<HeartBoxersComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetDisabledCount());
// Try removing pending component (should be uneventful, but it is a branch internally)
auto removalOutcome = AzToolsFramework::RemoveComponents(component);
ASSERT_TRUE(removalOutcome.IsSuccess());
ASSERT_EQ(1, CountComponentsOnEntity<BlueJeansComponent>(m_entity1));
ASSERT_EQ(1, CountComponentsOnEntity<WhiteBriefsComponent>(m_entity1));
ASSERT_EQ(0, CountComponentsOnEntity<HeartBoxersComponent>(m_entity1));
ASSERT_EQ(2, m_entity1Counter.GetCount());
ASSERT_EQ(0, CountPendingComponentsOnEntity<BlueJeansComponent>(m_entity1));
ASSERT_EQ(0, CountPendingComponentsOnEntity<WhiteBriefsComponent>(m_entity1));
ASSERT_EQ(0, CountPendingComponentsOnEntity<HeartBoxersComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetPendingCount());
ASSERT_EQ(0, CountDisabledComponentsOnEntity<BlueJeansComponent>(m_entity1));
ASSERT_EQ(0, CountDisabledComponentsOnEntity<WhiteBriefsComponent>(m_entity1));
ASSERT_EQ(0, CountDisabledComponentsOnEntity<HeartBoxersComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetDisabledCount());
}
// Add a component to two entities, where the component requires a service,
// and one entity already has that service, but the other entity does not.
TEST_F(AddComponentsTest, TwoEntitiesWhereOneHasRequiredServiceAndOneDoesNot)
{
// entity1 already has socks
auto outcome = AzToolsFramework::AddComponents<WoolSocksComponent>::ToEntities(m_entity1);
ASSERT_TRUE(outcome.IsSuccess());
ASSERT_TRUE(VerifyAddedValidComponents<WoolSocksComponent>::OnOutcomeForEntity(outcome, m_entity1));
ASSERT_EQ(1, CountComponentsOnEntity<WoolSocksComponent>(m_entity1));
ASSERT_EQ(1, m_entity1Counter.GetCount());
ASSERT_EQ(0, CountPendingComponentsOnEntity<WoolSocksComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetPendingCount());
ASSERT_EQ(0, CountDisabledComponentsOnEntity<WoolSocksComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetDisabledCount());
outcome = AzToolsFramework::AddComponents<LeatherBootsComponent>::ToEntities(m_entity1, m_entity2);
ASSERT_TRUE(outcome.IsSuccess());
ASSERT_TRUE(VerifyAddedValidComponents<LeatherBootsComponent>::OnOutcomeForEntity(outcome, m_entity1));
ASSERT_TRUE(VerifyAddedPendingComponents<LeatherBootsComponent>::OnOutcomeForEntity(outcome, m_entity2));
ASSERT_EQ(1, CountComponentsOnEntity<WoolSocksComponent>(m_entity1));
ASSERT_EQ(1, CountComponentsOnEntity<LeatherBootsComponent>(m_entity1));
ASSERT_EQ(2, m_entity1Counter.GetCount());
ASSERT_EQ(0, CountPendingComponentsOnEntity<WoolSocksComponent>(m_entity1));
ASSERT_EQ(0, CountPendingComponentsOnEntity<LeatherBootsComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetPendingCount());
ASSERT_EQ(0, CountDisabledComponentsOnEntity<WoolSocksComponent>(m_entity1));
ASSERT_EQ(0, CountDisabledComponentsOnEntity<LeatherBootsComponent>(m_entity1));
ASSERT_EQ(0, m_entity1Counter.GetDisabledCount());
ASSERT_EQ(0, CountComponentsOnEntity<LeatherBootsComponent>(m_entity2));
ASSERT_EQ(0, CountComponentsOnEntity<WoolSocksComponent>(m_entity2));
ASSERT_EQ(0, m_entity2Counter.GetCount());
ASSERT_EQ(1, CountPendingComponentsOnEntity<LeatherBootsComponent>(m_entity2));
ASSERT_EQ(0, CountPendingComponentsOnEntity<WoolSocksComponent>(m_entity2));
ASSERT_EQ(1, m_entity2Counter.GetPendingCount());
ASSERT_EQ(0, CountDisabledComponentsOnEntity<LeatherBootsComponent>(m_entity2));
ASSERT_EQ(0, CountDisabledComponentsOnEntity<WoolSocksComponent>(m_entity2));
ASSERT_EQ(0, m_entity2Counter.GetDisabledCount());
}
// Test adding a component which requires a service,
// but all candidates which provide that service conflicts with some existing component.
TEST_F(AddComponentsTest, RequiredServiceConflictsWithExistingComponents)
{
auto outcome = AzToolsFramework::AddComponents<HatesSocksComponent>::ToEntities(m_entity1);
ASSERT_TRUE(outcome.IsSuccess());
ASSERT_TRUE(VerifyAddedValidComponents<HatesSocksComponent>::OnOutcomeForEntity(outcome, m_entity1));
// Save this for tests and removal later
AZ::Component* hatesSocksComponent = outcome.GetValue()[m_entity1->GetId()].m_addedValidComponents[0];
// Adding boots
outcome = AzToolsFramework::AddComponents<LeatherBootsComponent>::ToEntities(m_entity1, m_entity2);
ASSERT_TRUE(outcome.IsSuccess());
ASSERT_TRUE(VerifyAddedPendingComponents<LeatherBootsComponent>::OnOutcomeForEntity(outcome, m_entity1));
ASSERT_TRUE(VerifyAddedPendingComponents<LeatherBootsComponent>::OnOutcomeForEntity(outcome, m_entity2));
auto leatherBootsComponent = outcome.GetValue()[m_entity1->GetId()].m_addedPendingComponents[0];
// Add socks to make it valid, but incompatible with HatesSocks on entity 1
outcome = AzToolsFramework::AddComponents<WoolSocksComponent>::ToEntities(m_entity1, m_entity2);
ASSERT_TRUE(VerifyAddedPendingComponents<WoolSocksComponent>::OnOutcomeForEntity(outcome, m_entity1));
// Socks will work on entity 2 and leather boots should be valid now because of it
ASSERT_TRUE(VerifyAddedValidComponents<WoolSocksComponent>::AndAdditionalValidatedComponents<LeatherBootsComponent>::OnOutcomeForEntity(outcome, m_entity2));
// Save this component for later
auto woolSocksComponent = outcome.GetValue()[m_entity1->GetId()].m_addedPendingComponents[0];
// Check pending status
AzToolsFramework::EntityCompositionRequestBus::Events::PendingComponentInfo pendingComponentInfo;
// First check leather boots, it should indicate it is waiting on woolSocksComponent
AzToolsFramework::EntityCompositionRequestBus::BroadcastResult(pendingComponentInfo, &AzToolsFramework::EntityCompositionRequestBus::Events::GetPendingComponentInfo, leatherBootsComponent);
// Should have one pending component
ASSERT_EQ(pendingComponentInfo.m_missingRequiredServices.size(), 0);
ASSERT_EQ(pendingComponentInfo.m_validComponentsThatAreIncompatible.size(), 0);
ASSERT_EQ(pendingComponentInfo.m_pendingComponentsWithRequiredServices.size(), 1);
// And that pending component should be the woolSocksComponent
ASSERT_EQ(pendingComponentInfo.m_pendingComponentsWithRequiredServices[0], woolSocksComponent);
// Now check the wool socks, they should be incompatible with Hates Socks
AzToolsFramework::EntityCompositionRequestBus::BroadcastResult(pendingComponentInfo, &AzToolsFramework::EntityCompositionRequestBus::Events::GetPendingComponentInfo, woolSocksComponent);
// Should have one incompatible component
ASSERT_EQ(pendingComponentInfo.m_missingRequiredServices.size(), 0);
ASSERT_EQ(pendingComponentInfo.m_validComponentsThatAreIncompatible.size(), 1);
ASSERT_EQ(pendingComponentInfo.m_pendingComponentsWithRequiredServices.size(), 0);
// And that incompatible component should be the hatesSocksComponent
ASSERT_EQ(pendingComponentInfo.m_validComponentsThatAreIncompatible[0], hatesSocksComponent);
// Remove HatesSocks from entity 1 to valid the entire entity
auto removalOutcome = AzToolsFramework::RemoveComponents(hatesSocksComponent);
ASSERT_TRUE(removalOutcome.IsSuccess());
ASSERT_TRUE((VerifyRemovalValidatedComponents<WoolSocksComponent, LeatherBootsComponent>::OnOutcomeForEntity(removalOutcome, m_entity1)));
}
// Test adding, enabling, disabling several components
TEST_F(AddComponentsTest, EnableDisableConflictingServices)
{
auto outcome = AzToolsFramework::AddComponents<LeatherBootsComponent>::ToEntities(m_entity1);
ASSERT_TRUE(outcome.IsSuccess());
ASSERT_EQ(0, m_entity1Counter.GetCount());
ASSERT_EQ(1, m_entity1Counter.GetPendingCount());
ASSERT_EQ(0, m_entity1Counter.GetDisabledCount());
outcome = AzToolsFramework::AddComponents<HatesSocksComponent>::ToEntities(m_entity1);
ASSERT_TRUE(outcome.IsSuccess());
ASSERT_EQ(1, m_entity1Counter.GetCount());
ASSERT_EQ(1, m_entity1Counter.GetPendingCount());
ASSERT_EQ(0, m_entity1Counter.GetDisabledCount());
outcome = AzToolsFramework::AddComponents<BlueJeansComponent>::ToEntities(m_entity1);
ASSERT_TRUE(outcome.IsSuccess());
ASSERT_EQ(1, m_entity1Counter.GetCount());
ASSERT_EQ(2, m_entity1Counter.GetPendingCount());
ASSERT_EQ(0, m_entity1Counter.GetDisabledCount());
outcome = AzToolsFramework::AddComponents<WhiteBriefsComponent>::ToEntities(m_entity1);
ASSERT_TRUE(outcome.IsSuccess());
ASSERT_EQ(3, m_entity1Counter.GetCount());
ASSERT_EQ(1, m_entity1Counter.GetPendingCount());
ASSERT_EQ(0, m_entity1Counter.GetDisabledCount());
outcome = AzToolsFramework::AddComponents<HeartBoxersComponent>::ToEntities(m_entity1);
ASSERT_TRUE(outcome.IsSuccess());
ASSERT_EQ(3, m_entity1Counter.GetCount());
ASSERT_EQ(2, m_entity1Counter.GetPendingCount());
ASSERT_EQ(0, m_entity1Counter.GetDisabledCount());
outcome = AzToolsFramework::AddComponents<KnifeSheathComponent>::ToEntities(m_entity1);
ASSERT_TRUE(outcome.IsSuccess());
ASSERT_EQ(3, m_entity1Counter.GetCount());
ASSERT_EQ(3, m_entity1Counter.GetPendingCount());
ASSERT_EQ(0, m_entity1Counter.GetDisabledCount());
outcome = AzToolsFramework::AddComponents<WoolSocksComponent>::ToEntities(m_entity1);
ASSERT_TRUE(outcome.IsSuccess());
ASSERT_EQ(3, m_entity1Counter.GetCount());
ASSERT_EQ(4, m_entity1Counter.GetPendingCount());
ASSERT_EQ(0, m_entity1Counter.GetDisabledCount());
AzToolsFramework::DisableComponents(GetComponentsForEntity<HatesSocksComponent>(m_entity1));
ASSERT_EQ(4, m_entity1Counter.GetCount());
ASSERT_EQ(2, m_entity1Counter.GetPendingCount());
ASSERT_EQ(1, m_entity1Counter.GetDisabledCount());
AzToolsFramework::DisableComponents(GetComponentsForEntity<HeartBoxersComponent>(m_entity1));
ASSERT_EQ(4, m_entity1Counter.GetCount());
ASSERT_EQ(1, m_entity1Counter.GetPendingCount());
ASSERT_EQ(2, m_entity1Counter.GetDisabledCount());
AzToolsFramework::DisableComponents(GetComponentsForEntity<KnifeSheathComponent>(m_entity1));
ASSERT_EQ(4, m_entity1Counter.GetCount());
ASSERT_EQ(0, m_entity1Counter.GetPendingCount());
ASSERT_EQ(3, m_entity1Counter.GetDisabledCount());
AzToolsFramework::DisableComponents(GetComponentsForEntity<WhiteBriefsComponent>(m_entity1));
ASSERT_EQ(2, m_entity1Counter.GetCount());
ASSERT_EQ(1, m_entity1Counter.GetPendingCount());
ASSERT_EQ(4, m_entity1Counter.GetDisabledCount());
AzToolsFramework::EnableComponents(GetComponentsForEntity<HeartBoxersComponent>(m_entity1));
ASSERT_EQ(4, m_entity1Counter.GetCount());
ASSERT_EQ(0, m_entity1Counter.GetPendingCount());
ASSERT_EQ(3, m_entity1Counter.GetDisabledCount());
}
// A reusable testing fixture that ensures basic application services are mocked.
// Provided:
// Basic Component descriptor functionality
// Memory
// Serialize (and Edit) contexts
class MockApplicationFixture
: public AllocatorsFixture
, public AZ::ComponentApplicationBus::Handler
{
public:
AZStd::unique_ptr<AZ::SerializeContext> m_serializeContext;
AZStd::vector<const ComponentDescriptor*> m_descriptors;
//////////////////////////////////////////////////////////////////////////
// ComponentApplicationMessages
ComponentApplication* GetApplication() override { return nullptr; }
void RegisterComponentDescriptor(const ComponentDescriptor* descriptor) override
{
m_descriptors.push_back(descriptor);
descriptor->Reflect(m_serializeContext.get());
}
void UnregisterComponentDescriptor(const ComponentDescriptor*) override {}
void RegisterEntityAddedEventHandler(EntityAddedEvent::Handler&) override {}
void RegisterEntityRemovedEventHandler(EntityRemovedEvent::Handler&) override {}
void RegisterEntityActivatedEventHandler(EntityActivatedEvent::Handler&) override {}
void RegisterEntityDeactivatedEventHandler(EntityDeactivatedEvent::Handler&) override {}
void SignalEntityActivated(Entity*) override {}
void SignalEntityDeactivated(Entity*) override {}
bool AddEntity(Entity*) override { return true; }
bool RemoveEntity(Entity*) override { return true; }
bool DeleteEntity(const EntityId&) override { return true; }
Entity* FindEntity(const EntityId&) override { return nullptr; }
SerializeContext* GetSerializeContext() override { return m_serializeContext.get(); }
BehaviorContext* GetBehaviorContext() override { return nullptr; }
JsonRegistrationContext* GetJsonRegistrationContext() override { return nullptr; }
const char* GetEngineRoot() const override { return nullptr; }
const char* GetExecutableFolder() const override { return nullptr; }
void EnumerateEntities(const EntityCallback& /*callback*/) override {}
void QueryApplicationType(AZ::ApplicationTypeQuery& /*appType*/) const override {}
//////////////////////////////////////////////////////////////////////////
MockApplicationFixture()
: AllocatorsFixture()
{
}
void SetUp() override
{
AllocatorsFixture::SetUp();
ComponentApplicationBus::Handler::BusConnect();
AZ::Interface<AZ::ComponentApplicationRequests>::Register(this);
m_serializeContext.reset(aznew AZ::SerializeContext(true, true));
Entity::Reflect(m_serializeContext.get());
}
void TearDown() override
{
for (auto descriptor : m_descriptors)
{
delete descriptor;
}
m_descriptors.set_capacity(0);
m_serializeContext.reset();
AZ::Interface<AZ::ComponentApplicationRequests>::Unregister(this);
ComponentApplicationBus::Handler::BusDisconnect();
AllocatorsFixture::TearDown();
}
};
// add scrubbing capability to the above fixture by adding some components to it.
class EntityTest_Scrubbing : public MockApplicationFixture
{
protected:
AZStd::unique_ptr<Entity> m_fakeSystemEntity;
public:
class VisibleComponent
: public AzToolsFramework::Components::EditorComponentBase
{
public:
AZ_COMPONENT(VisibleComponent, "{6CEC2D1E-08CF-4609-9BEE-BA9D32B4C223}", AzToolsFramework::Components::EditorComponentBase);
void Activate() override {}
void Deactivate() override {}
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
{
provided.push_back(AZ_CRC("ValidComponentService"));
}
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
{
incompatible.push_back(AZ_CRC("ValidComponentService"));
}
static void Reflect(ReflectContext* reflection)
{
SerializeContext* serializeContext = azrtti_cast<SerializeContext*>(reflection);
if (serializeContext)
{
serializeContext->Class<VisibleComponent, AzToolsFramework::Components::EditorComponentBase>();
AZ::EditContext* ec = serializeContext->GetEditContext();
ec->Class<VisibleComponent>("Visible Component", "A class that should show up in the property editor")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::Show)
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("Game", 0x232b318c));
}
}
};
class HiddenComponent
: public AzToolsFramework::Components::EditorComponentBase
{
public:
AZ_COMPONENT(HiddenComponent, "{E4D2AD8B-3930-46FC-837A-8DDFCA0FB1AF}", AzToolsFramework::Components::EditorComponentBase);
static Component* s_wasDeleted;
~HiddenComponent() override
{
s_wasDeleted = this;
}
void Activate() override {}
void Deactivate() override {}
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
{
provided.push_back(AZ_CRC("HiddenComponentService"));
}
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
{
incompatible.push_back(AZ_CRC("HiddenComponentService"));
}
static void Reflect(ReflectContext* reflection)
{
SerializeContext* serializeContext = azrtti_cast<SerializeContext*>(reflection);
if (serializeContext)
{
serializeContext->Class<HiddenComponent, AzToolsFramework::Components::EditorComponentBase>();
AZ::EditContext* ec = serializeContext->GetEditContext();
ec->Class<HiddenComponent>("Hidden Component", "A class that should not show up in the property editor")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::Hide)
->Attribute(AZ::Edit::Attributes::HideIcon, true)
->Attribute(AZ::Edit::Attributes::SliceFlags, AZ::Edit::SliceFlags::HideOnAdd | AZ::Edit::SliceFlags::PushWhenHidden);
}
}
};
void SetUp() override
{
MockApplicationFixture::SetUp();
RegisterComponentDescriptor(VisibleComponent::CreateDescriptor());
RegisterComponentDescriptor(HiddenComponent::CreateDescriptor());
RegisterComponentDescriptor(AzToolsFramework::Components::EditorPendingCompositionComponent::CreateDescriptor());
RegisterComponentDescriptor(AzToolsFramework::Components::EditorEntityActionComponent::CreateDescriptor());
m_fakeSystemEntity.reset(aznew Entity());
m_fakeSystemEntity->CreateComponent<AzToolsFramework::Components::EditorEntityActionComponent>();
m_fakeSystemEntity->Init();
m_fakeSystemEntity->Activate();
}
void TearDown() override
{
m_fakeSystemEntity->Deactivate();
m_fakeSystemEntity.reset();
MockApplicationFixture::TearDown();
}
};
Component* EntityTest_Scrubbing::HiddenComponent::s_wasDeleted = nullptr;
TEST_F(EntityTest_Scrubbing, ConflictingVisibleComponents_AreInvalidated)
{
// in this test we make sure that visible components (ones which show up on the UI)
// that conflict with each other are properly disabled and moved to the pending list during scrubbing.
// Component setup:
AZ::Entity newEntity;
AZ::Component* firstValidComponent = aznew VisibleComponent();
newEntity.AddComponent(firstValidComponent);
AZ::Component* conflictingVisibleComponent = aznew VisibleComponent();
newEntity.AddComponent(conflictingVisibleComponent);
EntityList entities;
entities.push_back(&newEntity);
EntityCompositionRequests::ScrubEntitiesOutcome resultValue = AZ::Failure<AZStd::string>("Didn't get called");
EntityCompositionRequestBus::BroadcastResult(resultValue, &EntityCompositionRequestBus::Events::ScrubEntities, entities);
ASSERT_TRUE(resultValue.IsSuccess());
ASSERT_EQ(resultValue.GetValue().size(), 1);
EntityCompositionRequests::ScrubEntityResults& resultForThisEntity = resultValue.GetValue()[entities[0]->GetId()];
EXPECT_EQ(resultForThisEntity.m_invalidatedComponents.size(), 1);
EXPECT_TRUE(AZStd::find(resultForThisEntity.m_invalidatedComponents.begin(), resultForThisEntity.m_invalidatedComponents.end(), conflictingVisibleComponent) != resultForThisEntity.m_invalidatedComponents.end());
// The "Validated components" array should be empty becuase it should only list previously invalid components that were somehow validated by the scrubbing.
EXPECT_EQ(resultForThisEntity.m_validatedComponents.size(), 0);
// make sure the valid visible one wasn't removed:
EXPECT_EQ(newEntity.FindComponent(azrtti_typeid<VisibleComponent>()), firstValidComponent);
}
TEST_F(EntityTest_Scrubbing, ConflictingHiddenComponents_AreDeleted)
{
// in this test we make sure that when an entity contains a conflicting hidden component
// the hidden component is deleted as part of thes scrubbing.
// Component setup:
AZ::Entity newEntity;
AZ::Component* validHiddenComponent = aznew HiddenComponent();
AZ::Component* conflictingHiddenComponent = aznew HiddenComponent();
newEntity.AddComponent(validHiddenComponent);
newEntity.AddComponent(conflictingHiddenComponent);
EntityTest_Scrubbing::HiddenComponent::s_wasDeleted = nullptr;
EntityList entities;
entities.push_back(&newEntity);
EntityCompositionRequests::ScrubEntitiesOutcome resultValue = AZ::Failure<AZStd::string>("Didn't get called");
EntityCompositionRequestBus::BroadcastResult(resultValue, &EntityCompositionRequestBus::Events::ScrubEntities, entities);
// we cannnot test anything further if the array is empty or it failed.
ASSERT_TRUE(resultValue.IsSuccess());
ASSERT_EQ(resultValue.GetValue().size(), 1);
EntityCompositionRequests::ScrubEntityResults& resultForThisEntity = resultValue.GetValue()[entities[0]->GetId()];
// we must NOT find the conflicting component - should have been deleted.
EXPECT_EQ(EntityTest_Scrubbing::HiddenComponent::s_wasDeleted, conflictingHiddenComponent);
// we must also not find it on the invalidated list, since it has been deleted.
EXPECT_EQ(resultForThisEntity.m_invalidatedComponents.size(), 0);
// The "Validated components" array should be empty becuase it should only list previously invalid components that were somehow validated by the scrubbing.
EXPECT_EQ(resultForThisEntity.m_validatedComponents.size(), 0);
// make sure the remaining component on the entity is the correct hidden component
EXPECT_EQ(newEntity.FindComponents(azrtti_typeid<HiddenComponent>()).size(), 1); // there should only be one of them.
EXPECT_EQ(newEntity.FindComponent(azrtti_typeid<HiddenComponent>()), validHiddenComponent);
}
TEST_F(EntityTest_Scrubbing, NonConflictingVisibleComponents_AreReinstated)
{
// in this test we make sure that if a pending component (inactive due to prior problems)
// no longer has those problems, it is made valid and active when scrubbing occurs.
// Component setup:
AZ::Entity newEntity;
AZ::Component* firstValidComponent = aznew VisibleComponent();
AZ::Component* conflictingVisibleComponent = aznew VisibleComponent();
newEntity.AddComponent(firstValidComponent);
newEntity.AddComponent(conflictingVisibleComponent);
EntityList entities;
entities.push_back(&newEntity);
{
EntityCompositionRequests::ScrubEntitiesOutcome resultValue = AZ::Failure<AZStd::string>("Didn't get called");
EntityCompositionRequestBus::BroadcastResult(resultValue, &EntityCompositionRequestBus::Events::ScrubEntities, entities);
ASSERT_TRUE(resultValue.IsSuccess());
ASSERT_EQ(resultValue.GetValue().size(), 1);
// note that the actual results of the above operation are already verified in another test. now we go further with this
// and actually delete the original component so that the second one can become valid.
}
newEntity.RemoveComponent(firstValidComponent);
delete firstValidComponent;
// now re-scrub and expect to see the previously disabled conflicting one become the active one:
{
EntityCompositionRequests::ScrubEntitiesOutcome resultValue = AZ::Failure<AZStd::string>("Didn't get called");
EntityCompositionRequestBus::BroadcastResult(resultValue, &EntityCompositionRequestBus::Events::ScrubEntities, entities);
ASSERT_TRUE(resultValue.IsSuccess());
ASSERT_EQ(resultValue.GetValue().size(), 1);
EntityCompositionRequests::ScrubEntityResults& resultForThisEntity = resultValue.GetValue()[entities[0]->GetId()];
// nothing should be invalidated
EXPECT_EQ(resultForThisEntity.m_invalidatedComponents.size(), 0);
// the visible component should now be activated.
ASSERT_EQ(resultForThisEntity.m_validatedComponents.size(), 1);
EXPECT_EQ(resultForThisEntity.m_validatedComponents[0], conflictingVisibleComponent);
// make sure its actually active, on the entity.
EXPECT_EQ(newEntity.FindComponent(azrtti_typeid<VisibleComponent>()), conflictingVisibleComponent);
}
}
TEST_F(EntityTest_Scrubbing, InactiveEntityWithInvalidComponents_AreValidatedByPendingComponents)
{
// This test takes an entity with known invalid component setup that has not been activated
// Adds pending components which will satisfy the invalid component setup
// And expects scrub entities to succeed in this case.
// Note - this is an edge case when deserializing a module entity or system entity from the app descriptor
RegisterComponentDescriptor(LeatherBootsComponent::CreateDescriptor());
RegisterComponentDescriptor(WoolSocksComponent::CreateDescriptor());
RegisterComponentDescriptor(AzToolsFramework::Components::GenericComponentWrapper::CreateDescriptor());
AZ::Entity* testEntity = aznew AZ::Entity("Test Scrubbing Entity");
testEntity->CreateComponent<AzToolsFramework::Components::EditorPendingCompositionComponent>();
testEntity->Init(); // init to kick off the pending composition request bus, but don't activate because we have invalid components
EntityList entities;
entities.push_back(testEntity);
// Manually add boots component that requires the socks component, to simulate this being read out of app descriptor
testEntity->CreateComponent<AzToolsFramework::Components::GenericComponentWrapper>(aznew LeatherBootsComponent());
EntityCompositionRequests::ScrubEntitiesOutcome scrubResults = AZ::Failure<AZStd::string>("Didn't get called");
EntityCompositionRequestBus::BroadcastResult(scrubResults, &EntityCompositionRequestBus::Events::ScrubEntities, entities);
ASSERT_TRUE(scrubResults.IsSuccess());
// If component is invalidated by scrubbing, it should now be in the pending set
AZ::Entity::ComponentArrayType pendingComponents;
AzToolsFramework::EditorPendingCompositionRequestBus::Event(testEntity->GetId(), &AzToolsFramework::EditorPendingCompositionRequests::GetPendingComponents, pendingComponents);
ASSERT_EQ(pendingComponents.size(), 1);
// the boots component should be flagged as invalid, since our entity was not activated
EntityCompositionRequests::ScrubEntityResults& resultForTestEntity = scrubResults.GetValue()[entities[0]->GetId()];
ASSERT_EQ(resultForTestEntity.m_invalidatedComponents.size(), 1);
// Don't actually want to keep the component in the pending set, so that we can validate the initial problem, so add it back onto the entity
pendingComponents.clear();
AZ::Component* invalidComponent = resultForTestEntity.m_invalidatedComponents[0];
AzToolsFramework::EditorPendingCompositionRequestBus::Event(testEntity->GetId(), &AzToolsFramework::EditorPendingCompositionRequests::RemovePendingComponent, invalidComponent);
AzToolsFramework::EditorPendingCompositionRequestBus::Event(testEntity->GetId(), &AzToolsFramework::EditorPendingCompositionRequests::GetPendingComponents, pendingComponents);
ASSERT_TRUE(pendingComponents.empty());
delete invalidComponent;
// now add a socks component to the pending set which will fulfill the boots' dependency
testEntity->CreateComponent<AzToolsFramework::Components::GenericComponentWrapper>(aznew LeatherBootsComponent());
AzToolsFramework::EditorPendingCompositionRequestBus::Event(testEntity->GetId(), &AzToolsFramework::EditorPendingCompositionRequests::AddPendingComponent, aznew WoolSocksComponent());
pendingComponents.clear();
AzToolsFramework::EditorPendingCompositionRequestBus::Event(testEntity->GetId(), &AzToolsFramework::EditorPendingCompositionRequests::GetPendingComponents, pendingComponents);
ASSERT_EQ(pendingComponents.size(), 1);
scrubResults = AZ::Failure<AZStd::string>("Didn't get called");
EntityCompositionRequestBus::BroadcastResult(scrubResults, &EntityCompositionRequestBus::Events::ScrubEntities, entities);
ASSERT_TRUE(scrubResults.IsSuccess());
resultForTestEntity = scrubResults.GetValue()[entities[0]->GetId()];
ASSERT_TRUE(resultForTestEntity.m_invalidatedComponents.empty());
// this should now succeed because the wool socks component is on the entity
testEntity->Activate();
ASSERT_EQ(testEntity->GetState(), Entity::State::Active);
delete testEntity;
}
} // namespace UnitTest