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

1130 lines
39 KiB
C++

// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Tasks/DirectiveUtilTask_Delay.h"
#include "Tasks/DirectiveUtilTask_Flow.h"
#include "Tasks/DirectiveUtilTask_AsyncLoadAsset.h"
#include "Tests/DirectiveUtilTestObject.h"
#include "Engine/World.h"
#include "Engine/Engine.h"
#include "Engine/GameInstance.h"
#include "Engine/StaticMesh.h"
#include "Engine/StaticMeshActor.h"
#include "TimerManager.h"
#include "UObject/SoftObjectPath.h"
#include "UObject/SoftObjectPtr.h"
#include "HAL/PlatformTime.h"
#include "Misc/AutomationTest.h"
#include <limits>
namespace DirectiveUtilAsyncTaskTestHelpers
{
UDirectiveUtilDelegateListener* CreateScenarioListener()
{
UGameInstance* GameInstance = NewObject<UGameInstance>(GEngine);
if (!GameInstance)
{
return nullptr;
}
GameInstance->AddToRoot();
GameInstance->InitializeStandalone();
UWorld* World = GameInstance->GetWorld();
if (!World)
{
GameInstance->Shutdown();
GameInstance->RemoveFromRoot();
return nullptr;
}
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
Listener->ScenarioWorld = World;
Listener->ScenarioGameInstance = GameInstance;
return Listener;
}
void DestroyScenario(UDirectiveUtilDelegateListener* Listener)
{
if (!Listener)
{
return;
}
UGameInstance* GameInstance = Listener->ScenarioGameInstance.Get();
Listener->Keepalive = nullptr;
Listener->ScenarioWorld = nullptr;
Listener->ScenarioGameInstance = nullptr;
if (GameInstance)
{
GameInstance->Shutdown();
GameInstance->RemoveFromRoot();
}
Listener->RemoveFromRoot();
}
UDirectiveUtilDelegateListener* StartDelayScenario(float Duration, bool bCancel, bool bUseEndTask = false)
{
UDirectiveUtilDelegateListener* Listener = CreateScenarioListener();
if (!Listener)
{
return nullptr;
}
UDirectiveUtilTask_Delay* Task = UDirectiveUtilTask_Delay::CancellableDelay(Listener->ScenarioWorld, Duration);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnCompleted);
Task->Activate();
if (bCancel)
{
if (bUseEndTask)
{
Task->EndTask();
}
else
{
Task->Cancel();
}
}
return Listener;
}
UDirectiveUtilDelegateListener* StartDurationScenario(float Duration, float UpdateInterval, bool bCancel)
{
UDirectiveUtilDelegateListener* Listener = CreateScenarioListener();
if (!Listener)
{
return nullptr;
}
UDirectiveUtilTask_UpdateForDuration* Task = UDirectiveUtilTask_UpdateForDuration::UpdateForDuration(
Listener->ScenarioWorld,
Duration,
UpdateInterval);
Listener->Keepalive = Task;
Task->Updated.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnDurationUpdated);
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnCompleted);
Task->Activate();
if (bCancel)
{
Task->Cancel();
}
return Listener;
}
UDirectiveUtilDelegateListener* StartRepeatScenario(
int32 Count,
float Interval,
float InitialDelay,
bool bCancel,
int32 NextIndex = 0)
{
UDirectiveUtilDelegateListener* Listener = CreateScenarioListener();
if (!Listener)
{
return nullptr;
}
UDirectiveUtilTask_RepeatWithInterval* Task = UDirectiveUtilTask_RepeatWithInterval::RepeatWithInterval(
Listener->ScenarioWorld,
Count,
Interval,
InitialDelay);
Listener->Keepalive = Task;
Task->Iteration.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnRepeatIteration);
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnCompleted);
#if WITH_DEV_AUTOMATION_TESTS
Task->SetNextIndexForTesting(NextIndex);
#endif
Task->Activate();
if (bCancel)
{
Task->Cancel();
}
return Listener;
}
}
DEFINE_LATENT_AUTOMATION_COMMAND_FOUR_PARAMETER(FDirectiveUtilTickDelayScenario, FAutomationTestBase*, Test, UDirectiveUtilDelegateListener*, Listener, int32, FramesRemaining, bool, bExpectComplete);
bool FDirectiveUtilTickDelayScenario::Update()
{
if (!Listener)
{
return true;
}
UWorld* World = Listener->ScenarioWorld.Get();
if (World)
{
World->GetTimerManager().Tick(0.1f);
}
const bool bBudgetExhausted = (--FramesRemaining <= 0);
if (Listener->bCompleted || bBudgetExhausted)
{
if (bExpectComplete)
{
Test->TestTrue(TEXT("Cancellable delay broadcasts Completed once its timer elapses"), Listener->bCompleted);
Test->TestEqual(TEXT("Cancellable delay broadcasts Completed exactly once"), Listener->CompletedCount, 1);
}
else
{
Test->TestFalse(TEXT("Cancel prevents the delay from firing Completed"), Listener->bCompleted);
}
DirectiveUtilAsyncTaskTestHelpers::DestroyScenario(Listener);
return true;
}
return false;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilDelayTaskTest, "DirectiveUtilities.AsyncTaskDelayTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilDelayTaskTest::RunTest(const FString& Parameters)
{
AddExpectedMessagePlain(TEXT("Cancellable Delay failed to activate. World is null."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
if (UDirectiveUtilDelegateListener* Completed = DirectiveUtilAsyncTaskTestHelpers::StartDelayScenario(0.05f, false))
{
if (const UDirectiveUtilTask_Delay* Task = Cast<UDirectiveUtilTask_Delay>(Completed->Keepalive))
{
TestTrue(TEXT("Cancellable delay is active after activation"), Task->IsActive());
}
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickDelayScenario(this, Completed, 600, true));
}
else
{
AddError(TEXT("Failed to create a transient world for the delay completion scenario."));
}
if (UDirectiveUtilDelegateListener* Cancelled = DirectiveUtilAsyncTaskTestHelpers::StartDelayScenario(0.05f, true))
{
if (UDirectiveUtilTask_Delay* Task = Cast<UDirectiveUtilTask_Delay>(Cancelled->Keepalive))
{
TestFalse(TEXT("Cancelled delay is inactive"), Task->IsActive());
Task->Cancel();
}
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickDelayScenario(this, Cancelled, 10, false));
}
else
{
AddError(TEXT("Failed to create a transient world for the delay cancellation scenario."));
}
if (UDirectiveUtilDelegateListener* LegacyCancelled = DirectiveUtilAsyncTaskTestHelpers::StartDelayScenario(0.05f, true, true))
{
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickDelayScenario(this, LegacyCancelled, 10, false));
}
else
{
AddError(TEXT("Failed to create the EndTask compatibility scenario."));
}
if (UDirectiveUtilDelegateListener* Immediate = DirectiveUtilAsyncTaskTestHelpers::StartDelayScenario(0.0f, false))
{
TestFalse(TEXT("A non-positive delay does not complete during activation"), Immediate->bCompleted);
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickDelayScenario(this, Immediate, 3, true));
}
else
{
AddError(TEXT("Failed to create the immediate delay scenario."));
}
if (UDirectiveUtilDelegateListener* NonFinite = DirectiveUtilAsyncTaskTestHelpers::StartDelayScenario(
std::numeric_limits<float>::quiet_NaN(),
false))
{
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickDelayScenario(this, NonFinite, 3, true));
}
else
{
AddError(TEXT("Failed to create the non-finite delay scenario."));
}
UDirectiveUtilTask_Delay* NullWorldTask = UDirectiveUtilTask_Delay::CancellableDelay(nullptr, 0.05f);
if (TestNotNull("CancellableDelay returns a task even with a null context", NullWorldTask))
{
NullWorldTask->AddToRoot();
NullWorldTask->Activate();
NullWorldTask->RemoveFromRoot();
}
return true;
}
DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(
FDirectiveUtilTickDurationCadenceScenario,
FAutomationTestBase*, Test,
UDirectiveUtilDelegateListener*, Listener,
double, Deadline);
bool FDirectiveUtilTickDurationCadenceScenario::Update()
{
if (!Listener)
{
Listener = DirectiveUtilAsyncTaskTestHelpers::StartDurationScenario(0.1f, 0.02f, false);
Deadline = FPlatformTime::Seconds() + 2.0;
if (!Listener)
{
Test->AddError(TEXT("Failed to create the Update for Duration cadence scenario."));
return true;
}
return false;
}
if (UWorld* World = Listener->ScenarioWorld.Get())
{
FTimerManager& TimerManager = World->GetTimerManager();
if (!TimerManager.HasBeenTickedThisFrame())
{
TimerManager.Tick(0.01f);
}
}
if (!Listener->bCompleted && FPlatformTime::Seconds() < Deadline)
{
return false;
}
Test->TestTrue(TEXT("Update for Duration completes with a positive update interval"), Listener->bCompleted);
Test->TestTrue(TEXT("Update for Duration emits repeated interval updates"), Listener->UpdatedCount >= 4);
Test->TestTrue(TEXT("Update for Duration does not emit excessive interval updates"), Listener->UpdatedCount <= 7);
Test->TestTrue(TEXT("Update for Duration emits progress before its final update"), Listener->UpdateAlphas.Num() > 1 && Listener->UpdateAlphas[0] > 0.0f && Listener->UpdateAlphas[0] < 1.0f);
Test->TestEqual(TEXT("Update for Duration interval scenario completes once"), Listener->CompletedCount, 1);
if (const UDirectiveUtilTask_UpdateForDuration* Task = Cast<UDirectiveUtilTask_UpdateForDuration>(Listener->Keepalive))
{
Test->TestFalse(TEXT("Completed Update for Duration is inactive"), Task->IsActive());
}
DirectiveUtilAsyncTaskTestHelpers::DestroyScenario(Listener);
return true;
}
DEFINE_LATENT_AUTOMATION_COMMAND_FOUR_PARAMETER(
FDirectiveUtilCancelDurationAfterUpdateScenario,
FAutomationTestBase*, Test,
UDirectiveUtilDelegateListener*, Listener,
double, Deadline,
int32, UpdatesAtCancellation);
bool FDirectiveUtilCancelDurationAfterUpdateScenario::Update()
{
if (!Listener)
{
Listener = DirectiveUtilAsyncTaskTestHelpers::StartDurationScenario(0.5f, 0.02f, false);
Deadline = FPlatformTime::Seconds() + 2.0;
if (!Listener)
{
Test->AddError(TEXT("Failed to create the Update for Duration cancellation scenario."));
return true;
}
return false;
}
if (UWorld* World = Listener->ScenarioWorld.Get())
{
FTimerManager& TimerManager = World->GetTimerManager();
if (!TimerManager.HasBeenTickedThisFrame())
{
TimerManager.Tick(0.01f);
}
}
if (UpdatesAtCancellation == 0 && Listener->UpdatedCount > 0)
{
UpdatesAtCancellation = Listener->UpdatedCount;
if (UDirectiveUtilTask_UpdateForDuration* Task = Cast<UDirectiveUtilTask_UpdateForDuration>(Listener->Keepalive))
{
Task->Cancel();
}
Deadline = FPlatformTime::Seconds() + 0.05;
}
if (FPlatformTime::Seconds() < Deadline)
{
return false;
}
Test->TestTrue(TEXT("Update for Duration reached an update before cancellation"), UpdatesAtCancellation > 0);
Test->TestFalse(TEXT("Cancelling Update for Duration after an update prevents completion"), Listener->bCompleted);
Test->TestEqual(TEXT("Cancelling Update for Duration stops later updates"), Listener->UpdatedCount, UpdatesAtCancellation);
if (const UDirectiveUtilTask_UpdateForDuration* Task = Cast<UDirectiveUtilTask_UpdateForDuration>(Listener->Keepalive))
{
Test->TestFalse(TEXT("Cancelled Update for Duration is inactive"), Task->IsActive());
}
DirectiveUtilAsyncTaskTestHelpers::DestroyScenario(Listener);
return true;
}
DEFINE_LATENT_AUTOMATION_COMMAND_FIVE_PARAMETER(
FDirectiveUtilTickDurationScenario,
FAutomationTestBase*, Test,
UDirectiveUtilDelegateListener*, Listener,
int32, FramesRemaining,
bool, bExpectComplete,
float, ExpectedDuration);
bool FDirectiveUtilTickDurationScenario::Update()
{
if (!Listener)
{
return true;
}
if (UWorld* World = Listener->ScenarioWorld.Get())
{
World->GetTimerManager().Tick(0.1f);
}
const bool bBudgetExhausted = --FramesRemaining <= 0;
if (!Listener->bCompleted && !bBudgetExhausted)
{
return false;
}
if (bExpectComplete)
{
Test->TestTrue(TEXT("Update for Duration fires Completed"), Listener->bCompleted);
Test->TestEqual(TEXT("Update for Duration fires Completed once"), Listener->CompletedCount, 1);
Test->TestTrue(TEXT("Update for Duration emits at least one update"), Listener->UpdatedCount > 0);
Test->TestEqual(TEXT("Update for Duration ends at alpha 1"), Listener->LastAlpha, 1.0f);
Test->TestEqual(TEXT("Update for Duration reports its final elapsed time"), Listener->LastElapsedTime, FMath::Max(ExpectedDuration, 0.0f));
float PreviousElapsedTime = 0.0f;
for (int32 UpdateIndex = 0; UpdateIndex < Listener->UpdatedCount; ++UpdateIndex)
{
Test->TestTrue(TEXT("Update for Duration elapsed time is monotonic"), Listener->UpdateElapsedTimes[UpdateIndex] >= PreviousElapsedTime);
Test->TestTrue(TEXT("Update for Duration delta time is non-negative"), Listener->UpdateDeltaTimes[UpdateIndex] >= 0.0f);
Test->TestTrue(TEXT("Update for Duration alpha stays in range"), Listener->UpdateAlphas[UpdateIndex] >= 0.0f && Listener->UpdateAlphas[UpdateIndex] <= 1.0f);
PreviousElapsedTime = Listener->UpdateElapsedTimes[UpdateIndex];
}
}
else
{
Test->TestFalse(TEXT("Cancel prevents Update for Duration from firing Completed"), Listener->bCompleted);
Test->TestEqual(TEXT("Cancel prevents Update for Duration callbacks"), Listener->UpdatedCount, 0);
}
DirectiveUtilAsyncTaskTestHelpers::DestroyScenario(Listener);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FDirectiveUtilUpdateForDurationTaskTest,
"DirectiveUtilities.AsyncTaskUpdateForDurationTests",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilUpdateForDurationTaskTest::RunTest(const FString& Parameters)
{
AddExpectedMessagePlain(TEXT("Update for Duration failed to activate. World is null."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
if (UDirectiveUtilDelegateListener* Completed = DirectiveUtilAsyncTaskTestHelpers::StartDurationScenario(0.01f, 0.002f, false))
{
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickDurationScenario(this, Completed, 600, true, 0.01f));
}
else
{
AddError(TEXT("Failed to create the Update for Duration completion scenario."));
}
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickDurationCadenceScenario(this, nullptr, 0.0));
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilCancelDurationAfterUpdateScenario(this, nullptr, 0.0, 0));
if (UDirectiveUtilDelegateListener* Immediate = DirectiveUtilAsyncTaskTestHelpers::StartDurationScenario(0.0f, 0.0f, false))
{
TestFalse(TEXT("Update for Duration does not complete during activation"), Immediate->bCompleted);
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickDurationScenario(this, Immediate, 3, true, 0.0f));
}
else
{
AddError(TEXT("Failed to create the immediate Update for Duration scenario."));
}
if (UDirectiveUtilDelegateListener* NonFinite = DirectiveUtilAsyncTaskTestHelpers::StartDurationScenario(std::numeric_limits<float>::quiet_NaN(), 0.0f, false))
{
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickDurationScenario(this, NonFinite, 3, true, 0.0f));
}
else
{
AddError(TEXT("Failed to create the non-finite Update for Duration scenario."));
}
if (UDirectiveUtilDelegateListener* Cancelled = DirectiveUtilAsyncTaskTestHelpers::StartDurationScenario(1.0f, 0.0f, true))
{
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickDurationScenario(this, Cancelled, 3, false, 1.0f));
}
else
{
AddError(TEXT("Failed to create the cancelled Update for Duration scenario."));
}
if (UDirectiveUtilDelegateListener* PerTick = DirectiveUtilAsyncTaskTestHelpers::StartDurationScenario(10.0f, 0.0f, false))
{
if (UWorld* World = PerTick->ScenarioWorld.Get())
{
World->GetTimerManager().Tick(1.0f);
TestEqual(TEXT("A zero update interval emits at most one update per timer tick"), PerTick->UpdatedCount, 1);
}
if (UDirectiveUtilTask_UpdateForDuration* Task = Cast<UDirectiveUtilTask_UpdateForDuration>(PerTick->Keepalive))
{
Task->Cancel();
}
DirectiveUtilAsyncTaskTestHelpers::DestroyScenario(PerTick);
}
else
{
AddError(TEXT("Failed to create the per-tick Update for Duration scenario."));
}
UDirectiveUtilTask_UpdateForDuration* NullWorldTask = UDirectiveUtilTask_UpdateForDuration::UpdateForDuration(nullptr, 1.0f);
if (TestNotNull("UpdateForDuration returns a task with a null context", NullWorldTask))
{
NullWorldTask->AddToRoot();
NullWorldTask->Activate();
NullWorldTask->RemoveFromRoot();
}
return true;
}
DEFINE_LATENT_AUTOMATION_COMMAND_FIVE_PARAMETER(
FDirectiveUtilTickRepeatCadenceScenario,
FAutomationTestBase*, Test,
UDirectiveUtilDelegateListener*, Listener,
double, Deadline,
int32, TickCount,
TArray<int32>, IterationTicks);
bool FDirectiveUtilTickRepeatCadenceScenario::Update()
{
if (!Listener)
{
Listener = DirectiveUtilAsyncTaskTestHelpers::StartRepeatScenario(3, 0.03f, 0.04f, false);
Deadline = FPlatformTime::Seconds() + 2.0;
if (!Listener)
{
Test->AddError(TEXT("Failed to create the Repeat with Interval cadence scenario."));
return true;
}
return false;
}
if (UWorld* World = Listener->ScenarioWorld.Get())
{
FTimerManager& TimerManager = World->GetTimerManager();
if (!TimerManager.HasBeenTickedThisFrame())
{
TimerManager.Tick(0.01f);
}
}
++TickCount;
while (IterationTicks.Num() < Listener->IterationCount)
{
IterationTicks.Add(TickCount);
}
if (!Listener->bCompleted && FPlatformTime::Seconds() < Deadline)
{
return false;
}
Test->TestTrue(TEXT("Repeat with Interval completes with positive delays"), Listener->bCompleted);
Test->TestEqual(TEXT("Repeat with Interval emits every delayed iteration"), IterationTicks.Num(), 3);
if (IterationTicks.Num() == 3)
{
Test->TestTrue(TEXT("Repeat with Interval respects its initial delay"), IterationTicks[0] > 1);
Test->TestTrue(TEXT("Repeat with Interval separates its first interval"), IterationTicks[1] > IterationTicks[0]);
Test->TestTrue(TEXT("Repeat with Interval separates its second interval"), IterationTicks[2] > IterationTicks[1]);
}
Test->TestEqual(TEXT("Repeat with Interval delayed scenario completes once"), Listener->CompletedCount, 1);
if (const UDirectiveUtilTask_RepeatWithInterval* Task = Cast<UDirectiveUtilTask_RepeatWithInterval>(Listener->Keepalive))
{
Test->TestFalse(TEXT("Completed Repeat with Interval is inactive"), Task->IsActive());
}
DirectiveUtilAsyncTaskTestHelpers::DestroyScenario(Listener);
return true;
}
DEFINE_LATENT_AUTOMATION_COMMAND_FOUR_PARAMETER(
FDirectiveUtilCancelRepeatAfterIterationScenario,
FAutomationTestBase*, Test,
UDirectiveUtilDelegateListener*, Listener,
double, Deadline,
int32, IterationsAtCancellation);
bool FDirectiveUtilCancelRepeatAfterIterationScenario::Update()
{
if (!Listener)
{
Listener = DirectiveUtilAsyncTaskTestHelpers::StartRepeatScenario(10, 0.02f, 0.0f, false);
Deadline = FPlatformTime::Seconds() + 2.0;
if (!Listener)
{
Test->AddError(TEXT("Failed to create the Repeat with Interval cancellation scenario."));
return true;
}
return false;
}
if (UWorld* World = Listener->ScenarioWorld.Get())
{
FTimerManager& TimerManager = World->GetTimerManager();
if (!TimerManager.HasBeenTickedThisFrame())
{
TimerManager.Tick(0.01f);
}
}
if (IterationsAtCancellation == 0 && Listener->IterationCount > 0)
{
IterationsAtCancellation = Listener->IterationCount;
if (UDirectiveUtilTask_RepeatWithInterval* Task = Cast<UDirectiveUtilTask_RepeatWithInterval>(Listener->Keepalive))
{
Task->Cancel();
}
Deadline = FPlatformTime::Seconds() + 0.05;
}
if (FPlatformTime::Seconds() < Deadline)
{
return false;
}
Test->TestTrue(TEXT("Repeat with Interval reached an iteration before cancellation"), IterationsAtCancellation > 0);
Test->TestFalse(TEXT("Cancelling Repeat with Interval after an iteration prevents completion"), Listener->bCompleted);
Test->TestEqual(TEXT("Cancelling Repeat with Interval stops later iterations"), Listener->IterationCount, IterationsAtCancellation);
if (const UDirectiveUtilTask_RepeatWithInterval* Task = Cast<UDirectiveUtilTask_RepeatWithInterval>(Listener->Keepalive))
{
Test->TestFalse(TEXT("Cancelled Repeat with Interval is inactive"), Task->IsActive());
}
DirectiveUtilAsyncTaskTestHelpers::DestroyScenario(Listener);
return true;
}
DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(
FDirectiveUtilTickInfiniteRepeatScenario,
FAutomationTestBase*, Test,
UDirectiveUtilDelegateListener*, Listener,
int32, FramesRemaining);
DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(
FDirectiveUtilTickRepeatRolloverScenario,
FAutomationTestBase*, Test,
UDirectiveUtilDelegateListener*, Listener,
int32, FramesRemaining);
bool FDirectiveUtilTickRepeatRolloverScenario::Update()
{
if (!Listener)
{
return true;
}
if (UWorld* World = Listener->ScenarioWorld.Get())
{
World->GetTimerManager().Tick(0.1f);
}
if (Listener->IterationCount < 2 && --FramesRemaining > 0)
{
return false;
}
Test->TestTrue(
TEXT("An infinite Repeat with Interval wraps its index without signed overflow"),
Listener->IterationIndices.Num() >= 2
&& Listener->IterationIndices[0] == MAX_int32
&& Listener->IterationIndices[1] == 0);
if (UDirectiveUtilTask_RepeatWithInterval* Task = Cast<UDirectiveUtilTask_RepeatWithInterval>(Listener->Keepalive))
{
Task->Cancel();
}
DirectiveUtilAsyncTaskTestHelpers::DestroyScenario(Listener);
return true;
}
bool FDirectiveUtilTickInfiniteRepeatScenario::Update()
{
if (!Listener)
{
return true;
}
if (UWorld* World = Listener->ScenarioWorld.Get())
{
World->GetTimerManager().Tick(0.1f);
}
if (--FramesRemaining > 0)
{
return false;
}
Test->TestFalse(TEXT("An infinite Repeat with Interval does not complete on its own"), Listener->bCompleted);
Test->TestTrue(TEXT("An infinite Repeat with Interval keeps iterating"), Listener->IterationCount >= 3);
TArray<int32> ExpectedRemaining;
ExpectedRemaining.Init(-1, Listener->IterationCount);
Test->TestEqual(TEXT("An infinite Repeat with Interval reports Remaining as -1"),
Listener->IterationRemaining, ExpectedRemaining);
if (UDirectiveUtilTask_RepeatWithInterval* Task = Cast<UDirectiveUtilTask_RepeatWithInterval>(Listener->Keepalive))
{
const int32 IterationsAtCancel = Listener->IterationCount;
Task->Cancel();
if (UWorld* World = Listener->ScenarioWorld.Get())
{
World->GetTimerManager().Tick(0.1f);
World->GetTimerManager().Tick(0.1f);
}
Test->TestEqual(TEXT("Cancel stops further infinite iterations"), Listener->IterationCount, IterationsAtCancel);
Test->TestFalse(TEXT("Cancel does not fire Completed for an infinite Repeat"), Listener->bCompleted);
Test->TestFalse(TEXT("Cancelled infinite Repeat is inactive"), Task->IsActive());
}
DirectiveUtilAsyncTaskTestHelpers::DestroyScenario(Listener);
return true;
}
DEFINE_LATENT_AUTOMATION_COMMAND_FIVE_PARAMETER(
FDirectiveUtilTickRepeatScenario,
FAutomationTestBase*, Test,
UDirectiveUtilDelegateListener*, Listener,
int32, FramesRemaining,
int32, ExpectedIterations,
bool, bExpectComplete);
bool FDirectiveUtilTickRepeatScenario::Update()
{
if (!Listener)
{
return true;
}
if (UWorld* World = Listener->ScenarioWorld.Get())
{
World->GetTimerManager().Tick(0.1f);
}
const bool bBudgetExhausted = --FramesRemaining <= 0;
if (!Listener->bCompleted && !bBudgetExhausted)
{
return false;
}
if (bExpectComplete)
{
Test->TestTrue(TEXT("Repeat with Interval fires Completed"), Listener->bCompleted);
Test->TestEqual(TEXT("Repeat with Interval fires Completed once"), Listener->CompletedCount, 1);
Test->TestEqual(TEXT("Repeat with Interval emits the requested iteration count"), Listener->IterationCount, ExpectedIterations);
TArray<int32> ExpectedIndices;
TArray<int32> ExpectedRemaining;
for (int32 Index = 0; Index < ExpectedIterations; ++Index)
{
ExpectedIndices.Add(Index);
ExpectedRemaining.Add(ExpectedIterations - Index - 1);
}
Test->TestEqual(TEXT("Repeat with Interval reports zero-based indices"), Listener->IterationIndices, ExpectedIndices);
Test->TestEqual(TEXT("Repeat with Interval reports remaining iterations"), Listener->IterationRemaining, ExpectedRemaining);
}
else
{
Test->TestFalse(TEXT("Cancel prevents Repeat with Interval from firing Completed"), Listener->bCompleted);
Test->TestEqual(TEXT("Cancel prevents Repeat with Interval callbacks"), Listener->IterationCount, 0);
}
DirectiveUtilAsyncTaskTestHelpers::DestroyScenario(Listener);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FDirectiveUtilRepeatWithIntervalTaskTest,
"DirectiveUtilities.AsyncTaskRepeatWithIntervalTests",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilRepeatWithIntervalTaskTest::RunTest(const FString& Parameters)
{
AddExpectedMessagePlain(TEXT("Repeat with Interval failed to activate. World is null."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
if (UDirectiveUtilDelegateListener* Repeated = DirectiveUtilAsyncTaskTestHelpers::StartRepeatScenario(3, 0.0f, 0.0f, false))
{
TestEqual(TEXT("Repeat with Interval does not iterate during activation"), Repeated->IterationCount, 0);
if (UWorld* World = Repeated->ScenarioWorld.Get())
{
World->GetTimerManager().Tick(1.0f);
TestEqual(TEXT("A zero interval emits at most one iteration per timer tick"), Repeated->IterationCount, 1);
}
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickRepeatScenario(this, Repeated, 6, 3, true));
}
else
{
AddError(TEXT("Failed to create the Repeat with Interval scenario."));
}
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickRepeatCadenceScenario(this, nullptr, 0.0, 0, TArray<int32>()));
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilCancelRepeatAfterIterationScenario(this, nullptr, 0.0, 0));
if (UDirectiveUtilDelegateListener* Empty = DirectiveUtilAsyncTaskTestHelpers::StartRepeatScenario(0, 0.1f, 0.1f, false))
{
TestFalse(TEXT("An empty Repeat with Interval does not complete during activation"), Empty->bCompleted);
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickRepeatScenario(this, Empty, 3, 0, true));
}
else
{
AddError(TEXT("Failed to create the empty Repeat with Interval scenario."));
}
if (UDirectiveUtilDelegateListener* Infinite = DirectiveUtilAsyncTaskTestHelpers::StartRepeatScenario(-1, 0.0f, 0.0f, false))
{
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickInfiniteRepeatScenario(this, Infinite, 5));
}
else
{
AddError(TEXT("Failed to create the infinite Repeat with Interval scenario."));
}
#if WITH_DEV_AUTOMATION_TESTS
if (UDirectiveUtilDelegateListener* Rollover = DirectiveUtilAsyncTaskTestHelpers::StartRepeatScenario(
-1, 0.0f, 0.0f, false, MAX_int32))
{
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickRepeatRolloverScenario(this, Rollover, 5));
}
else
{
AddError(TEXT("Failed to create the repeat-index rollover scenario."));
}
#endif
if (UDirectiveUtilDelegateListener* NegativeCount = DirectiveUtilAsyncTaskTestHelpers::StartRepeatScenario(-4, 0.1f, 0.1f, false))
{
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickRepeatScenario(this, NegativeCount, 3, 0, true));
}
else
{
AddError(TEXT("Failed to create the negative-count Repeat with Interval scenario."));
}
if (UDirectiveUtilDelegateListener* NegativeTiming = DirectiveUtilAsyncTaskTestHelpers::StartRepeatScenario(2, -1.0f, -1.0f, false))
{
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickRepeatScenario(this, NegativeTiming, 5, 2, true));
}
else
{
AddError(TEXT("Failed to create the negative-timing Repeat with Interval scenario."));
}
if (UDirectiveUtilDelegateListener* NonFinite = DirectiveUtilAsyncTaskTestHelpers::StartRepeatScenario(
2,
std::numeric_limits<float>::infinity(),
std::numeric_limits<float>::quiet_NaN(),
false))
{
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickRepeatScenario(this, NonFinite, 5, 2, true));
}
else
{
AddError(TEXT("Failed to create the non-finite Repeat with Interval scenario."));
}
if (UDirectiveUtilDelegateListener* Cancelled = DirectiveUtilAsyncTaskTestHelpers::StartRepeatScenario(3, 0.0f, 0.0f, true))
{
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickRepeatScenario(this, Cancelled, 3, 0, false));
}
else
{
AddError(TEXT("Failed to create the cancelled Repeat with Interval scenario."));
}
UDirectiveUtilTask_RepeatWithInterval* NullWorldTask = UDirectiveUtilTask_RepeatWithInterval::RepeatWithInterval(nullptr, 1, 0.0f);
if (TestNotNull("RepeatWithInterval returns a task with a null context", NullWorldTask))
{
NullWorldTask->AddToRoot();
NullWorldTask->Activate();
NullWorldTask->RemoveFromRoot();
}
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FDirectiveUtilAsyncTaskShutdownTest,
"DirectiveUtilities.AsyncTaskShutdownTests",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilAsyncTaskShutdownTest::RunTest(const FString& Parameters)
{
auto VerifyShutdown = [this](UDirectiveUtilDelegateListener* Listener, const FString& ScenarioName) {
if (!Listener)
{
AddError(FString::Printf(TEXT("Failed to create the %s shutdown scenario."), *ScenarioName));
return;
}
UCancellableAsyncAction* Task = Cast<UCancellableAsyncAction>(Listener->Keepalive);
UGameInstance* GameInstance = Listener->ScenarioGameInstance.Get();
UWorld* World = Listener->ScenarioWorld.Get();
if (!TestNotNull(*FString::Printf(TEXT("%s creates a cancellable task"), *ScenarioName), Task)
|| !TestNotNull(*FString::Printf(TEXT("%s creates a game instance"), *ScenarioName), GameInstance))
{
DirectiveUtilAsyncTaskTestHelpers::DestroyScenario(Listener);
return;
}
FWorldDelegates::OnWorldCleanup.Broadcast(World, true, true);
TestFalse(*FString::Printf(TEXT("%s is inactive after world cleanup"), *ScenarioName), Task->IsActive());
if (World)
{
World->GetTimerManager().Tick(10.0f);
}
TestFalse(*FString::Printf(TEXT("%s does not complete after world cleanup"), *ScenarioName), Listener->bCompleted);
TestEqual(*FString::Printf(TEXT("%s emits no updates after world cleanup"), *ScenarioName), Listener->UpdatedCount, 0);
TestEqual(*FString::Printf(TEXT("%s emits no iterations after world cleanup"), *ScenarioName), Listener->IterationCount, 0);
DirectiveUtilAsyncTaskTestHelpers::DestroyScenario(Listener);
};
VerifyShutdown(DirectiveUtilAsyncTaskTestHelpers::StartDelayScenario(60.0f, false), TEXT("Cancellable Delay"));
VerifyShutdown(DirectiveUtilAsyncTaskTestHelpers::StartDurationScenario(60.0f, 1.0f, false), TEXT("Update for Duration"));
VerifyShutdown(DirectiveUtilAsyncTaskTestHelpers::StartRepeatScenario(60, 1.0f, 1.0f, false), TEXT("Repeat with Interval"));
return true;
}
DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FDirectiveUtilWaitForAsyncLoad, FAutomationTestBase*, Test, UDirectiveUtilDelegateListener*, Listener, int32, FramesRemaining);
bool FDirectiveUtilWaitForAsyncLoad::Update()
{
if (!Listener)
{
return true;
}
if (Listener->bFailed)
{
Test->AddError(TEXT("Async Load Asset reported failure while loading a valid engine asset."));
Listener->RemoveFromRoot();
Listener->Keepalive = nullptr;
return true;
}
if (Listener->bCompleted)
{
Test->TestNotNull(TEXT("Async Load Asset resolved the requested asset"), Listener->LastObject.Get());
Listener->RemoveFromRoot();
Listener->Keepalive = nullptr;
return true;
}
if (--FramesRemaining <= 0)
{
Test->AddError(TEXT("Async Load Asset did not complete within the frame budget."));
Listener->RemoveFromRoot();
Listener->Keepalive = nullptr;
return true;
}
return false;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilAsyncLoadAssetTest, "DirectiveUtilities.AsyncTaskLoadAssetTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilAsyncLoadAssetTest::RunTest(const FString& Parameters)
{
AddExpectedMessagePlain(TEXT("Async Load Asset failed to activate. The soft object reference is null."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_AsyncLoadAsset* Task = UDirectiveUtilTask_AsyncLoadAsset::AsyncLoadAsset(nullptr, TSoftObjectPtr<UObject>());
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnObjectCompleted);
Task->Failed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnObjectFailed);
Task->Activate();
TestTrue("Null soft reference broadcasts Failed", Listener->bFailed);
TestFalse("Null soft reference does not broadcast Completed", Listener->bCompleted);
Listener->RemoveFromRoot();
Listener->Keepalive = nullptr;
}
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
const TSoftObjectPtr<UObject> SoftCube(FSoftObjectPath(TEXT("/Engine/BasicShapes/Cube.Cube")));
UDirectiveUtilTask_AsyncLoadAsset* Task = UDirectiveUtilTask_AsyncLoadAsset::AsyncLoadAsset(nullptr, SoftCube);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnObjectCompleted);
Task->Failed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnObjectFailed);
Task->Activate();
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilWaitForAsyncLoad(this, Listener, 600));
}
return true;
}
DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FDirectiveUtilWaitForBatchAsyncLoad, FAutomationTestBase*, Test, UDirectiveUtilDelegateListener*, Listener, int32, FramesRemaining);
bool FDirectiveUtilWaitForBatchAsyncLoad::Update()
{
if (!Listener)
{
return true;
}
if (Listener->bCompleted)
{
Test->TestEqual(TEXT("Async Load Assets broadcasts Completed exactly once"), Listener->CompletedCount, 1);
Test->TestEqual(TEXT("Async Load Assets preserves the input slot count"), Listener->LastObjects.Num(), 3);
if (Listener->LastObjects.Num() == 3)
{
Test->TestNotNull(TEXT("Async Load Assets resolves the first asset"), Listener->LastObjects[0].Get());
Test->TestNotNull(TEXT("Async Load Assets resolves the second asset"), Listener->LastObjects[1].Get());
Test->TestNull(TEXT("Async Load Assets keeps a null slot for an unset reference"), Listener->LastObjects[2].Get());
}
Listener->RemoveFromRoot();
Listener->Keepalive = nullptr;
return true;
}
if (--FramesRemaining <= 0)
{
Test->AddError(TEXT("Async Load Assets did not complete within the frame budget."));
Listener->RemoveFromRoot();
Listener->Keepalive = nullptr;
return true;
}
return false;
}
DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FDirectiveUtilVerifyCancelledBatchLoad, FAutomationTestBase*, Test, UDirectiveUtilDelegateListener*, Listener, int32, FramesRemaining);
bool FDirectiveUtilVerifyCancelledBatchLoad::Update()
{
if (!Listener)
{
return true;
}
if (--FramesRemaining > 0)
{
return false;
}
Test->TestFalse(TEXT("Cancel prevents the batch Completed broadcast"), Listener->bCompleted);
Listener->RemoveFromRoot();
Listener->Keepalive = nullptr;
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilAsyncLoadAssetsTest, "DirectiveUtilities.AsyncTaskLoadAssetsTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilAsyncLoadAssetsTest::RunTest(const FString& Parameters)
{
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_AsyncLoadAssets* Task = UDirectiveUtilTask_AsyncLoadAssets::AsyncLoadAssets(nullptr, TArray<TSoftObjectPtr<UObject>>());
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnObjectsCompleted);
Task->Activate();
TestTrue("Empty batch broadcasts Completed immediately", Listener->bCompleted);
TestEqual("Empty batch broadcasts Completed exactly once", Listener->CompletedCount, 1);
TestEqual("Empty batch reports an empty array", Listener->LastObjects.Num(), 0);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
TArray<TSoftObjectPtr<UObject>> Assets;
Assets.Add(TSoftObjectPtr<UObject>(FSoftObjectPath(TEXT("/Engine/BasicShapes/Cube.Cube"))));
Assets.Add(TSoftObjectPtr<UObject>(FSoftObjectPath(TEXT("/Engine/BasicShapes/Sphere.Sphere"))));
Assets.Add(TSoftObjectPtr<UObject>());
UDirectiveUtilTask_AsyncLoadAssets* Task = UDirectiveUtilTask_AsyncLoadAssets::AsyncLoadAssets(nullptr, Assets);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnObjectsCompleted);
Task->Activate();
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilWaitForBatchAsyncLoad(this, Listener, 600));
}
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
TArray<TSoftObjectPtr<UObject>> Assets;
// RuntimeHost always cooks /Engine/BasicShapes. Use an asset that the
// successful batch above did not load so this remains a valid cancellation
// probe without producing a missing-package warning in packaged builds.
Assets.Add(TSoftObjectPtr<UObject>(FSoftObjectPath(TEXT("/Engine/BasicShapes/Cone.Cone"))));
UDirectiveUtilTask_AsyncLoadAssets* Task = UDirectiveUtilTask_AsyncLoadAssets::AsyncLoadAssets(nullptr, Assets);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnObjectsCompleted);
Task->Activate();
if (Listener->bCompleted)
{
AddInfo(TEXT("Batch load completed synchronously (asset already in memory); skipping the cancel scenario."));
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
else
{
Task->Cancel();
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilVerifyCancelledBatchLoad(this, Listener, 10));
}
}
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilAsyncLoadClassTest, "DirectiveUtilities.AsyncTaskLoadClassTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilAsyncLoadClassTest::RunTest(const FString& Parameters)
{
AddExpectedMessagePlain(TEXT("Async Load Class failed to activate. The soft class reference is null."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_AsyncLoadClass* Task = UDirectiveUtilTask_AsyncLoadClass::AsyncLoadClass(nullptr, TSoftClassPtr<UObject>());
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnClassCompleted);
Task->Failed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnClassFailed);
Task->Activate();
TestTrue("Null soft class broadcasts Failed", Listener->bFailed);
TestFalse("Null soft class does not broadcast Completed", Listener->bCompleted);
Listener->RemoveFromRoot();
Listener->Keepalive = nullptr;
}
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
const TSoftClassPtr<UObject> SoftClass(AStaticMeshActor::StaticClass());
UDirectiveUtilTask_AsyncLoadClass* Task = UDirectiveUtilTask_AsyncLoadClass::AsyncLoadClass(nullptr, SoftClass);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnClassCompleted);
Task->Failed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnClassFailed);
Task->Activate();
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilWaitForAsyncLoad(this, Listener, 600));
}
return true;
}