Files
ProjectEleri/Plugins/DirectiveUtilities/Source/DirectiveUtilitiesTests/Private/Tests/DirectiveUtilAsyncTraceTest.cpp

481 lines
18 KiB
C++

// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Tasks/DirectiveUtilTask_AsyncTrace.h"
#include "Tasks/DirectiveUtilTask_MoveToLocation.h"
#include "Tests/DirectiveUtilTestObject.h"
#include "Engine/World.h"
#include "Engine/Engine.h"
#include "Engine/EngineBaseTypes.h"
#include "Engine/EngineTypes.h"
#include "Engine/StaticMesh.h"
#include "Engine/StaticMeshActor.h"
#include "Components/StaticMeshComponent.h"
#include "GameFramework/DefaultPawn.h"
#include "GameFramework/PlayerController.h"
#include "TimerManager.h"
#include "Misc/AutomationTest.h"
namespace DirectiveUtilAsyncTraceTestHelpers
{
UWorld* CreateTraceWorld()
{
UWorld* World = UWorld::CreateWorld(EWorldType::Game, false);
if (!World)
{
return nullptr;
}
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Game);
WorldContext.SetCurrentWorld(World);
World->InitializeActorsForPlay(FURL());
World->BeginPlay();
return World;
}
AStaticMeshActor* SpawnBlockingCube(UWorld* World, UStaticMesh* CubeMesh)
{
AStaticMeshActor* Cube = World->SpawnActor<AStaticMeshActor>(FVector::ZeroVector, FRotator::ZeroRotator);
UStaticMeshComponent* Component = Cube->GetStaticMeshComponent();
Component->SetMobility(EComponentMobility::Movable);
Component->SetStaticMesh(CubeMesh);
Component->SetCollisionProfileName(TEXT("BlockAll"));
Component->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
return Cube;
}
}
bool UDirectiveUtilTestMoveToLocationTask::HasRegisteredTimers() const
{
const UWorld* World = TimerWorld.Get();
return World && (World->GetTimerManager().TimerExists(TimerHandle) || World->GetTimerManager().TimerExists(StuckTimerHandle));
}
void UDirectiveUtilTestMoveToLocationTask::RegisterTimersForTest(UWorld* World)
{
TimerWorld = World;
World->GetTimerManager().SetTimer(TimerHandle, FTimerDelegate::CreateLambda([] {}), 60.0f, true);
World->GetTimerManager().SetTimer(StuckTimerHandle, FTimerDelegate::CreateLambda([] {}), 60.0f, true);
}
bool UDirectiveUtilTestMoveToActorTask::HasRegisteredTimers() const
{
const UWorld* World = TimerWorld.Get();
return World && (World->GetTimerManager().TimerExists(TimerHandle) || World->GetTimerManager().TimerExists(StuckTimerHandle));
}
void UDirectiveUtilTestMoveToActorTask::RegisterTimersForTest(UWorld* World)
{
TimerWorld = World;
World->GetTimerManager().SetTimer(TimerHandle, FTimerDelegate::CreateLambda([] {}), 60.0f, true);
World->GetTimerManager().SetTimer(StuckTimerHandle, FTimerDelegate::CreateLambda([] {}), 60.0f, true);
}
class FDirectiveUtilTickTraceWorld : public IAutomationLatentCommand
{
public:
FDirectiveUtilTickTraceWorld(FAutomationTestBase* InTest, UWorld* InWorld, const TArray<UDirectiveUtilDelegateListener*>& InListeners, int32 InFrames)
: Test(InTest)
, World(InWorld)
, Listeners(InListeners)
, FramesRemaining(InFrames)
{
}
virtual bool Update() override
{
if (UWorld* TickWorld = World.Get())
{
TickWorld->Tick(LEVELTICK_All, 0.05f);
}
bool bAllComplete = true;
for (const UDirectiveUtilDelegateListener* Listener : Listeners)
{
if (Listener && !Listener->bCompleted)
{
bAllComplete = false;
break;
}
}
if (bAllComplete || --FramesRemaining <= 0)
{
for (UDirectiveUtilDelegateListener* Listener : Listeners)
{
if (!Listener)
{
continue;
}
Test->TestTrue(TEXT("Async trace broadcasts Completed"), Listener->bCompleted);
Test->TestTrue(TEXT("Async trace through a blocking cube reports a hit"), Listener->HitCount > 0);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
if (UWorld* TearDownWorld = World.Get())
{
GEngine->DestroyWorldContext(TearDownWorld);
TearDownWorld->DestroyWorld(false);
}
return true;
}
return false;
}
private:
FAutomationTestBase* Test;
TWeakObjectPtr<UWorld> World;
TArray<UDirectiveUtilDelegateListener*> Listeners;
int32 FramesRemaining;
};
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilAsyncTraceTest, "DirectiveUtilities.AsyncTaskTraceTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilAsyncTraceTest::RunTest(const FString& Parameters)
{
AddExpectedMessagePlain(TEXT("Async Trace failed to activate. World is null."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_AsyncTrace* Task = UDirectiveUtilTask_AsyncTrace::AsyncLineTraceByChannel(nullptr, FVector::ZeroVector, FVector(0, 0, 100), ETraceTypeQuery::TraceTypeQuery1, false);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnTraceCompleted);
Task->Activate();
TestTrue("Null world trace still broadcasts Completed", Listener->bCompleted);
TestEqual("Null world trace reports no hits", Listener->HitCount, 0);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
UStaticMesh* CubeMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
if (!CubeMesh)
{
AddError(TEXT("Engine cube mesh unavailable for async trace hit scenarios."));
return false;
}
UWorld* World = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
if (!World)
{
AddError(TEXT("Failed to create a transient game world for the async trace test."));
return false;
}
DirectiveUtilAsyncTraceTestHelpers::SpawnBlockingCube(World, CubeMesh);
const FVector Start(0.0f, 0.0f, 500.0f);
const FVector End(0.0f, 0.0f, -500.0f);
const ETraceTypeQuery VisibilityChannel = ETraceTypeQuery::TraceTypeQuery1;
auto MakeListener = [](UDirectiveUtilTask_AsyncTrace* Task) -> UDirectiveUtilDelegateListener*
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnTraceCompleted);
Task->Activate();
return Listener;
};
TArray<UDirectiveUtilDelegateListener*> Listeners;
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncLineTraceByChannel(World, Start, End, VisibilityChannel, false)));
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncSphereTraceByChannel(World, Start, End, 25.0f, VisibilityChannel, false)));
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncBoxTraceByChannel(World, Start, End, FVector(25.0f), FRotator::ZeroRotator, VisibilityChannel, false)));
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncCapsuleTraceByChannel(World, Start, End, 25.0f, 50.0f, VisibilityChannel, false)));
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickTraceWorld(this, World, Listeners, 120));
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMoveToLocationTest, "DirectiveUtilities.AsyncTaskMoveToLocationTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
{
AddExpectedMessagePlain(TEXT("Controller, pawn, or world is unavailable while moving to location. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
AddExpectedMessagePlain(TEXT("Controller or pawn has been destroyed while moving to location. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_MoveToLocation* Task = UDirectiveUtilTask_MoveToLocation::MoveToLocation(nullptr, nullptr, FVector(100.0f, 0.0f, 0.0f));
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
Task->Activate();
TestTrue("Null controller broadcasts Completed", Listener->bCompleted);
TestFalse("Null controller reports failure", Listener->bLastSuccess);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
UWorld* World = UWorld::CreateWorld(EWorldType::Game, false);
if (!World)
{
AddError(TEXT("Failed to create a transient world for the move-to-location test."));
return false;
}
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Game);
WorldContext.SetCurrentWorld(World);
{
APlayerController* Controller = World->SpawnActor<APlayerController>();
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_MoveToLocation* Task = UDirectiveUtilTask_MoveToLocation::MoveToLocation(World, Controller, FVector(100.0f, 0.0f, 0.0f));
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
Task->Activate();
TestTrue("Controller without a pawn broadcasts Completed", Listener->bCompleted);
TestFalse("Controller without a pawn reports failure", Listener->bLastSuccess);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
{
APlayerController* Controller = World->SpawnActor<APlayerController>();
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_MoveToLocation* Task = UDirectiveUtilTask_MoveToLocation::MoveToLocation(World, Controller, FVector(100.0f, 0.0f, 0.0f));
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
Task->EndTask();
TestTrue("EndTask broadcasts Completed", Listener->bCompleted);
TestFalse("EndTask reports failure", Listener->bLastSuccess);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
{
APlayerController* Controller = World->SpawnActor<APlayerController>();
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_MoveToLocation* Task = UDirectiveUtilTask_MoveToLocation::MoveToLocation(World, Controller, FVector(100.0f, 0.0f, 0.0f));
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
Task->EndTask();
Task->EndTask();
TestEqual("Double EndTask broadcasts Completed exactly once", Listener->CompletedCount, 1);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
{
APlayerController* Controller = World->SpawnActor<APlayerController>();
ADefaultPawn* Pawn = World->SpawnActor<ADefaultPawn>();
if (Controller && Pawn)
{
Controller->SetPawn(Pawn);
UDirectiveUtilTestMoveToLocationTask* Task = NewObject<UDirectiveUtilTestMoveToLocationTask>();
Task->Configure(Controller, FVector(1000.0f, 0.0f, 0.0f), true);
Task->RegisterTimersForTest(World);
TestTrue("Move to location registers both lifecycle timers", Task->HasRegisteredTimers());
Task->ClearController();
Task->Complete();
TestFalse("Move to location clears timers without a controller", Task->HasRegisteredTimers());
Task->Configure(Controller, FVector(1000.0f, 0.0f, 0.0f), true);
Task->Activate();
TestFalse("A completed move to location should not restart", Task->HasRegisteredTimers());
}
}
GEngine->DestroyWorldContext(World);
World->DestroyWorld(false);
{
UWorld* MoveWorld = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
if (!MoveWorld)
{
AddError(TEXT("Failed to create a transient game world for the no-navigation move scenario."));
return false;
}
APlayerController* Controller = MoveWorld->SpawnActor<APlayerController>();
ADefaultPawn* Pawn = MoveWorld->SpawnActor<ADefaultPawn>(FVector::ZeroVector, FRotator::ZeroRotator);
if (!Controller || !Pawn)
{
AddError(TEXT("Failed to spawn a controller or pawn for the no-navigation move scenario."));
GEngine->DestroyWorldContext(MoveWorld);
MoveWorld->DestroyWorld(false);
return false;
}
Controller->SetPawn(Pawn);
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_MoveToLocation* Task = UDirectiveUtilTask_MoveToLocation::MoveToLocation(MoveWorld, Controller, FVector(10000.0f, 0.0f, 0.0f), 100.0f, false);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
Task->Activate();
TestTrue("A location move without navigation broadcasts Completed", Listener->bCompleted);
TestFalse("A location move without navigation reports failure", Listener->bLastSuccess);
TestEqual("A location move without navigation completes exactly once", Listener->CompletedCount, 1);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
GEngine->DestroyWorldContext(MoveWorld);
MoveWorld->DestroyWorld(false);
}
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMoveToActorTest, "DirectiveUtilities.AsyncTaskMoveToActorTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters)
{
AddExpectedMessagePlain(TEXT("Controller, pawn, goal, or world is unavailable while moving to actor. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
AddExpectedMessagePlain(TEXT("Controller, pawn, or world is unavailable while moving to actor. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
AddExpectedMessagePlain(TEXT("Controller, pawn, or goal has been destroyed while moving to actor. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_MoveToActor* Task = UDirectiveUtilTask_MoveToActor::MoveToActor(nullptr, nullptr, nullptr);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
Task->Activate();
TestTrue("Null controller broadcasts Completed", Listener->bCompleted);
TestFalse("Null controller reports failure", Listener->bLastSuccess);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
UWorld* World = UWorld::CreateWorld(EWorldType::Game, false);
if (!World)
{
AddError(TEXT("Failed to create a transient world for the move-to-actor test."));
return false;
}
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Game);
WorldContext.SetCurrentWorld(World);
{
APlayerController* Controller = World->SpawnActor<APlayerController>();
ADefaultPawn* Pawn = World->SpawnActor<ADefaultPawn>(FVector::ZeroVector, FRotator::ZeroRotator);
if (Controller && Pawn)
{
Controller->SetPawn(Pawn);
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_MoveToActor* Task = UDirectiveUtilTask_MoveToActor::MoveToActor(World, Controller, nullptr);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
Task->Activate();
TestTrue("Null goal broadcasts Completed", Listener->bCompleted);
TestFalse("Null goal reports failure", Listener->bLastSuccess);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
else
{
AddError(TEXT("Failed to spawn a controller or pawn for the null-goal scenario."));
}
}
{
APlayerController* Controller = World->SpawnActor<APlayerController>();
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_MoveToActor* Task = UDirectiveUtilTask_MoveToActor::MoveToActor(World, Controller, nullptr);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
Task->EndTask();
Task->EndTask();
TestEqual("Double EndTask broadcasts Completed exactly once", Listener->CompletedCount, 1);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
{
APlayerController* Controller = World->SpawnActor<APlayerController>();
ADefaultPawn* Pawn = World->SpawnActor<ADefaultPawn>();
AStaticMeshActor* Goal = World->SpawnActor<AStaticMeshActor>();
if (Controller && Pawn && Goal)
{
Controller->SetPawn(Pawn);
UDirectiveUtilTestMoveToActorTask* Task = NewObject<UDirectiveUtilTestMoveToActorTask>();
Task->Configure(Controller, Goal, true);
Task->RegisterTimersForTest(World);
TestTrue("Move to actor registers both lifecycle timers", Task->HasRegisteredTimers());
Task->ClearController();
Task->Complete();
TestFalse("Move to actor clears timers without a controller", Task->HasRegisteredTimers());
Task->Configure(Controller, Goal, true);
Task->Activate();
TestFalse("A completed move to actor should not restart", Task->HasRegisteredTimers());
}
}
GEngine->DestroyWorldContext(World);
World->DestroyWorld(false);
{
UWorld* MoveWorld = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
if (!MoveWorld)
{
AddError(TEXT("Failed to create a transient game world for the no-navigation move scenario."));
return false;
}
APlayerController* Controller = MoveWorld->SpawnActor<APlayerController>();
ADefaultPawn* Pawn = MoveWorld->SpawnActor<ADefaultPawn>(FVector::ZeroVector, FRotator::ZeroRotator);
AStaticMeshActor* GoalActor = MoveWorld->SpawnActor<AStaticMeshActor>(FVector(10000.0f, 0.0f, 0.0f), FRotator::ZeroRotator);
if (!Controller || !Pawn || !GoalActor)
{
AddError(TEXT("Failed to spawn a controller, pawn, or goal for the no-navigation move scenario."));
GEngine->DestroyWorldContext(MoveWorld);
MoveWorld->DestroyWorld(false);
return false;
}
Controller->SetPawn(Pawn);
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_MoveToActor* Task = UDirectiveUtilTask_MoveToActor::MoveToActor(MoveWorld, Controller, GoalActor, 100.0f, false);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
Task->Activate();
TestTrue("An actor move without navigation broadcasts Completed", Listener->bCompleted);
TestFalse("An actor move without navigation reports failure", Listener->bLastSuccess);
TestEqual("An actor move without navigation completes exactly once", Listener->CompletedCount, 1);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
GEngine->DestroyWorldContext(MoveWorld);
MoveWorld->DestroyWorld(false);
}
return true;
}