// Copyright Epic Games, Inc. All Rights Reserved. #include "AITestsCommon.h" #include "MassProcessorDependencySolver.h" #include "MassEntityTestTypes.h" #include "MassTypeManager.h" #define LOCTEXT_NAMESPACE "MassTest" UE_DISABLE_OPTIMIZATION_SHIP namespace FMassDependencySolverTest { template static FName GetProcessorName() { return T::StaticClass()->GetFName(); } struct FDependencySolverBase : FAITestBase { TArray Processors; TArray Result; TSharedPtr EntityManager; virtual bool SetUp() override { EntityManager = MakeShareable(new FMassEntityManager()); Processors.Reset(); EntityManager->GetTypeManager().RegisterType(); EntityManager->GetTypeManager().RegisterType(); EntityManager->GetTypeManager().RegisterType(); return true; } void Solve() { Result.Reset(); FMassProcessorDependencySolver Solver(MakeArrayView((UMassProcessor**)Processors.GetData(), Processors.Num())); Solver.ResolveDependencies(Result, EntityManager); } }; struct FTrivialDependency : FDependencySolverBase { UMassTestProcessorBase* Proc = nullptr; virtual bool SetUp() override { FDependencySolverBase::SetUp(); Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); return true; } virtual bool InstantTest() override { Solve(); AITEST_EQUAL("The results should contain only a single processor", Result.Num(), 1); AITEST_EQUAL("The sole processor should be the one we've added", Result[0].Processor, Proc); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FTrivialDependency, "System.Mass.Dependencies.Trivial"); struct FSimpleDependency : FDependencySolverBase { virtual bool SetUp() override { FDependencySolverBase::SetUp(); { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); Proc->GetMutableExecutionOrder().ExecuteAfter.Add(GetProcessorName()); } { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); Proc->GetMutableExecutionOrder().ExecuteAfter.Add(GetProcessorName()); } Processors.Add(NewTestProcessor(EntityManager)); return true; } virtual bool InstantTest() override { Solve(); AITEST_TRUE("C is expected to be first", Result[0].Name == GetProcessorName()); AITEST_TRUE("A is expected to be second", Result[1].Name == GetProcessorName()); AITEST_TRUE("B is expected to be third", Result[2].Name == GetProcessorName()); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FSimpleDependency, "System.Mass.Dependencies.Simple"); struct FMissingDependency : FDependencySolverBase { virtual bool SetUp() override { FDependencySolverBase::SetUp(); { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); Proc->GetMutableExecutionOrder().ExecuteAfter.Add(TEXT("NonExistingDependency")); } { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); Proc->GetMutableExecutionOrder().ExecuteBefore.Add(TEXT("NonExistingDependency2")); Proc->GetMutableExecutionOrder().ExecuteAfter.Add(GetProcessorName()); } { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); } return true; } virtual bool InstantTest() override { Solve(); // event though there's no direct dependency between A and B due to declared dependencies on "NonExistingDependency" // B should come before A AITEST_TRUE("C is expected to be the first one", Result[0].Name == GetProcessorName()); AITEST_TRUE("Then B", Result[1].Name == GetProcessorName()); AITEST_TRUE("With A being last", Result[2].Name == GetProcessorName()); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FMissingDependency, "System.Mass.Dependencies.MissingDependencies"); struct FDeepGroup : FDependencySolverBase { virtual bool SetUp() override { FDependencySolverBase::SetUp(); { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); Proc->GetMutableExecutionOrder().ExecuteAfter.Add(TEXT("W.X.Y.Z")); Proc->GetMutableExecutionOrder().ExecuteInGroup = TEXT("P.Q.R"); } { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); Proc->GetMutableExecutionOrder().ExecuteInGroup = TEXT("W.X.Y.Z"); } return true; } virtual bool InstantTest() override { Solve(); // dump all the group information from the Result collection for easier ordering testing for (int32 i = 0; i < Result.Num(); ++i) { if (Result[i].NodeType != FMassProcessorOrderInfo::EDependencyNodeType::Processor) { Result.RemoveAt(i--, EAllowShrinking::No); } } AITEST_TRUE("B is expected to be first", Result[0].Name == GetProcessorName()); AITEST_TRUE("A is expected to be second", Result[1].Name == GetProcessorName()); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FDeepGroup, "System.Mass.Dependencies.DeepGroup"); struct FComplexScenario : FDependencySolverBase { virtual bool SetUp() override { FDependencySolverBase::SetUp(); { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); Proc->GetMutableExecutionOrder().ExecuteInGroup = TEXT("X.Z"); Proc->GetMutableExecutionOrder().ExecuteAfter.Add(TEXT("X.Y")); Proc->GetMutableExecutionOrder().ExecuteAfter.Add(UMassTestProcessor_E::StaticClass()->GetFName()); } { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); Proc->GetMutableExecutionOrder().ExecuteInGroup = TEXT("X.Y"); } { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); Proc->GetMutableExecutionOrder().ExecuteInGroup = TEXT("X.Y"); } { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); Proc->GetMutableExecutionOrder().ExecuteBefore.Add(UMassTestProcessor_A::StaticClass()->GetFName()); Proc->GetMutableExecutionOrder().ExecuteBefore.Add(TEXT("X.Y")); } { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); Proc->GetMutableExecutionOrder().ExecuteInGroup = TEXT("X.Z"); } { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); Proc->GetMutableExecutionOrder().ExecuteAfter.Add(UMassTestProcessor_A::StaticClass()->GetFName()); } return true; } virtual bool InstantTest() override { Solve(); AITEST_TRUE("None of the processors should have been pruned", Result.Num() == Processors.Num()); for (int32 i = 0; i < Result.Num(); ++i) { AITEST_EQUAL("We expect only processor nodes in the results", Result[i].NodeType, FMassProcessorOrderInfo::EDependencyNodeType::Processor); } AITEST_TRUE("D is the only fully dependency-less processor so should be first", Result[0].Name == GetProcessorName()); AITEST_TRUE("B and C come next", (Result[1].Name == GetProcessorName() || Result[2].Name == GetProcessorName()) && (Result[1].Name == GetProcessorName() || Result[2].Name == GetProcessorName())); AITEST_TRUE("Following by E", Result[3].Name == GetProcessorName()); AITEST_TRUE("Then A", Result[4].Name == GetProcessorName()); AITEST_TRUE("F is last", Result[5].Name == GetProcessorName()); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FComplexScenario, "System.Mass.Dependencies.Complex"); struct FThreadUnsafeWriteSubsystem : FDependencySolverBase { virtual bool SetUp() override { FDependencySolverBase::SetUp(); { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); Proc->EntityQuery.AddSubsystemRequirement(EMassFragmentAccess::ReadWrite); } { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); Proc->EntityQuery.AddSubsystemRequirement(EMassFragmentAccess::ReadWrite); } return true; } virtual bool InstantTest() override { Solve(); AITEST_TRUE("Dependency between processors is expected", Result[0].Dependencies.IsEmpty() != Result[1].Dependencies.IsEmpty()); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FThreadUnsafeWriteSubsystem, "System.Mass.Dependencies.ThreadUnsafeWriteSubsystem"); struct FThreadSafeWriteSubsystem : FDependencySolverBase { virtual bool SetUp() override { FDependencySolverBase::SetUp(); { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); Proc->EntityQuery.AddSubsystemRequirement(EMassFragmentAccess::ReadWrite); } { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); Proc->EntityQuery.AddSubsystemRequirement(EMassFragmentAccess::ReadWrite); } return true; } virtual bool InstantTest() override { Solve(); AITEST_TRUE("No dependency between processors is expected", Result[0].Dependencies.IsEmpty() && Result[1].Dependencies.IsEmpty()); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FThreadSafeWriteSubsystem, "System.Mass.Dependencies.ThreadSafeWriteSubsystem"); struct FBadInput_Empty : FDependencySolverBase { virtual bool InstantTest() override { Solve(); AITEST_TRUE("Empty input should be handled gracefully", Result.Num() == 0); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FBadInput_Empty, "System.Mass.Dependencies.BadInput.Empty"); struct FBadInput_Null : FDependencySolverBase { virtual bool InstantTest() override { Processors.Reset(); Processors.Add(nullptr); { AITEST_SCOPED_CHECK(TEXT("nullptr found in Processors"), 1); Solve(); } AITEST_TRUE("Single nullptr input should be handled gracefully", Result.Num() == 0); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FBadInput_Null, "System.Mass.Dependencies.BadInput.SingleNull"); struct FBadInput_MultipleNulls : FDependencySolverBase { virtual bool InstantTest() override { Processors.Reset(); Processors.Add(nullptr); Processors.Add(nullptr); Processors.Add(nullptr); { AITEST_SCOPED_CHECK(TEXT("nullptr found in Processors"), 3); Solve(); } AITEST_TRUE("Multiple nullptr inputs should be handled gracefully", Result.Num() == 0); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FBadInput_MultipleNulls, "System.Mass.Dependencies.BadInput.MultipleNulls"); struct FBadInput_MixedNulls : FDependencySolverBase { virtual bool InstantTest() override { Processors.Reset(); Processors.Add(nullptr); Processors.Add(NewTestProcessor(EntityManager)); Processors.Add(nullptr); Processors.Add(NewTestProcessor(EntityManager)); { AITEST_SCOPED_CHECK(TEXT("nullptr found in Processors"), 2); Solve(); } AITEST_TRUE("Mixed nullptr and proper inputs should be handled gracefully", Result.Num() == 2); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FBadInput_MixedNulls, "System.Mass.Dependencies.BadInput.NullsMixedIn"); struct FBadInput_Duplicates : FDependencySolverBase { virtual bool InstantTest() override { Processors.Reset(); Processors.Add(NewTestProcessor(EntityManager)); Processors.Add(NewTestProcessor(EntityManager)); Processors.Add(NewTestProcessor(EntityManager)); { AITEST_SCOPED_CHECK(TEXT("already registered. Duplicates are not supported"), 2); Solve(); } AITEST_TRUE("Duplicates in input should be handled gracefully", Result.Num() == 1); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FBadInput_Duplicates, "System.Mass.Dependencies.BadInput.Duplicates"); struct FGroupNamesGeneration : FAITestBase { virtual bool InstantTest() override { TArray SubGroupNames; FName EmptyName; FMassProcessorDependencySolver::CreateSubGroupNames(EmptyName, SubGroupNames); AITEST_TRUE("Empty group name is supported", SubGroupNames.Num() > 0); AITEST_TRUE("Empty group name handled like any other name", SubGroupNames[0] == EmptyName.ToString()); FMassProcessorDependencySolver::CreateSubGroupNames(TEXT("X"), SubGroupNames); AITEST_TRUE("Trivial group name is supported", SubGroupNames.Num() > 0); AITEST_TRUE("Trivial group name shouldn\'t get decorated", SubGroupNames[0] == TEXT("X")); FMassProcessorDependencySolver::CreateSubGroupNames(TEXT("W.X.Y.Z"), SubGroupNames); AITEST_TRUE("Complex group name should be result in a number of group names equal to group name\'s depth", SubGroupNames.Num() == 4); AITEST_TRUE("Group name W.X.Y.Z should contain subgroup W", SubGroupNames.Find(TEXT("W")) != INDEX_NONE); AITEST_TRUE("Group name W.X.Y.Z should contain subgroup W.X", SubGroupNames.Find(TEXT("W.X")) != INDEX_NONE); AITEST_TRUE("Group name W.X.Y.Z should contain subgroup W.X.Y", SubGroupNames.Find(TEXT("W.X.Y")) != INDEX_NONE); AITEST_TRUE("Group name W.X.Y.Z should contain subgroup W.X.Y.Z", SubGroupNames.Find(TEXT("W.X.Y.Z")) != INDEX_NONE); AITEST_TRUE("Split up of group name W.X.Y.Z should result in a given order" , SubGroupNames[0] == TEXT("W") && SubGroupNames[1] == TEXT("W.X") && SubGroupNames[2] == TEXT("W.X.Y") && SubGroupNames[3] == TEXT("W.X.Y.Z")); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FGroupNamesGeneration, "System.Mass.Dependencies.SubgroupNames"); struct FCircularDependency : FDependencySolverBase { virtual bool SetUp() override { FDependencySolverBase::SetUp(); TSharedRef EntityManagerRef = EntityManager.ToSharedRef(); { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); Proc->EntityQuery.Initialize(EntityManagerRef); Proc->EntityQuery.AddRequirement(EMassFragmentAccess::ReadWrite); Proc->GetMutableExecutionOrder().ExecuteAfter.Add(GetProcessorName()); } { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); Proc->EntityQuery.Initialize(EntityManagerRef); Proc->EntityQuery.AddRequirement(EMassFragmentAccess::ReadWrite); Proc->GetMutableExecutionOrder().ExecuteAfter.Add(GetProcessorName()); } { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); Proc->EntityQuery.Initialize(EntityManagerRef); Proc->EntityQuery.AddRequirement(EMassFragmentAccess::ReadWrite); Proc->GetMutableExecutionOrder().ExecuteAfter.Add(GetProcessorName()); } { UMassTestProcessorBase* Proc = Processors.Add_GetRef(NewTestProcessor(EntityManager)); Proc->EntityQuery.Initialize(EntityManagerRef); Proc->EntityQuery.AddRequirement(EMassFragmentAccess::ReadWrite); Proc->GetMutableExecutionOrder().ExecuteAfter.Add(GetProcessorName()); } return true; } virtual bool InstantTest() override { { AITEST_SCOPED_CHECK(TEXT("Detected processing dependency cycle"), 1); AITEST_SCOPED_CHECK(TEXT("Encountered processing dependency cycle"), 1); Solve(); } // every subsequent processor is expected to depend only on the previous one since all the processors use exactly the same resources AITEST_TRUE("The first processor has no dependencies", Result[0].Dependencies.IsEmpty()); for (int i = 1; i < Result.Num(); ++i) { const auto& ResultNode = Result[i]; AITEST_EQUAL("The subsequent processors has only one dependency", ResultNode.Dependencies.Num(), 1); AITEST_EQUAL("The subsequent processors depend only on the previous one", ResultNode.Dependencies[0], Result[i - 1].Name); } return true; } }; IMPLEMENT_AI_INSTANT_TEST(FCircularDependency, "System.Mass.Dependencies.Circular"); } // FMassDependencySolverTest UE_ENABLE_OPTIMIZATION_SHIP #undef LOCTEXT_NAMESPACE