Import helper plugin
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "DirectiveUtilitiesTests.h"
|
||||
|
||||
void FDirectiveUtilitiesTestsModule::StartupModule()
|
||||
{
|
||||
}
|
||||
|
||||
void FDirectiveUtilitiesTestsModule::ShutdownModule()
|
||||
{
|
||||
}
|
||||
|
||||
IMPLEMENT_MODULE(FDirectiveUtilitiesTestsModule, DirectiveUtilitiesTests)
|
||||
@@ -0,0 +1,211 @@
|
||||
#include "Libraries/DirectiveUtilArrayFunctionLibrary.h"
|
||||
#include "Tests/DirectiveUtilTestObject.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilArrayFunctionLibraryTest, "DirectiveUtilities.ArrayFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilArrayFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UDirectiveUtilTestObject* TestObject = NewObject<UDirectiveUtilTestObject>();
|
||||
|
||||
TestObject->TestArray = {1, 2, 3, 4, 5};
|
||||
|
||||
FArrayProperty* ArrayProperty = FindFProperty<FArrayProperty>(UDirectiveUtilTestObject::StaticClass(), GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestArray));
|
||||
|
||||
const int32 NextIndex = UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, 2, false);
|
||||
const int32 PreviousIndex = UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, 2, false);
|
||||
|
||||
TestEqual("Array_NextIndex should return the next index in the array", NextIndex, 3);
|
||||
TestEqual("Array_PreviousIndex should return the previous index in the array", PreviousIndex, 1);
|
||||
|
||||
const int32 NextIndexLooped = UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, 4, true);
|
||||
const int32 PreviousIndexLooped = UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, 0, true);
|
||||
|
||||
TestEqual("Array_NextIndex should return the first index when looping", NextIndexLooped, 0);
|
||||
TestEqual("Array_PreviousIndex should return the last index when looping", PreviousIndexLooped, 4);
|
||||
|
||||
const int32 NextIndexNonLooped = UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, 4, false);
|
||||
const int32 PreviousIndexNonLooped = UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, 0, false);
|
||||
|
||||
TestEqual("Array_NextIndex should return the last index when not looping", NextIndexNonLooped, 4);
|
||||
TestEqual("Array_PreviousIndex should return the first index when not looping", PreviousIndexNonLooped, 0);
|
||||
|
||||
const int32 NextIndexOutOfBounds = UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, 5, false);
|
||||
const int32 PreviousIndexOutOfBounds = UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, -1, false);
|
||||
|
||||
TestEqual("Array_NextIndex should return the last index when out of bounds", NextIndexOutOfBounds, 4);
|
||||
TestEqual("Array_PreviousIndex should return the first index when out of bounds", PreviousIndexOutOfBounds, 0);
|
||||
|
||||
TestEqual("Array_NextIndex should clamp a negative input index to 0 (no loop)",
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, -2, false), 0);
|
||||
TestEqual("Array_NextIndex should clamp a negative input index to 0 (loop)",
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, -2, true), 0);
|
||||
TestEqual("Array_NextIndex should wrap a large input index to 0 when looping",
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, 10, true), 0);
|
||||
TestEqual("Array_NextIndex should clamp a large input index to the last index when not looping",
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, 10, false), 4);
|
||||
TestEqual("Array_PreviousIndex should clamp a large input index to the last index (no loop)",
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, 10, false), 4);
|
||||
TestEqual("Array_PreviousIndex should clamp a large input index to the last index (loop)",
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, 10, true), 4);
|
||||
TestEqual("Array_PreviousIndex should wrap a negative input index to the last index when looping",
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, -1, true), 4);
|
||||
|
||||
TestObject->TestArray = {1, 2, 3, 4, 5};
|
||||
|
||||
int32 FirstItem = -1;
|
||||
const bool bGotFirst = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetFirstItem(&TestObject->TestArray, ArrayProperty, &FirstItem);
|
||||
TestTrue("GetFirstItem should succeed on a non-empty array", bGotFirst);
|
||||
TestEqual("GetFirstItem should return the first element", FirstItem, 1);
|
||||
|
||||
int32 LastItem = -1;
|
||||
const bool bGotLast = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetLastItem(&TestObject->TestArray, ArrayProperty, &LastItem);
|
||||
TestTrue("GetLastItem should succeed on a non-empty array", bGotLast);
|
||||
TestEqual("GetLastItem should return the last element", LastItem, 5);
|
||||
|
||||
int32 IndexItem = -1;
|
||||
const bool bGotIndex = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetItemAtIndex(&TestObject->TestArray, ArrayProperty, 2, &IndexItem);
|
||||
TestTrue("GetItemAtIndex should succeed for a valid index", bGotIndex);
|
||||
TestEqual("GetItemAtIndex should return the element at the index", IndexItem, 3);
|
||||
|
||||
int32 OutOfRangeItem = 777;
|
||||
const bool bGotOutOfRange = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetItemAtIndex(&TestObject->TestArray, ArrayProperty, 10, &OutOfRangeItem);
|
||||
TestFalse("GetItemAtIndex should fail for an out-of-range index", bGotOutOfRange);
|
||||
TestEqual("GetItemAtIndex should reset the output to default on failure", OutOfRangeItem, 0);
|
||||
|
||||
int32 RandomItem = -1;
|
||||
int32 RandomIndex = -1;
|
||||
const bool bGotRandom = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetRandomItem(&TestObject->TestArray, ArrayProperty, &RandomItem, &RandomIndex);
|
||||
TestTrue("GetRandomItem should succeed on a non-empty array", bGotRandom);
|
||||
TestTrue("GetRandomItem should return a valid index", TestObject->TestArray.IsValidIndex(RandomIndex));
|
||||
if (TestObject->TestArray.IsValidIndex(RandomIndex))
|
||||
{
|
||||
TestEqual("GetRandomItem item should match the element at the returned index", RandomItem, TestObject->TestArray[RandomIndex]);
|
||||
}
|
||||
|
||||
TestObject->TestArray.Empty();
|
||||
|
||||
int32 EmptyItem = 999;
|
||||
TestFalse("GetFirstItem should fail on an empty array", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetFirstItem(&TestObject->TestArray, ArrayProperty, &EmptyItem));
|
||||
TestEqual("GetFirstItem should reset the output to default on an empty array", EmptyItem, 0);
|
||||
|
||||
EmptyItem = 999;
|
||||
TestFalse("GetLastItem should fail on an empty array", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetLastItem(&TestObject->TestArray, ArrayProperty, &EmptyItem));
|
||||
TestEqual("GetLastItem should reset the output to default on an empty array", EmptyItem, 0);
|
||||
|
||||
int32 EmptyRandomItem = 999;
|
||||
int32 EmptyRandomIndex = 5;
|
||||
TestFalse("GetRandomItem should fail on an empty array", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetRandomItem(&TestObject->TestArray, ArrayProperty, &EmptyRandomItem, &EmptyRandomIndex));
|
||||
TestEqual("GetRandomItem should return INDEX_NONE on an empty array", EmptyRandomIndex, static_cast<int32>(INDEX_NONE));
|
||||
TestEqual("GetRandomItem should reset the output to default on an empty array", EmptyRandomItem, 0);
|
||||
|
||||
TestObject->TestArray = {1, 2, 3};
|
||||
int32 PoppedItem = -1;
|
||||
const bool bPopped = UDirectiveUtilArrayFunctionLibrary::GenericArray_Pop(&TestObject->TestArray, ArrayProperty, &PoppedItem);
|
||||
TestTrue("Pop should succeed on a non-empty array", bPopped);
|
||||
TestEqual("Pop should return the last element", PoppedItem, 3);
|
||||
TestEqual("Pop should shrink the array by one", TestObject->TestArray.Num(), 2);
|
||||
TestEqual("Pop should leave the new last element intact", TestObject->TestArray.Last(), 2);
|
||||
|
||||
TestObject->TestArray = {1, 2, 3};
|
||||
int32 PoppedFirst = -1;
|
||||
const bool bPoppedFirst = UDirectiveUtilArrayFunctionLibrary::GenericArray_PopFirst(&TestObject->TestArray, ArrayProperty, &PoppedFirst);
|
||||
TestTrue("PopFirst should succeed on a non-empty array", bPoppedFirst);
|
||||
TestEqual("PopFirst should return the first element", PoppedFirst, 1);
|
||||
TestEqual("PopFirst should shrink the array by one", TestObject->TestArray.Num(), 2);
|
||||
TestEqual("PopFirst should shift the remaining elements down", TestObject->TestArray[0], 2);
|
||||
|
||||
TestObject->TestArray.Empty();
|
||||
int32 PoppedEmpty = 888;
|
||||
TestFalse("Pop should fail on an empty array", UDirectiveUtilArrayFunctionLibrary::GenericArray_Pop(&TestObject->TestArray, ArrayProperty, &PoppedEmpty));
|
||||
TestEqual("Pop should reset the output to default on an empty array", PoppedEmpty, 0);
|
||||
TestFalse("PopFirst should fail on an empty array", UDirectiveUtilArrayFunctionLibrary::GenericArray_PopFirst(&TestObject->TestArray, ArrayProperty, &PoppedEmpty));
|
||||
|
||||
TestObject->TestArray = {10, 20, 30, 40};
|
||||
const bool bRemoved = UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtSwap(&TestObject->TestArray, ArrayProperty, 1);
|
||||
TestTrue("RemoveAtSwap should succeed for a valid index", bRemoved);
|
||||
TestEqual("RemoveAtSwap should shrink the array by one", TestObject->TestArray.Num(), 3);
|
||||
TestFalse("RemoveAtSwap should have removed the target element", TestObject->TestArray.Contains(20));
|
||||
TestEqual("RemoveAtSwap should move the previously-last element into the removed slot", TestObject->TestArray[1], 40);
|
||||
|
||||
const bool bRemovedOutOfRange = UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtSwap(&TestObject->TestArray, ArrayProperty, 99);
|
||||
TestFalse("RemoveAtSwap should fail for an out-of-range index", bRemovedOutOfRange);
|
||||
TestEqual("RemoveAtSwap should not change the array on failure", TestObject->TestArray.Num(), 3);
|
||||
|
||||
TestObject->TestArray = {10, 20, 30};
|
||||
const bool bRemovedLast = UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtSwap(&TestObject->TestArray, ArrayProperty, 2);
|
||||
TestTrue("RemoveAtSwap should succeed when removing the last element", bRemovedLast);
|
||||
TestEqual("RemoveAtSwap on the last element should shrink the array", TestObject->TestArray.Num(), 2);
|
||||
TestEqual("RemoveAtSwap on the last element should preserve the order of the rest", TestObject->TestArray[1], 20);
|
||||
|
||||
TestObject->TestArray = {1, 2, 2, 3, 1, 4};
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestArray, ArrayProperty);
|
||||
TestEqual("RemoveDuplicates should remove all duplicate entries", TestObject->TestArray.Num(), 4);
|
||||
if (TestObject->TestArray.Num() == 4)
|
||||
{
|
||||
TestEqual("RemoveDuplicates should keep the first occurrence (index 0)", TestObject->TestArray[0], 1);
|
||||
TestEqual("RemoveDuplicates should keep the first occurrence (index 1)", TestObject->TestArray[1], 2);
|
||||
TestEqual("RemoveDuplicates should keep the first occurrence (index 2)", TestObject->TestArray[2], 3);
|
||||
TestEqual("RemoveDuplicates should keep the first occurrence (index 3)", TestObject->TestArray[3], 4);
|
||||
}
|
||||
|
||||
TestObject->TestArray = {5, 5, 5};
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestArray, ArrayProperty);
|
||||
TestEqual("RemoveDuplicates should collapse an all-duplicates array to one element", TestObject->TestArray.Num(), 1);
|
||||
if (TestObject->TestArray.Num() == 1)
|
||||
{
|
||||
TestEqual("RemoveDuplicates should keep the single remaining value", TestObject->TestArray[0], 5);
|
||||
}
|
||||
|
||||
TestObject->TestArray.Empty();
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestArray, ArrayProperty);
|
||||
TestEqual("RemoveDuplicates on an empty array should leave it empty", TestObject->TestArray.Num(), 0);
|
||||
|
||||
TestObject->TestArray = {10, 20, 30, 40, 50};
|
||||
TArray<int32> SliceOut;
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(&TestObject->TestArray, ArrayProperty, 1, 3, &SliceOut, ArrayProperty);
|
||||
TestEqual("Slice should copy a contiguous range", SliceOut, TArray<int32>({20, 30, 40}));
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(&TestObject->TestArray, ArrayProperty, 3, 99, &SliceOut, ArrayProperty);
|
||||
TestEqual("Slice should clamp Count to the available elements", SliceOut, TArray<int32>({40, 50}));
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(&TestObject->TestArray, ArrayProperty, -5, 2, &SliceOut, ArrayProperty);
|
||||
TestEqual("Slice should clamp a negative start index to 0", SliceOut, TArray<int32>({10, 20}));
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(&TestObject->TestArray, ArrayProperty, 0, 0, &SliceOut, ArrayProperty);
|
||||
TestEqual("Slice with Count 0 should be empty", SliceOut.Num(), 0);
|
||||
|
||||
TestObject->TestArray = {1, 2, 3, 4, 5};
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(&TestObject->TestArray, ArrayProperty, 2);
|
||||
TestEqual("Rotate by +2 should rotate toward the end", TestObject->TestArray, TArray<int32>({4, 5, 1, 2, 3}));
|
||||
TestObject->TestArray = {1, 2, 3, 4, 5};
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(&TestObject->TestArray, ArrayProperty, -1);
|
||||
TestEqual("Rotate by -1 should rotate toward the start", TestObject->TestArray, TArray<int32>({2, 3, 4, 5, 1}));
|
||||
TestObject->TestArray = {1, 2, 3};
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(&TestObject->TestArray, ArrayProperty, 3);
|
||||
TestEqual("Rotate by Length should be a no-op", TestObject->TestArray, TArray<int32>({1, 2, 3}));
|
||||
|
||||
TestObject->TestArray = {1, 2, 2, 3, 1, 4};
|
||||
TArray<int32> DistinctOut;
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct(&TestObject->TestArray, ArrayProperty, &DistinctOut, ArrayProperty);
|
||||
TestEqual("GetDistinct should keep first occurrences in order", DistinctOut, TArray<int32>({1, 2, 3, 4}));
|
||||
TestEqual("GetDistinct should not modify the source array", TestObject->TestArray.Num(), 6);
|
||||
|
||||
TestObject->TestArray = {5, 1, 5, 2, 5, 3};
|
||||
int32 ItemToCount = 5;
|
||||
TestEqual("CountOccurrences should count matches", UDirectiveUtilArrayFunctionLibrary::GenericArray_CountOccurrences(&TestObject->TestArray, ArrayProperty, &ItemToCount), 3);
|
||||
int32 MissingItem = 99;
|
||||
TestEqual("CountOccurrences should return 0 for an absent item", UDirectiveUtilArrayFunctionLibrary::GenericArray_CountOccurrences(&TestObject->TestArray, ArrayProperty, &MissingItem), 0);
|
||||
|
||||
TestObject->TestArray = {7, 7, 8, 7, 9, 8};
|
||||
int32 MostCommonItem = -1;
|
||||
int32 MostCommonCount = -1;
|
||||
const bool bGotMostCommon = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetMostCommon(&TestObject->TestArray, ArrayProperty, &MostCommonItem, &MostCommonCount);
|
||||
TestTrue("GetMostCommon should succeed on a non-empty array", bGotMostCommon);
|
||||
TestEqual("GetMostCommon should return the most frequent element", MostCommonItem, 7);
|
||||
TestEqual("GetMostCommon should return the occurrence count", MostCommonCount, 3);
|
||||
TestObject->TestArray.Empty();
|
||||
int32 EmptyMostItem = 5;
|
||||
int32 EmptyMostCount = 5;
|
||||
TestFalse("GetMostCommon should fail on an empty array", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetMostCommon(&TestObject->TestArray, ArrayProperty, &EmptyMostItem, &EmptyMostCount));
|
||||
TestEqual("GetMostCommon should reset the count on an empty array", EmptyMostCount, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Tasks/DirectiveUtilTask_Delay.h"
|
||||
#include "Tasks/DirectiveUtilTask_AsyncLoadAsset.h"
|
||||
#include "Tests/DirectiveUtilTestObject.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "Engine/StaticMesh.h"
|
||||
#include "Engine/StaticMeshActor.h"
|
||||
#include "TimerManager.h"
|
||||
#include "UObject/SoftObjectPath.h"
|
||||
#include "UObject/SoftObjectPtr.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
namespace DirectiveUtilAsyncTaskTestHelpers
|
||||
{
|
||||
/**
|
||||
* Spawns a transient world, starts a cancellable delay in it, and returns a rooted listener bound
|
||||
* to the delay's Completed delegate. The world is stored on the listener so the latent command can
|
||||
* tick its timer manager across frames and tear it down once the scenario settles.
|
||||
*
|
||||
* A timer set on a never-ticked FTimerManager is queued as pending and only promoted to active on
|
||||
* the first tick; it fires on a later tick. Because a manager can be ticked at most once per frame,
|
||||
* driving the timer to completion requires advancing real frames, hence the latent command below.
|
||||
*/
|
||||
UDirectiveUtilDelegateListener* StartDelayScenario(float Duration, bool bCancel)
|
||||
{
|
||||
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
|
||||
if (!World)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
|
||||
WorldContext.SetCurrentWorld(World);
|
||||
|
||||
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
||||
Listener->AddToRoot();
|
||||
Listener->ScenarioWorld = World;
|
||||
|
||||
UDirectiveUtilTask_Delay* Task = UDirectiveUtilTask_Delay::CancellableDelay(World, Duration);
|
||||
Listener->Keepalive = Task;
|
||||
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnCompleted);
|
||||
Task->Activate();
|
||||
|
||||
if (bCancel)
|
||||
{
|
||||
Task->EndTask();
|
||||
}
|
||||
|
||||
return Listener;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Latent command that ticks a delay scenario's world each frame until the delay completes or the
|
||||
* frame budget runs out, then asserts against the expected outcome and tears the world down.
|
||||
*/
|
||||
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("EndTask cancels the delay so Completed never fires"), Listener->bCompleted);
|
||||
}
|
||||
|
||||
if (World)
|
||||
{
|
||||
GEngine->DestroyWorldContext(World);
|
||||
World->DestroyWorld(false);
|
||||
}
|
||||
Listener->ScenarioWorld = nullptr;
|
||||
Listener->Keepalive = nullptr;
|
||||
Listener->RemoveFromRoot();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* DirectiveUtilTask_Delay: verifies the timer-backed completion fires, that EndTask cancels it before it fires,
|
||||
* and that activating with a null world context is guarded rather than crashing.
|
||||
*/
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilDelayTaskTest, "DirectiveUtilities.AsyncTaskDelayTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilDelayTaskTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// The null-world activation intentionally logs a warning.
|
||||
AddExpectedMessagePlain(TEXT("Cancellable Delay failed to activate. World is null."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
// Completion path: the delay fires once its timer elapses (driven across frames by the latent command).
|
||||
if (UDirectiveUtilDelegateListener* Completed = DirectiveUtilAsyncTaskTestHelpers::StartDelayScenario(0.05f, /*bCancel=*/false))
|
||||
{
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickDelayScenario(this, Completed, 600, /*bExpectComplete=*/true));
|
||||
}
|
||||
else
|
||||
{
|
||||
AddError(TEXT("Failed to create a transient world for the delay completion scenario."));
|
||||
}
|
||||
|
||||
// Cancellation path: EndTask clears the timer so Completed never fires across a short window.
|
||||
if (UDirectiveUtilDelegateListener* Cancelled = DirectiveUtilAsyncTaskTestHelpers::StartDelayScenario(0.05f, /*bCancel=*/true))
|
||||
{
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickDelayScenario(this, Cancelled, 10, /*bExpectComplete=*/false));
|
||||
}
|
||||
else
|
||||
{
|
||||
AddError(TEXT("Failed to create a transient world for the delay cancellation scenario."));
|
||||
}
|
||||
|
||||
// Null world guard: activating with a null context object logs a warning and does not crash.
|
||||
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;
|
||||
}
|
||||
|
||||
/** Latent command that waits for an async-load listener to settle (completed or failed) or times out. */
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* DirectiveUtilTask_AsyncLoadAsset: verifies the null soft-reference path broadcasts Failed synchronously, and
|
||||
* that loading a real engine asset eventually broadcasts Completed with the resolved object.
|
||||
*/
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilAsyncLoadAssetTest, "DirectiveUtilities.AsyncTaskLoadAssetTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilAsyncLoadAssetTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// The null soft-reference path intentionally logs a warning.
|
||||
AddExpectedMessagePlain(TEXT("Async Load Asset failed to activate. The soft object reference is null."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
// Failure path: a null soft reference broadcasts Failed synchronously on activation.
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// Success path: loading a real engine asset broadcasts Completed with the resolved object.
|
||||
{
|
||||
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();
|
||||
|
||||
// The streamable completion delegate fires on a later engine tick; poll until it settles.
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilWaitForAsyncLoad(this, Listener, 600));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Latent command that waits for a batch async-load listener to complete, then asserts the slot contract. */
|
||||
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;
|
||||
}
|
||||
|
||||
/** Latent command that waits a fixed frame window and then asserts a cancelled batch load never completed. */
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* DirectiveUtilTask_AsyncLoadAssets: verifies a batch resolves in input order with null slots for unset references,
|
||||
* that an empty input completes immediately with an empty array, and that Cancel suppresses Completed.
|
||||
*/
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilAsyncLoadAssetsTest, "DirectiveUtilities.AsyncTaskLoadAssetsTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilAsyncLoadAssetsTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Empty input: Completed broadcasts synchronously on activation with an empty array.
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
// Batch path: two valid engine meshes plus an unset reference resolve in input order.
|
||||
{
|
||||
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();
|
||||
|
||||
// The streamable completion delegate fires on a later engine tick; poll until it settles.
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilWaitForBatchAsyncLoad(this, Listener, 600));
|
||||
}
|
||||
|
||||
// Cancel path: cancelling before completion suppresses the Completed broadcast. Uses a mesh no
|
||||
// other test loads so the request is genuinely in flight when Cancel arrives; if the asset is
|
||||
// already in memory the batch completes synchronously and there is nothing left to cancel.
|
||||
{
|
||||
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
||||
Listener->AddToRoot();
|
||||
|
||||
TArray<TSoftObjectPtr<UObject>> Assets;
|
||||
Assets.Add(TSoftObjectPtr<UObject>(FSoftObjectPath(TEXT("/Engine/EngineMeshes/SM_MatPreviewMesh_01.SM_MatPreviewMesh_01"))));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* DirectiveUtilTask_AsyncLoadClass: verifies the null soft-class path broadcasts Failed synchronously, and that
|
||||
* loading a real class broadcasts Completed with the resolved class.
|
||||
*/
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilAsyncLoadClassTest, "DirectiveUtilities.AsyncTaskLoadClassTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilAsyncLoadClassTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// The null soft-class path intentionally logs a warning.
|
||||
AddExpectedMessagePlain(TEXT("Async Load Class failed to activate. The soft class reference is null."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
// Failure path: a null soft class reference broadcasts Failed synchronously on activation.
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// Success path: loading a real class broadcasts Completed with the resolved class.
|
||||
{
|
||||
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();
|
||||
|
||||
// The streamable completion delegate fires on a later engine tick; poll until it settles.
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilWaitForAsyncLoad(this, Listener, 600));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // WITH_EDITOR
|
||||
@@ -0,0 +1,502 @@
|
||||
// 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 "Misc/AutomationTest.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
namespace DirectiveUtilAsyncTraceTestHelpers
|
||||
{
|
||||
/** Creates a transient game world with a physics scene, initialized for play so traces resolve. */
|
||||
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;
|
||||
}
|
||||
|
||||
/** Spawns a blocking cube actor at the origin so traces have something to hit. */
|
||||
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);
|
||||
Component->UpdateCollisionProfile();
|
||||
return Cube;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Latent command that ticks a trace world each frame until every listener has reported completion
|
||||
* (or the frame budget runs out), asserts the expected outcome, and tears the world down.
|
||||
*/
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* DirectiveUtilTask_AsyncTrace: verifies the null-world guard broadcasts an empty result, and that each trace
|
||||
* shape (line, sphere, box, capsule) resolves against a blocking body and reports a hit.
|
||||
*/
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilAsyncTraceTest, "DirectiveUtilities.AsyncTaskTraceTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilAsyncTraceTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// The null-world activation intentionally logs a warning.
|
||||
AddExpectedMessagePlain(TEXT("Async Trace failed to activate. World is null."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
// Null world guard: activating with a null context broadcasts an empty result and does not crash.
|
||||
{
|
||||
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)
|
||||
{
|
||||
AddInfo(TEXT("Engine cube mesh unavailable; skipping async trace hit scenarios."));
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Trace straight down through the cube at the origin so every shape intersects it.
|
||||
const FVector Start(0.0f, 0.0f, 500.0f);
|
||||
const FVector End(0.0f, 0.0f, -500.0f);
|
||||
const ETraceTypeQuery Channel = ETraceTypeQuery::TraceTypeQuery1; // Visibility, which BlockAll blocks.
|
||||
|
||||
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, Channel, false)));
|
||||
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncSphereTraceByChannel(World, Start, End, 25.0f, Channel, false)));
|
||||
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncBoxTraceByChannel(World, Start, End, FVector(25.0f), FRotator::ZeroRotator, Channel, false)));
|
||||
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncCapsuleTraceByChannel(World, Start, End, 25.0f, 50.0f, Channel, false)));
|
||||
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickTraceWorld(this, World, Listeners, 120));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Latent command that ticks a move-to-location world each frame until the listener reports
|
||||
* completion (or the frame budget runs out), asserts a single failed completion, and tears the
|
||||
* world down.
|
||||
*/
|
||||
class FDirectiveUtilTickMoveToLocationWorld : public IAutomationLatentCommand
|
||||
{
|
||||
public:
|
||||
FDirectiveUtilTickMoveToLocationWorld(FAutomationTestBase* InTest, UWorld* InWorld, UDirectiveUtilDelegateListener* InListener, int32 InFrames)
|
||||
: Test(InTest)
|
||||
, World(InWorld)
|
||||
, Listener(InListener)
|
||||
, FramesRemaining(InFrames)
|
||||
{
|
||||
}
|
||||
|
||||
virtual bool Update() override
|
||||
{
|
||||
if (UWorld* TickWorld = World.Get())
|
||||
{
|
||||
TickWorld->Tick(LEVELTICK_All, 0.05f);
|
||||
}
|
||||
|
||||
if (Listener && !Listener->bCompleted && --FramesRemaining > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Listener)
|
||||
{
|
||||
Test->TestTrue(TEXT("Move without navigation broadcasts Completed"), Listener->bCompleted);
|
||||
Test->TestFalse(TEXT("Move without navigation reports failure"), Listener->bLastSuccess);
|
||||
Test->TestEqual(TEXT("Move without navigation completes exactly once"), Listener->CompletedCount, 1);
|
||||
Listener->Keepalive = nullptr;
|
||||
Listener->RemoveFromRoot();
|
||||
}
|
||||
|
||||
if (UWorld* TearDownWorld = World.Get())
|
||||
{
|
||||
GEngine->DestroyWorldContext(TearDownWorld);
|
||||
TearDownWorld->DestroyWorld(false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
FAutomationTestBase* Test;
|
||||
TWeakObjectPtr<UWorld> World;
|
||||
UDirectiveUtilDelegateListener* Listener;
|
||||
int32 FramesRemaining;
|
||||
};
|
||||
|
||||
/**
|
||||
* DirectiveUtilTask_MoveToLocation: verifies the guard paths (null controller, controller without a pawn) and
|
||||
* EndTask all broadcast Completed(false) without crashing, that a second EndTask does not broadcast
|
||||
* again, and that a move with no navigation data terminates with failure. The successful navigation
|
||||
* path requires a built navigation mesh and is exercised in a project-level test rather than here.
|
||||
*/
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMoveToLocationTest, "DirectiveUtilities.AsyncTaskMoveToLocationTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// The guard paths intentionally log a warning.
|
||||
AddExpectedMessagePlain(TEXT("Controller or pawn has been destroyed while moving to location. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
// Null controller guard: activating broadcasts Completed(false).
|
||||
{
|
||||
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::Editor, false);
|
||||
if (!World)
|
||||
{
|
||||
AddError(TEXT("Failed to create a transient world for the move-to-location test."));
|
||||
return false;
|
||||
}
|
||||
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
|
||||
WorldContext.SetCurrentWorld(World);
|
||||
|
||||
// Controller-without-pawn guard: activating broadcasts Completed(false).
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
// EndTask broadcasts Completed(false) and clears timers without crashing.
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
// Double completion guard: a second EndTask does not broadcast Completed again.
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
GEngine->DestroyWorldContext(World);
|
||||
World->DestroyWorld(false);
|
||||
|
||||
// No-navigation failure: without a navmesh the idle path-following check terminates the task
|
||||
// with failure instead of polling forever.
|
||||
{
|
||||
// SimpleMoveToLocation may warn when the world has no navigation system.
|
||||
AddExpectedMessagePlain(TEXT("SimpleMoveToActor called for NavSys:"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
AddExpectedMessagePlain(TEXT("SimpleMove failed for"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
UWorld* MoveWorld = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
|
||||
if (!MoveWorld)
|
||||
{
|
||||
AddInfo(TEXT("Failed to create a transient game world; skipping the no-navigation move scenario."));
|
||||
return true;
|
||||
}
|
||||
|
||||
APlayerController* Controller = MoveWorld->SpawnActor<APlayerController>();
|
||||
ADefaultPawn* Pawn = MoveWorld->SpawnActor<ADefaultPawn>(FVector::ZeroVector, FRotator::ZeroRotator);
|
||||
if (!Controller || !Pawn)
|
||||
{
|
||||
AddInfo(TEXT("Failed to spawn a controller or pawn; skipping the no-navigation move scenario."));
|
||||
GEngine->DestroyWorldContext(MoveWorld);
|
||||
MoveWorld->DestroyWorld(false);
|
||||
return true;
|
||||
}
|
||||
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();
|
||||
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickMoveToLocationWorld(this, MoveWorld, Listener, 120));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* DirectiveUtilTask_MoveToActor: verifies the guard paths (null controller, null goal) broadcast Completed(false)
|
||||
* without crashing, that a second EndTask does not broadcast again, and that a move with no
|
||||
* navigation data terminates with failure. The successful navigation path requires a built
|
||||
* navigation mesh and is exercised in a project-level test rather than here.
|
||||
*/
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMoveToActorTest, "DirectiveUtilities.AsyncTaskMoveToActorTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// The guard paths intentionally log a warning.
|
||||
AddExpectedMessagePlain(TEXT("Controller, pawn, or goal has been destroyed while moving to actor. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
// Null controller guard: activating broadcasts Completed(false).
|
||||
{
|
||||
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::Editor, false);
|
||||
if (!World)
|
||||
{
|
||||
AddError(TEXT("Failed to create a transient world for the move-to-actor test."));
|
||||
return false;
|
||||
}
|
||||
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
|
||||
WorldContext.SetCurrentWorld(World);
|
||||
|
||||
// Null goal guard: a controller with a pawn but no goal broadcasts Completed(false).
|
||||
{
|
||||
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
|
||||
{
|
||||
AddInfo(TEXT("Failed to spawn a controller or pawn; skipping the null-goal scenario."));
|
||||
}
|
||||
}
|
||||
|
||||
// Double completion guard: a second EndTask does not broadcast Completed again.
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
GEngine->DestroyWorldContext(World);
|
||||
World->DestroyWorld(false);
|
||||
|
||||
// No-navigation failure: without a navmesh the idle path-following check terminates the task
|
||||
// with failure instead of polling forever.
|
||||
{
|
||||
// SimpleMoveToActor may warn when the world has no navigation system.
|
||||
AddExpectedMessagePlain(TEXT("SimpleMoveToActor called for NavSys:"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
AddExpectedMessagePlain(TEXT("SimpleMove failed for"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
UWorld* MoveWorld = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
|
||||
if (!MoveWorld)
|
||||
{
|
||||
AddInfo(TEXT("Failed to create a transient game world; skipping the no-navigation move scenario."));
|
||||
return true;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
AddInfo(TEXT("Failed to spawn a controller, pawn, or goal; skipping the no-navigation move scenario."));
|
||||
GEngine->DestroyWorldContext(MoveWorld);
|
||||
MoveWorld->DestroyWorld(false);
|
||||
return true;
|
||||
}
|
||||
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();
|
||||
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickMoveToLocationWorld(this, MoveWorld, Listener, 120));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // WITH_EDITOR
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "UObject/UObjectIterator.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilBlueprintCategoryTest, "DirectiveUtilities.BlueprintCategoryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilBlueprintCategoryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
const FString ExpectedCategoryRoot = TEXT("Directive Utilities");
|
||||
const TSet<FString> PluginScriptPackages = {
|
||||
TEXT("/Script/DirectiveUtilitiesRuntime"),
|
||||
TEXT("/Script/DirectiveUtilitiesEditor")
|
||||
};
|
||||
|
||||
int32 TestedFunctionCount = 0;
|
||||
|
||||
for (TObjectIterator<UClass> ClassIterator; ClassIterator; ++ClassIterator)
|
||||
{
|
||||
UClass* Class = *ClassIterator;
|
||||
if (!PluginScriptPackages.Contains(Class->GetOutermost()->GetName()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (TFieldIterator<UFunction> FunctionIterator(Class, EFieldIteratorFlags::ExcludeSuper); FunctionIterator; ++FunctionIterator)
|
||||
{
|
||||
const UFunction* Function = *FunctionIterator;
|
||||
if (!Function->HasAnyFunctionFlags(FUNC_BlueprintCallable | FUNC_BlueprintPure))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
++TestedFunctionCount;
|
||||
const FString Category = Function->GetMetaData(TEXT("Category"));
|
||||
FString CategoryRoot = Category;
|
||||
int32 CategoryDelimiterIndex = INDEX_NONE;
|
||||
if (CategoryRoot.FindChar(TEXT('|'), CategoryDelimiterIndex))
|
||||
{
|
||||
CategoryRoot.LeftInline(CategoryDelimiterIndex);
|
||||
}
|
||||
CategoryRoot.TrimStartAndEndInline();
|
||||
TestTrue(
|
||||
FString::Printf(TEXT("%s.%s uses the Directive Utilities category root"), *Class->GetName(), *Function->GetName()),
|
||||
CategoryRoot == ExpectedCategoryRoot
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TestTrue(TEXT("Blueprint-exposed plugin functions were found"), TestedFunctionCount > 0);
|
||||
return !HasAnyErrors();
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
#if WITH_EDITOR
|
||||
|
||||
#include "Subsystems/DirectiveUtilEditorActorSubsystem.h"
|
||||
#include "Types/DirectiveUtilEditorTypes.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "Engine/StaticMesh.h"
|
||||
#include "Engine/StaticMeshActor.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "Components/BoxComponent.h"
|
||||
#include "Components/CapsuleComponent.h"
|
||||
#include "Materials/MaterialInterface.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Engine/Engine.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemTest, "DirectiveUtilities.EditorActorSubsystemTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilEditorActorSubsystemTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// IsActorWithinBoxBounds should not crash and should return false with null Actor
|
||||
TestFalse("IsActorWithinBoxBounds should return false with null Actor",
|
||||
UDirectiveUtilEditorActorSubsystem::IsActorWithinBoxBounds(nullptr, nullptr));
|
||||
|
||||
// IsActorWithinSphereBounds should not crash and should return false with null Actor
|
||||
TestFalse("IsActorWithinSphereBounds should return false with null Actor",
|
||||
UDirectiveUtilEditorActorSubsystem::IsActorWithinSphereBounds(nullptr, nullptr));
|
||||
|
||||
// IsActorWithinCapsuleBounds should not crash and should return false with null Actor
|
||||
TestFalse("IsActorWithinCapsuleBounds should return false with null CapsuleComponent",
|
||||
UDirectiveUtilEditorActorSubsystem::IsActorWithinCapsuleBounds(nullptr, nullptr));
|
||||
|
||||
// FilterEmptyActors should not crash with an empty array
|
||||
TArray<AActor*> EmptyActors;
|
||||
TArray<AActor*> FilteredActors;
|
||||
UDirectiveUtilEditorActorSubsystem::FilterEmptyActors(EmptyActors, FilteredActors, Include);
|
||||
TestEqual("FilterEmptyActors with empty input should produce empty output", FilteredActors.Num(), 0);
|
||||
|
||||
// FilterActorsByMaterialName should not crash with an empty array
|
||||
TArray<AActor*> MaterialFiltered;
|
||||
UDirectiveUtilEditorActorSubsystem::FilterActorsByMaterialName(EmptyActors, MaterialFiltered, TEXT("TestMaterial"), OverrideOnly, Include);
|
||||
TestEqual("FilterActorsByMaterialName with empty input should produce empty output", MaterialFiltered.Num(), 0);
|
||||
|
||||
// FilterActorsByVertCount should not crash with an empty array
|
||||
TArray<AActor*> VertFiltered;
|
||||
UDirectiveUtilEditorActorSubsystem::FilterActorsByVertCount(EmptyActors, VertFiltered, 0, 1000, Include);
|
||||
TestEqual("FilterActorsByVertCount with empty input should produce empty output", VertFiltered.Num(), 0);
|
||||
|
||||
// FilterActorsByBounds should not crash with an empty array
|
||||
TArray<AActor*> BoundsFiltered;
|
||||
UDirectiveUtilEditorActorSubsystem::FilterActorsByBounds(EmptyActors, BoundsFiltered, FVector::ZeroVector, FVector::OneVector, Include);
|
||||
TestEqual("FilterActorsByBounds with empty input should produce empty output", BoundsFiltered.Num(), 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemFilterTest, "DirectiveUtilities.EditorActorSubsystemFilterTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilEditorActorSubsystemFilterTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UStaticMesh* Cube = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
|
||||
UStaticMesh* Sphere = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Sphere.Sphere"));
|
||||
if (!Cube || !Sphere)
|
||||
{
|
||||
AddInfo(TEXT("Engine basic shapes unavailable; skipping include/exclude behaviour test."));
|
||||
return true;
|
||||
}
|
||||
|
||||
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
|
||||
if (!World)
|
||||
{
|
||||
AddError(TEXT("Failed to create a transient world for the filter test."));
|
||||
return false;
|
||||
}
|
||||
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
|
||||
WorldContext.SetCurrentWorld(World);
|
||||
|
||||
auto SpawnWithMeshes = [World](const TArray<UStaticMesh*>& Meshes) -> AActor*
|
||||
{
|
||||
AActor* Actor = World->SpawnActor<AActor>();
|
||||
USceneComponent* Root = NewObject<USceneComponent>(Actor);
|
||||
Actor->SetRootComponent(Root);
|
||||
Root->RegisterComponent();
|
||||
for (UStaticMesh* Mesh : Meshes)
|
||||
{
|
||||
UStaticMeshComponent* MeshComponent = NewObject<UStaticMeshComponent>(Actor);
|
||||
MeshComponent->SetupAttachment(Root);
|
||||
MeshComponent->RegisterComponent();
|
||||
MeshComponent->SetStaticMesh(Mesh);
|
||||
Actor->AddInstanceComponent(MeshComponent);
|
||||
}
|
||||
return Actor;
|
||||
};
|
||||
|
||||
AActor* ActorCubeAndSphere = SpawnWithMeshes({ Cube, Sphere });
|
||||
AActor* ActorCubeOnly = SpawnWithMeshes({ Cube });
|
||||
AActor* ActorNoMesh = SpawnWithMeshes({});
|
||||
const TArray<AActor*> Source = { ActorCubeAndSphere, ActorCubeOnly, ActorNoMesh };
|
||||
|
||||
// Include: actors that contain the cube mesh.
|
||||
TArray<AActor*> Included;
|
||||
UDirectiveUtilEditorActorSubsystem::FilterActorsByStaticMesh(Source, Included, Cube, Include);
|
||||
TestTrue("Include: multi-mesh actor containing the cube is included", Included.Contains(ActorCubeAndSphere));
|
||||
TestTrue("Include: cube-only actor is included", Included.Contains(ActorCubeOnly));
|
||||
TestFalse("Include: actor with no mesh is excluded", Included.Contains(ActorNoMesh));
|
||||
|
||||
// Exclude: actors that do NOT contain the cube. A multi-mesh actor that uses the cube in one slot
|
||||
// must still be excluded even though another slot uses a different mesh (the aggregation fix).
|
||||
TArray<AActor*> Excluded;
|
||||
UDirectiveUtilEditorActorSubsystem::FilterActorsByStaticMesh(Source, Excluded, Cube, Exclude);
|
||||
TestFalse("Exclude: multi-mesh actor containing the cube is not mistakenly included", Excluded.Contains(ActorCubeAndSphere));
|
||||
TestFalse("Exclude: cube-only actor is excluded", Excluded.Contains(ActorCubeOnly));
|
||||
TestTrue("Exclude: actor with no mesh is included", Excluded.Contains(ActorNoMesh));
|
||||
|
||||
GEngine->DestroyWorldContext(World);
|
||||
World->DestroyWorld(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemFilterCoverageTest, "DirectiveUtilities.EditorActorSubsystemFilterCoverageTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilEditorActorSubsystemFilterCoverageTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UStaticMesh* Cube = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
|
||||
UStaticMesh* Sphere = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Sphere.Sphere"));
|
||||
UMaterialInterface* MatA = LoadObject<UMaterialInterface>(nullptr, TEXT("/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial"));
|
||||
UMaterialInterface* MatB = LoadObject<UMaterialInterface>(nullptr, TEXT("/Engine/EngineMaterials/WorldGridMaterial.WorldGridMaterial"));
|
||||
if (!Cube || !Sphere || !MatA || !MatB)
|
||||
{
|
||||
AddInfo(TEXT("Engine basic shapes/materials unavailable; skipping filter coverage test."));
|
||||
return true;
|
||||
}
|
||||
|
||||
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
|
||||
if (!World)
|
||||
{
|
||||
AddError(TEXT("Failed to create a transient world for the filter coverage test."));
|
||||
return false;
|
||||
}
|
||||
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
|
||||
WorldContext.SetCurrentWorld(World);
|
||||
|
||||
auto SpawnMesh = [World](UStaticMesh* Mesh, EComponentMobility::Type Mobility) -> UStaticMeshComponent*
|
||||
{
|
||||
AActor* Actor = World->SpawnActor<AActor>();
|
||||
USceneComponent* Root = NewObject<USceneComponent>(Actor);
|
||||
Root->SetMobility(Mobility);
|
||||
Actor->SetRootComponent(Root);
|
||||
Root->RegisterComponent();
|
||||
UStaticMeshComponent* MeshComponent = NewObject<UStaticMeshComponent>(Actor);
|
||||
MeshComponent->SetMobility(Mobility);
|
||||
MeshComponent->SetupAttachment(Root);
|
||||
if (Mesh) { MeshComponent->SetStaticMesh(Mesh); }
|
||||
MeshComponent->RegisterComponent();
|
||||
Actor->AddInstanceComponent(MeshComponent);
|
||||
return MeshComponent;
|
||||
};
|
||||
|
||||
// Actor A: cube, MatA override, Static mobility, BlockAll collision, tag Alpha, at origin.
|
||||
UStaticMeshComponent* CompA = SpawnMesh(Cube, EComponentMobility::Static);
|
||||
AActor* ActorA = CompA->GetOwner();
|
||||
CompA->SetMaterial(0, MatA);
|
||||
CompA->SetCollisionProfileName(TEXT("BlockAll"));
|
||||
ActorA->Tags.Add(FName("Alpha"));
|
||||
ActorA->SetActorLocation(FVector::ZeroVector);
|
||||
|
||||
// Actor B: sphere, MatB override, Movable mobility, far away, no tag.
|
||||
UStaticMeshComponent* CompB = SpawnMesh(Sphere, EComponentMobility::Movable);
|
||||
AActor* ActorB = CompB->GetOwner();
|
||||
CompB->SetMaterial(0, MatB);
|
||||
ActorB->SetActorLocation(FVector(100000.0f, 0.0f, 0.0f));
|
||||
|
||||
// Actor C: no static mesh component at all.
|
||||
AActor* ActorC = World->SpawnActor<AActor>();
|
||||
USceneComponent* RootC = NewObject<USceneComponent>(ActorC);
|
||||
ActorC->SetRootComponent(RootC);
|
||||
RootC->RegisterComponent();
|
||||
|
||||
const TArray<AActor*> Src = { ActorA, ActorB, ActorC };
|
||||
auto RunFilter = [&Src](TFunctionRef<void(const TArray<AActor*>&, TArray<AActor*>&)> Fn) -> TArray<AActor*>
|
||||
{
|
||||
TArray<AActor*> Out;
|
||||
Fn(Src, Out);
|
||||
return Out;
|
||||
};
|
||||
|
||||
// Class: everything is an AActor.
|
||||
TestEqual("ByClass(AActor) includes all", RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByClass(S, O, AActor::StaticClass(), Include); }).Num(), 3);
|
||||
|
||||
// Tag (Include + Exclude complement).
|
||||
{
|
||||
const TArray<AActor*> Inc = RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByTag(S, O, FName("Alpha"), Include); });
|
||||
const TArray<AActor*> Exc = RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByTag(S, O, FName("Alpha"), Exclude); });
|
||||
TestTrue("ByTag Include => A only", Inc.Contains(ActorA) && !Inc.Contains(ActorB) && !Inc.Contains(ActorC));
|
||||
TestTrue("ByTag Exclude => B and C", !Exc.Contains(ActorA) && Exc.Contains(ActorB) && Exc.Contains(ActorC));
|
||||
}
|
||||
|
||||
// Static mesh by reference + by name.
|
||||
{
|
||||
const TArray<AActor*> Inc = RunFilter([&](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByStaticMesh(S, O, Cube, Include); });
|
||||
TestTrue("ByStaticMesh(Cube) => A only", Inc.Contains(ActorA) && !Inc.Contains(ActorB) && !Inc.Contains(ActorC));
|
||||
const TArray<AActor*> ByName = RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByStaticMeshName(S, O, TEXT("Cube"), Include); });
|
||||
TestTrue("ByStaticMeshName(Cube) => A", ByName.Contains(ActorA) && !ByName.Contains(ActorB));
|
||||
}
|
||||
|
||||
// Material by reference + by name (override slot).
|
||||
{
|
||||
const TArray<AActor*> Inc = RunFilter([&](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByMaterial(S, O, MatA, OverrideOnly, Include); });
|
||||
const TArray<AActor*> Exc = RunFilter([&](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByMaterial(S, O, MatA, OverrideOnly, Exclude); });
|
||||
TestTrue("ByMaterial(MatA) Include => A only", Inc.Contains(ActorA) && !Inc.Contains(ActorB));
|
||||
TestTrue("ByMaterial(MatA) Exclude => B and C, not A", !Exc.Contains(ActorA) && Exc.Contains(ActorB) && Exc.Contains(ActorC));
|
||||
const TArray<AActor*> ByName = RunFilter([&](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByMaterialName(S, O, MatA->GetName(), OverrideOnly, Include); });
|
||||
TestTrue("ByMaterialName(MatA) => A", ByName.Contains(ActorA) && !ByName.Contains(ActorB));
|
||||
}
|
||||
|
||||
// Vert / tri count: query the cube's actual counts, then assert in-range includes and out-of-range excludes.
|
||||
{
|
||||
const int32 Verts = Cube->GetNumVertices(0);
|
||||
TestTrue("ByVertCount [V,V] => A", RunFilter([Verts](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByVertCount(S, O, Verts, Verts, Include); }).Contains(ActorA));
|
||||
TestFalse("ByVertCount out-of-range => not A", RunFilter([Verts](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByVertCount(S, O, Verts + 100000, Verts + 200000, Include); }).Contains(ActorA));
|
||||
const int32 Tris = Cube->GetNumTriangles(0);
|
||||
TestTrue("ByTriCount [T,T] => A", RunFilter([Tris](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByTriCount(S, O, Tris, Tris, Include); }).Contains(ActorA));
|
||||
}
|
||||
|
||||
// Mobility.
|
||||
{
|
||||
TestTrue("ByMobility(Static) => A, not B", RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByMobility(S, O, EComponentMobility::Static, Include); }).Contains(ActorA));
|
||||
TestTrue("ByMobility(Movable) => B, not A", RunFilter([&](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByMobility(S, O, EComponentMobility::Movable, Include); }).Contains(ActorB));
|
||||
}
|
||||
|
||||
// Collision (BlockAll on A implies WorldStatic object type, QueryAndPhysics, blocking responses).
|
||||
{
|
||||
TestTrue("ByCollisionProfile(BlockAll) => A", RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByCollisionProfile(S, O, FName("BlockAll"), Include); }).Contains(ActorA));
|
||||
TestTrue("ByCollisionChannel(WorldStatic) => A", RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByCollisionChannel(S, O, ECC_WorldStatic, Include); }).Contains(ActorA));
|
||||
TestTrue("ByCollisionEnabled(QueryAndPhysics) => A", RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByCollisionEnabled(S, O, ECollisionEnabled::QueryAndPhysics, Include); }).Contains(ActorA));
|
||||
TestTrue("ByCollisionResponse(WorldDynamic, Block) => A", RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByCollisionResponse(S, O, ECC_WorldDynamic, ECR_Block, Include); }).Contains(ActorA));
|
||||
}
|
||||
|
||||
// Nanite: assert the filter partitions correctly (A matched by exactly one of true/false) without
|
||||
// reading the deprecated member directly.
|
||||
{
|
||||
const bool bInFalse = RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByNaniteState(S, O, false, Include); }).Contains(ActorA);
|
||||
const bool bInTrue = RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByNaniteState(S, O, true, Include); }).Contains(ActorA);
|
||||
TestTrue("ByNaniteState partitions A into exactly one of true/false", bInFalse != bInTrue);
|
||||
}
|
||||
|
||||
// LOD count: query the cube's LOD count and assert in-range includes A.
|
||||
{
|
||||
const int32 LODs = Cube->GetNumLODs();
|
||||
TestTrue("ByLODCount [n,n] => A", RunFilter([LODs](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByLODCount(S, O, LODs, LODs, Include); }).Contains(ActorA));
|
||||
}
|
||||
|
||||
// Actor bounds: query A's own size and assert a range around it includes A.
|
||||
{
|
||||
FVector Origin, Extent;
|
||||
ActorA->GetActorBounds(false, Origin, Extent);
|
||||
const FVector Size = Extent * 2.0f;
|
||||
TestTrue("ByBounds around A's size => A", RunFilter([&](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByBounds(S, O, Size - FVector(1.0f), Size + FVector(1.0f), Include); }).Contains(ActorA));
|
||||
}
|
||||
|
||||
// World location: A at origin, B far away.
|
||||
{
|
||||
const TArray<AActor*> Near = RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByWorldLocation(S, O, FVector::ZeroVector, 50.0f, Include); });
|
||||
TestTrue("ByWorldLocation near origin => A, not the far B", Near.Contains(ActorA) && !Near.Contains(ActorB));
|
||||
}
|
||||
|
||||
// Texture by name: a name no material uses -> Include matches none, Exclude matches all (exercises traversal).
|
||||
{
|
||||
TestEqual("ByTextureName(absent) Include => none", RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByTextureName(S, O, TEXT("__udcore_absent_texture__"), BaseAndOverride, Include); }).Num(), 0);
|
||||
TestEqual("ByTextureName(absent) Exclude => all", RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByTextureName(S, O, TEXT("__udcore_absent_texture__"), BaseAndOverride, Exclude); }).Num(), 3);
|
||||
}
|
||||
|
||||
GEngine->DestroyWorldContext(World);
|
||||
World->DestroyWorld(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemEmptyActorsTest, "DirectiveUtilities.EditorActorSubsystemEmptyActorsTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilEditorActorSubsystemEmptyActorsTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
|
||||
if (!World)
|
||||
{
|
||||
AddError(TEXT("Failed to create a transient world for the empty actors test."));
|
||||
return false;
|
||||
}
|
||||
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
|
||||
WorldContext.SetCurrentWorld(World);
|
||||
|
||||
// A bare actor whose only component is a childless scene root: the "Empty Actor" shape.
|
||||
AActor* BareActor = World->SpawnActor<AActor>();
|
||||
USceneComponent* BareRoot = NewObject<USceneComponent>(BareActor);
|
||||
BareActor->SetRootComponent(BareRoot);
|
||||
BareRoot->RegisterComponent();
|
||||
|
||||
AStaticMeshActor* MeshActor = World->SpawnActor<AStaticMeshActor>();
|
||||
|
||||
const TArray<AActor*> Source = { BareActor, MeshActor };
|
||||
|
||||
TArray<AActor*> Included;
|
||||
UDirectiveUtilEditorActorSubsystem::FilterEmptyActors(Source, Included, Include);
|
||||
TestTrue("Include: bare actor with only a scene root is empty", Included.Contains(BareActor));
|
||||
TestFalse("Include: static mesh actor is not empty", Included.Contains(MeshActor));
|
||||
TestEqual("Include: only the bare actor is returned", Included.Num(), 1);
|
||||
|
||||
TArray<AActor*> Excluded;
|
||||
UDirectiveUtilEditorActorSubsystem::FilterEmptyActors(Source, Excluded, Exclude);
|
||||
TestFalse("Exclude: bare actor is not returned", Excluded.Contains(BareActor));
|
||||
TestTrue("Exclude: static mesh actor is returned", Excluded.Contains(MeshActor));
|
||||
TestEqual("Exclude: only the static mesh actor is returned", Excluded.Num(), 1);
|
||||
|
||||
// Aliasing: filtering an array into itself rebuilds it in place.
|
||||
TArray<AActor*> Aliased = { BareActor, MeshActor };
|
||||
UDirectiveUtilEditorActorSubsystem::FilterEmptyActors(Aliased, Aliased, Include);
|
||||
TestEqual("Aliased: array is rebuilt in place with one entry", Aliased.Num(), 1);
|
||||
TestTrue("Aliased: only the bare actor remains", Aliased.Contains(BareActor));
|
||||
|
||||
GEngine->DestroyWorldContext(World);
|
||||
World->DestroyWorld(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemQueryAlignmentTest, "DirectiveUtilities.EditorActorSubsystemQueryAlignmentTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilEditorActorSubsystemQueryAlignmentTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UStaticMesh* Cube = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
|
||||
if (!Cube)
|
||||
{
|
||||
AddInfo(TEXT("Engine basic shapes unavailable; skipping query alignment test."));
|
||||
return true;
|
||||
}
|
||||
|
||||
// The GetActorsBy* queries read from the editor world rather than a passed array.
|
||||
UWorld* EditorWorld = nullptr;
|
||||
for (const FWorldContext& Context : GEngine->GetWorldContexts())
|
||||
{
|
||||
if (Context.WorldType == EWorldType::Editor && Context.World())
|
||||
{
|
||||
EditorWorld = Context.World();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!EditorWorld)
|
||||
{
|
||||
AddInfo(TEXT("No editor world available; skipping query alignment test."));
|
||||
return true;
|
||||
}
|
||||
|
||||
AActor* MeshActor = EditorWorld->SpawnActor<AActor>();
|
||||
USceneComponent* Root = NewObject<USceneComponent>(MeshActor);
|
||||
Root->SetMobility(EComponentMobility::Movable);
|
||||
MeshActor->SetRootComponent(Root);
|
||||
Root->RegisterComponent();
|
||||
UStaticMeshComponent* MeshComponent = NewObject<UStaticMeshComponent>(MeshActor);
|
||||
MeshComponent->SetMobility(EComponentMobility::Movable);
|
||||
MeshComponent->SetupAttachment(Root);
|
||||
MeshComponent->SetStaticMesh(Cube);
|
||||
MeshComponent->RegisterComponent();
|
||||
MeshActor->AddInstanceComponent(MeshComponent);
|
||||
MeshActor->SetActorLocation(FVector::ZeroVector);
|
||||
|
||||
// The query methods keep no instance state, so a transient instance is enough headless.
|
||||
UDirectiveUtilEditorActorSubsystem* Subsystem = NewObject<UDirectiveUtilEditorActorSubsystem>();
|
||||
|
||||
// Bounding box: an enclosing box finds the actor, a disjoint one does not.
|
||||
TArray<AActor*> InBox;
|
||||
Subsystem->GetActorsByBoundingBox(InBox, FVector(-100000.0f), FVector(100000.0f), World, Include);
|
||||
TestTrue("BoundingBox: enclosing box finds the actor", InBox.Contains(MeshActor));
|
||||
TArray<AActor*> OutOfBox;
|
||||
Subsystem->GetActorsByBoundingBox(OutOfBox, FVector(900000.0f), FVector(900100.0f), World, Include);
|
||||
TestFalse("BoundingBox: disjoint box does not find the actor", OutOfBox.Contains(MeshActor));
|
||||
|
||||
// Mesh name: a lowercase substring matches case-insensitively, as in the Filter variant.
|
||||
TArray<AActor*> ByName;
|
||||
Subsystem->GetActorsByStaticMeshName(ByName, TEXT("cub"), World, Include);
|
||||
TestTrue("StaticMeshName: lowercase substring finds the actor", ByName.Contains(MeshActor));
|
||||
|
||||
// Mobility: the root component's mobility is what counts.
|
||||
const TArray<AActor*> Source = { MeshActor };
|
||||
TArray<AActor*> MovableActors;
|
||||
UDirectiveUtilEditorActorSubsystem::FilterActorsByMobility(Source, MovableActors, EComponentMobility::Movable, Include);
|
||||
TestTrue("Mobility: movable root is matched", MovableActors.Contains(MeshActor));
|
||||
TArray<AActor*> StaticActors;
|
||||
UDirectiveUtilEditorActorSubsystem::FilterActorsByMobility(Source, StaticActors, EComponentMobility::Static, Include);
|
||||
TestFalse("Mobility: static does not match a movable root", StaticActors.Contains(MeshActor));
|
||||
|
||||
EditorWorld->DestroyActor(MeshActor);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemBoundsTest, "DirectiveUtilities.EditorActorSubsystemBoundsTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilEditorActorSubsystemBoundsTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
|
||||
if (!World)
|
||||
{
|
||||
AddError(TEXT("Failed to create a transient world for the bounds test."));
|
||||
return false;
|
||||
}
|
||||
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
|
||||
WorldContext.SetCurrentWorld(World);
|
||||
|
||||
auto SpawnPoint = [World](const FVector& Location, const FVector& Scale) -> AActor*
|
||||
{
|
||||
AActor* Actor = World->SpawnActor<AActor>();
|
||||
USceneComponent* Root = NewObject<USceneComponent>(Actor);
|
||||
Actor->SetRootComponent(Root);
|
||||
Root->RegisterComponent();
|
||||
Actor->SetActorScale3D(Scale);
|
||||
Actor->SetActorLocation(Location);
|
||||
return Actor;
|
||||
};
|
||||
|
||||
// Axis-aligned box of extent 100 at the origin.
|
||||
AActor* BoxActor = World->SpawnActor<AActor>();
|
||||
USceneComponent* BoxRoot = NewObject<USceneComponent>(BoxActor);
|
||||
BoxActor->SetRootComponent(BoxRoot);
|
||||
BoxRoot->RegisterComponent();
|
||||
UBoxComponent* Box = NewObject<UBoxComponent>(BoxActor);
|
||||
Box->SetupAttachment(BoxRoot);
|
||||
Box->RegisterComponent();
|
||||
Box->SetBoxExtent(FVector(100.0f, 100.0f, 100.0f));
|
||||
|
||||
// Point well inside the box, on an actor scaled to 0.1. The previous code multiplied the box
|
||||
// extent by the queried actor's scale and would wrongly report this as outside.
|
||||
TestTrue("Box: point inside is detected regardless of the queried actor's scale",
|
||||
UDirectiveUtilEditorActorSubsystem::IsActorWithinBoxBounds(SpawnPoint(FVector(50.0f, 50.0f, 50.0f), FVector(0.1f)), Box));
|
||||
TestFalse("Box: point beyond the extent is rejected",
|
||||
UDirectiveUtilEditorActorSubsystem::IsActorWithinBoxBounds(SpawnPoint(FVector(250.0f, 0.0f, 0.0f), FVector::OneVector), Box));
|
||||
|
||||
// Capsule of radius 50 and half-height 100 at the origin.
|
||||
AActor* CapsuleActor = World->SpawnActor<AActor>();
|
||||
USceneComponent* CapsuleRoot = NewObject<USceneComponent>(CapsuleActor);
|
||||
CapsuleActor->SetRootComponent(CapsuleRoot);
|
||||
CapsuleRoot->RegisterComponent();
|
||||
UCapsuleComponent* Capsule = NewObject<UCapsuleComponent>(CapsuleActor);
|
||||
Capsule->SetupAttachment(CapsuleRoot);
|
||||
Capsule->RegisterComponent();
|
||||
Capsule->SetCapsuleSize(50.0f, 100.0f);
|
||||
|
||||
TestTrue("Capsule: point within the upper cap is detected",
|
||||
UDirectiveUtilEditorActorSubsystem::IsActorWithinCapsuleBounds(SpawnPoint(FVector(0.0f, 0.0f, 90.0f), FVector::OneVector), Capsule));
|
||||
TestTrue("Capsule: point within the radius is detected",
|
||||
UDirectiveUtilEditorActorSubsystem::IsActorWithinCapsuleBounds(SpawnPoint(FVector(40.0f, 0.0f, 0.0f), FVector::OneVector), Capsule));
|
||||
TestFalse("Capsule: point beyond the radius is rejected (not a broad sphere)",
|
||||
UDirectiveUtilEditorActorSubsystem::IsActorWithinCapsuleBounds(SpawnPoint(FVector(60.0f, 0.0f, 0.0f), FVector::OneVector), Capsule));
|
||||
TestFalse("Capsule: point beyond the end cap is rejected",
|
||||
UDirectiveUtilEditorActorSubsystem::IsActorWithinCapsuleBounds(SpawnPoint(FVector(0.0f, 0.0f, 200.0f), FVector::OneVector), Capsule));
|
||||
|
||||
GEngine->DestroyWorldContext(World);
|
||||
World->DestroyWorld(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,51 @@
|
||||
#if WITH_EDITOR
|
||||
|
||||
#include "Libraries/DirectiveUtilEditorAssetLibrary.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorAssetLibraryTest, "DirectiveUtilities.EditorAssetLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilEditorAssetLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
EDirectiveUtilSuccessStatus Status = EDirectiveUtilSuccessStatus::Success;
|
||||
|
||||
const FString DefaultName = UDirectiveUtilEditorAssetLibrary::GetDefaultAssetNameForClass(nullptr, Status);
|
||||
TestEqual("GetDefaultAssetNameForClass should fail for a null class", Status, EDirectiveUtilSuccessStatus::Failure);
|
||||
TestTrue("GetDefaultAssetNameForClass should return an empty name for a null class", DefaultName.IsEmpty());
|
||||
|
||||
Status = EDirectiveUtilSuccessStatus::Success;
|
||||
const TArray<FAssetData> NoAssets = UDirectiveUtilEditorAssetLibrary::GetAssetsByClass(nullptr, TEXT("/Game"), false, true, Status);
|
||||
TestEqual("GetAssetsByClass should fail for a null class", Status, EDirectiveUtilSuccessStatus::Failure);
|
||||
TestEqual("GetAssetsByClass should return no assets for a null class", NoAssets.Num(), 0);
|
||||
|
||||
Status = EDirectiveUtilSuccessStatus::Success;
|
||||
const TArray<FString> NoDependencies = UDirectiveUtilEditorAssetLibrary::GetAssetDependencies(FAssetData(), false, Status);
|
||||
TestEqual("GetAssetDependencies should fail for an invalid asset", Status, EDirectiveUtilSuccessStatus::Failure);
|
||||
TestEqual("GetAssetDependencies should return nothing for an invalid asset", NoDependencies.Num(), 0);
|
||||
|
||||
Status = EDirectiveUtilSuccessStatus::Success;
|
||||
const TArray<FString> NoReferencers = UDirectiveUtilEditorAssetLibrary::GetAssetReferencers(FAssetData(), false, Status);
|
||||
TestEqual("GetAssetReferencers should fail for an invalid asset", Status, EDirectiveUtilSuccessStatus::Failure);
|
||||
TestEqual("GetAssetReferencers should return nothing for an invalid asset", NoReferencers.Num(), 0);
|
||||
|
||||
Status = EDirectiveUtilSuccessStatus::Failure;
|
||||
UDirectiveUtilEditorAssetLibrary::GetAssetsByClass(UWorld::StaticClass(), TEXT("/Game"), false, true, Status);
|
||||
TestEqual("GetAssetsByClass should succeed for a valid class query", Status, EDirectiveUtilSuccessStatus::Success);
|
||||
|
||||
const TMap<FDirectiveUtilAssetKey, FDirectiveUtilDuplicateAssetData> DuplicateAssets =
|
||||
UDirectiveUtilEditorAssetLibrary::FindDuplicateAssets({TEXT("/Engine/BasicShapes"), TEXT("/Engine/BasicShapes")}, false);
|
||||
for (const TPair<FDirectiveUtilAssetKey, FDirectiveUtilDuplicateAssetData>& Pair : DuplicateAssets)
|
||||
{
|
||||
TSet<FString> UniquePaths;
|
||||
for (const FString& AssetPath : Pair.Value.DuplicateAssetPaths)
|
||||
{
|
||||
UniquePaths.Add(AssetPath);
|
||||
}
|
||||
TestEqual("FindDuplicateAssets should return each asset path once", UniquePaths.Num(), Pair.Value.DuplicateAssetPaths.Num());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,97 @@
|
||||
#include "Libraries/DirectiveUtilFunctionLibrary.h"
|
||||
#include "Tests/AutomationCommon.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilFunctionLibraryTest, "DirectiveUtilities.FunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Preserve the user/CI clipboard so this test doesn't destroy it, and restore it before returning.
|
||||
const FString OriginalClipboard = UDirectiveUtilFunctionLibrary::GetStringFromClipboard();
|
||||
|
||||
const FText TestText = FText::FromString(TEXT("Hello, Clipboard!"));
|
||||
UDirectiveUtilFunctionLibrary::CopyTextToClipboard(TestText);
|
||||
const FText ClipboardText = UDirectiveUtilFunctionLibrary::GetTextFromClipboard();
|
||||
TestEqual("GetTextFromClipboard should return the copied text", ClipboardText.ToString(), TestText.ToString());
|
||||
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FEngineWaitLatentCommand(1.0f));
|
||||
|
||||
const FString TestString = TEXT("Hello, Clipboard!");
|
||||
UDirectiveUtilFunctionLibrary::CopyStringToClipboard(TestString);
|
||||
const FString ClipboardString = UDirectiveUtilFunctionLibrary::GetStringFromClipboard();
|
||||
TestEqual("GetStringFromClipboard should return the copied string", ClipboardString, TestString);
|
||||
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FEngineWaitLatentCommand(1.0f));
|
||||
|
||||
UDirectiveUtilFunctionLibrary::ClearClipboard();
|
||||
const FString ClearedClipboardString = UDirectiveUtilFunctionLibrary::GetStringFromClipboard();
|
||||
TestEqual("GetStringFromClipboard should return an empty string after clearing the clipboard", ClearedClipboardString, TEXT(""));
|
||||
|
||||
const FString ProjectVersion = UDirectiveUtilFunctionLibrary::GetProjectVersion();
|
||||
TestNotEqual("GetProjectVersion should return a non-empty string", ProjectVersion, FString(""));
|
||||
|
||||
TestTrue("IsRunningInEditor should return true in editor context",
|
||||
UDirectiveUtilFunctionLibrary::IsRunningInEditor());
|
||||
|
||||
// Pure pass-through to the engine's GetDerivedClasses (which appends to the output array).
|
||||
|
||||
// Happy path (recursive): the running module guarantees a stable hierarchy to query.
|
||||
TArray<UClass*> RecursiveDerived;
|
||||
UDirectiveUtilFunctionLibrary::GetChildClasses(UBlueprintFunctionLibrary::StaticClass(), true, RecursiveDerived);
|
||||
TestTrue("GetChildClasses should return a non-empty list for a base class with subclasses",
|
||||
RecursiveDerived.Num() > 0);
|
||||
TestTrue("GetChildClasses should include a known derived class (UDirectiveUtilFunctionLibrary)",
|
||||
RecursiveDerived.Contains(UDirectiveUtilFunctionLibrary::StaticClass()));
|
||||
TestFalse("GetChildClasses should not include the base class itself",
|
||||
RecursiveDerived.Contains(UBlueprintFunctionLibrary::StaticClass()));
|
||||
|
||||
// Non-recursive results must not exceed the recursive results for the same base.
|
||||
TArray<UClass*> NonRecursiveDerived;
|
||||
UDirectiveUtilFunctionLibrary::GetChildClasses(UBlueprintFunctionLibrary::StaticClass(), false, NonRecursiveDerived);
|
||||
TestTrue("GetChildClasses non-recursive count should not exceed recursive count",
|
||||
NonRecursiveDerived.Num() <= RecursiveDerived.Num());
|
||||
|
||||
// Leaf class with no subclasses: an empty input array should remain empty.
|
||||
TArray<UClass*> LeafDerived;
|
||||
UDirectiveUtilFunctionLibrary::GetChildClasses(UDirectiveUtilFunctionLibrary::StaticClass(), true, LeafDerived);
|
||||
TestEqual("GetChildClasses should return an empty list for a class with no subclasses",
|
||||
LeafDerived.Num(), 0);
|
||||
|
||||
// Null base class: should not crash and should add nothing.
|
||||
TArray<UClass*> NullBaseDerived;
|
||||
UDirectiveUtilFunctionLibrary::GetChildClasses(nullptr, true, NullBaseDerived);
|
||||
TestEqual("GetChildClasses should return an empty list for a null base class",
|
||||
NullBaseDerived.Num(), 0);
|
||||
|
||||
{
|
||||
const TCHAR* CommandLine = TEXT("-Fast -Mode=Quality -Name=\"Big Save\"");
|
||||
|
||||
TestTrue("HasCommandLineSwitch should find a present switch",
|
||||
UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(CommandLine, TEXT("Fast")));
|
||||
TestTrue("HasCommandLineSwitch should match case-insensitively",
|
||||
UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(CommandLine, TEXT("fast")));
|
||||
TestFalse("HasCommandLineSwitch should not find an absent switch",
|
||||
UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(CommandLine, TEXT("Slow")));
|
||||
TestFalse("HasCommandLineSwitch should return false for an empty switch",
|
||||
UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(CommandLine, TEXT("")));
|
||||
|
||||
FString Value;
|
||||
TestTrue("GetCommandLineOption should find a present key",
|
||||
UDirectiveUtilFunctionLibrary::GetCommandLineOption(CommandLine, TEXT("Mode"), Value));
|
||||
TestEqual("GetCommandLineOption should return the key's value", Value, FString(TEXT("Quality")));
|
||||
TestTrue("GetCommandLineOption should read a quoted value",
|
||||
UDirectiveUtilFunctionLibrary::GetCommandLineOption(CommandLine, TEXT("Name"), Value));
|
||||
TestEqual("GetCommandLineOption should return the quoted value without quotes", Value, FString(TEXT("Big Save")));
|
||||
TestFalse("GetCommandLineOption should not find an absent key",
|
||||
UDirectiveUtilFunctionLibrary::GetCommandLineOption(CommandLine, TEXT("Missing"), Value));
|
||||
TestFalse("GetCommandLineOption should return false for an empty key",
|
||||
UDirectiveUtilFunctionLibrary::GetCommandLineOption(CommandLine, TEXT(""), Value));
|
||||
|
||||
// Smoke test through the process-command-line path.
|
||||
TestFalse("HasCommandLineSwitch should not find a switch that was never passed",
|
||||
UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(TEXT("DirectiveUtilitiesDefinitelyNotPassed")));
|
||||
}
|
||||
|
||||
UDirectiveUtilFunctionLibrary::CopyStringToClipboard(OriginalClipboard);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
#include "Libraries/DirectiveUtilGameplayTagFunctionLibrary.h"
|
||||
#include "GameplayTagsManager.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilGameplayTagFunctionLibraryTest, "DirectiveUtilities.GameplayTagFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilGameplayTagFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Native tag registration is unavailable to a DeveloperTool module (module-type validation)
|
||||
// and the legacy FName path ensures on 5.6/5.7 after startup. Register through a runtime
|
||||
// ini tag source instead, which supports late addition from any module.
|
||||
const FName TestTagName(TEXT("DirectiveUtilities.Test.Alpha.Beta"));
|
||||
const FName SiblingTagName(TEXT("DirectiveUtilities.Test.Alpha.Gamma"));
|
||||
const FName GrandchildTagName(TEXT("DirectiveUtilities.Test.Alpha.Beta.Delta"));
|
||||
FGameplayTag Tag = FGameplayTag::RequestGameplayTag(TestTagName, false);
|
||||
FGameplayTag SiblingTag = FGameplayTag::RequestGameplayTag(SiblingTagName, false);
|
||||
FGameplayTag GrandchildTag = FGameplayTag::RequestGameplayTag(GrandchildTagName, false);
|
||||
if (!Tag.IsValid() || !SiblingTag.IsValid() || !GrandchildTag.IsValid())
|
||||
{
|
||||
// AddTagIniSearchPath will not rescan an already-added directory, so growing the
|
||||
// tag set requires a fresh directory name.
|
||||
const FString TagIniDirectory = FPaths::ProjectSavedDir() / TEXT("DirectiveUtilitiesTests") / TEXT("Tags");
|
||||
const FString TagIniPath = TagIniDirectory / TEXT("DirectiveUtilitiesTestTags.ini");
|
||||
const FString TagIniContents = TEXT("[/Script/GameplayTags.GameplayTagsList]\n")
|
||||
TEXT("GameplayTagList=(Tag=\"DirectiveUtilities.Test.Alpha.Beta\",DevComment=\"DirectiveUtilities automation test tag\")\n")
|
||||
TEXT("GameplayTagList=(Tag=\"DirectiveUtilities.Test.Alpha.Gamma\",DevComment=\"DirectiveUtilities automation test tag\")\n")
|
||||
TEXT("GameplayTagList=(Tag=\"DirectiveUtilities.Test.Alpha.Beta.Delta\",DevComment=\"DirectiveUtilities automation test tag\")\n");
|
||||
if (!FFileHelper::SaveStringToFile(TagIniContents, *TagIniPath))
|
||||
{
|
||||
AddError(TEXT("Failed to write the test tag ini file."));
|
||||
return false;
|
||||
}
|
||||
UGameplayTagsManager::Get().AddTagIniSearchPath(TagIniDirectory);
|
||||
Tag = FGameplayTag::RequestGameplayTag(TestTagName, false);
|
||||
SiblingTag = FGameplayTag::RequestGameplayTag(SiblingTagName, false);
|
||||
GrandchildTag = FGameplayTag::RequestGameplayTag(GrandchildTagName, false);
|
||||
}
|
||||
TestTrue("The registered test tag should be valid", Tag.IsValid());
|
||||
TestTrue("The registered sibling test tag should be valid", SiblingTag.IsValid());
|
||||
TestTrue("The registered grandchild test tag should be valid", GrandchildTag.IsValid());
|
||||
|
||||
TestEqual("GetTagSegments should return each segment in order",
|
||||
UDirectiveUtilGameplayTagFunctionLibrary::GetTagSegments(Tag),
|
||||
TArray<FString>({TEXT("DirectiveUtilities"), TEXT("Test"), TEXT("Alpha"), TEXT("Beta")}));
|
||||
TestEqual("GetTagDepth should return the segment count", UDirectiveUtilGameplayTagFunctionLibrary::GetTagDepth(Tag), 4);
|
||||
TestEqual("GetTagLeafName should return the leaf segment", UDirectiveUtilGameplayTagFunctionLibrary::GetTagLeafName(Tag), FString(TEXT("Beta")));
|
||||
|
||||
const FGameplayTag DirectParent = UDirectiveUtilGameplayTagFunctionLibrary::GetTagDirectParent(Tag);
|
||||
TestEqual("GetTagDirectParent should return the immediate parent", DirectParent.GetTagName(), FName(TEXT("DirectiveUtilities.Test.Alpha")));
|
||||
|
||||
const FGameplayTagContainer Parents = UDirectiveUtilGameplayTagFunctionLibrary::GetTagParents(Tag);
|
||||
TestTrue("GetTagParents should contain the direct parent", Parents.HasTagExact(DirectParent));
|
||||
TestFalse("GetTagParents should exclude the tag itself", Parents.HasTagExact(Tag));
|
||||
|
||||
const FGameplayTag InvalidTag;
|
||||
TestFalse("GetTagDirectParent of an invalid tag should be invalid", UDirectiveUtilGameplayTagFunctionLibrary::GetTagDirectParent(InvalidTag).IsValid());
|
||||
TestEqual("GetTagDepth of an invalid tag should be 0", UDirectiveUtilGameplayTagFunctionLibrary::GetTagDepth(InvalidTag), 0);
|
||||
TestTrue("GetTagLeafName of an invalid tag should be empty", UDirectiveUtilGameplayTagFunctionLibrary::GetTagLeafName(InvalidTag).IsEmpty());
|
||||
TestEqual("GetTagSegments of an invalid tag should be empty", UDirectiveUtilGameplayTagFunctionLibrary::GetTagSegments(InvalidTag).Num(), 0);
|
||||
|
||||
const FGameplayTag AlphaTag = FGameplayTag::RequestGameplayTag(FName(TEXT("DirectiveUtilities.Test.Alpha")), false);
|
||||
TestTrue("The auto-registered parent tag should be valid", AlphaTag.IsValid());
|
||||
|
||||
const FGameplayTagContainer Children = UDirectiveUtilGameplayTagFunctionLibrary::GetTagChildren(AlphaTag);
|
||||
TestTrue("GetTagChildren should contain the first direct child", Children.HasTagExact(Tag));
|
||||
TestTrue("GetTagChildren should contain the second direct child", Children.HasTagExact(SiblingTag));
|
||||
TestTrue("GetTagChildren should contain a grandchild", Children.HasTagExact(GrandchildTag));
|
||||
|
||||
const FGameplayTagContainer DirectChildren = UDirectiveUtilGameplayTagFunctionLibrary::GetTagDirectChildren(AlphaTag);
|
||||
TestTrue("GetTagDirectChildren should contain the first direct child", DirectChildren.HasTagExact(Tag));
|
||||
TestTrue("GetTagDirectChildren should contain the second direct child", DirectChildren.HasTagExact(SiblingTag));
|
||||
TestFalse("GetTagDirectChildren should exclude grandchildren", DirectChildren.HasTagExact(GrandchildTag));
|
||||
|
||||
TestTrue("GetTagChildren of a leaf tag should be empty", UDirectiveUtilGameplayTagFunctionLibrary::GetTagChildren(GrandchildTag).IsEmpty());
|
||||
|
||||
TestEqual("GetTagCommonAncestor should return the deepest shared ancestor",
|
||||
UDirectiveUtilGameplayTagFunctionLibrary::GetTagCommonAncestor(GrandchildTag, SiblingTag).GetTagName(), FName(TEXT("DirectiveUtilities.Test.Alpha")));
|
||||
TestEqual("GetTagCommonAncestor may return one of the inputs",
|
||||
UDirectiveUtilGameplayTagFunctionLibrary::GetTagCommonAncestor(Tag, GrandchildTag).GetTagName(), Tag.GetTagName());
|
||||
|
||||
TestTrue("GetTagChildren of an invalid tag should be empty", UDirectiveUtilGameplayTagFunctionLibrary::GetTagChildren(InvalidTag).IsEmpty());
|
||||
TestTrue("GetTagDirectChildren of an invalid tag should be empty", UDirectiveUtilGameplayTagFunctionLibrary::GetTagDirectChildren(InvalidTag).IsEmpty());
|
||||
TestFalse("GetTagCommonAncestor of an invalid tag should be invalid", UDirectiveUtilGameplayTagFunctionLibrary::GetTagCommonAncestor(InvalidTag, Tag).IsValid());
|
||||
|
||||
TestEqual("GetTagAtDepth should truncate to the requested depth",
|
||||
UDirectiveUtilGameplayTagFunctionLibrary::GetTagAtDepth(GrandchildTag, 3).GetTagName(), FName(TEXT("DirectiveUtilities.Test.Alpha")));
|
||||
TestEqual("GetTagAtDepth at the tag's own depth should return the tag",
|
||||
UDirectiveUtilGameplayTagFunctionLibrary::GetTagAtDepth(GrandchildTag, 5).GetTagName(), GrandchildTag.GetTagName());
|
||||
TestEqual("GetTagAtDepth beyond the tag's depth should return the tag",
|
||||
UDirectiveUtilGameplayTagFunctionLibrary::GetTagAtDepth(GrandchildTag, 99).GetTagName(), GrandchildTag.GetTagName());
|
||||
TestFalse("GetTagAtDepth of zero should be invalid", UDirectiveUtilGameplayTagFunctionLibrary::GetTagAtDepth(GrandchildTag, 0).IsValid());
|
||||
TestFalse("GetTagAtDepth of an invalid tag should be invalid", UDirectiveUtilGameplayTagFunctionLibrary::GetTagAtDepth(InvalidTag, 2).IsValid());
|
||||
|
||||
const FGameplayTagContainer Siblings = UDirectiveUtilGameplayTagFunctionLibrary::GetTagSiblings(Tag);
|
||||
TestTrue("GetTagSiblings should contain the sibling", Siblings.HasTagExact(SiblingTag));
|
||||
TestFalse("GetTagSiblings should exclude the tag itself", Siblings.HasTagExact(Tag));
|
||||
TestFalse("GetTagSiblings should exclude the tag's children", Siblings.HasTagExact(GrandchildTag));
|
||||
TestTrue("GetTagSiblings of an invalid tag should be empty", UDirectiveUtilGameplayTagFunctionLibrary::GetTagSiblings(InvalidTag).IsEmpty());
|
||||
|
||||
TestTrue("IsLeafTag should be true for a childless tag", UDirectiveUtilGameplayTagFunctionLibrary::IsLeafTag(GrandchildTag));
|
||||
TestFalse("IsLeafTag should be false for a tag with children", UDirectiveUtilGameplayTagFunctionLibrary::IsLeafTag(Tag));
|
||||
TestFalse("IsLeafTag of an invalid tag should be false", UDirectiveUtilGameplayTagFunctionLibrary::IsLeafTag(InvalidTag));
|
||||
|
||||
const TArray<FGameplayTag> FoundTags = UDirectiveUtilGameplayTagFunctionLibrary::FindRegisteredTags(TEXT("test.alpha"));
|
||||
TestTrue("FindRegisteredTags should match case-insensitively", FoundTags.Contains(Tag));
|
||||
TestTrue("FindRegisteredTags should find every matching tag", FoundTags.Contains(SiblingTag) && FoundTags.Contains(GrandchildTag));
|
||||
TestEqual("FindRegisteredTags with no match should be empty", UDirectiveUtilGameplayTagFunctionLibrary::FindRegisteredTags(TEXT("DirectiveUtilitiesNoSuchTagXYZ")).Num(), 0);
|
||||
TestEqual("FindRegisteredTags with an empty substring should be empty", UDirectiveUtilGameplayTagFunctionLibrary::FindRegisteredTags(TEXT("")).Num(), 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilInputFunctionLibrary.h"
|
||||
#include "Types/DirectiveUtilInputTypes.h"
|
||||
#include "Types/DirectiveUtilTypes.h"
|
||||
#include "InputMappingContext.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Engine/LocalPlayer.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
/**
|
||||
* Exercises the Enhanced Input library's active paths against a real EnhancedInput subsystem,
|
||||
* which requires a live local player. A standalone game instance + local player is built for this;
|
||||
* if that cannot be created in the headless harness the test skips its assertions rather than failing.
|
||||
*/
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilInputActivePathTest, "DirectiveUtilities.InputActivePathTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilInputActivePathTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// These warnings only fire on the graceful-skip paths; allow (but never require) them.
|
||||
AddExpectedMessagePlain(TEXT("LocalPlayer not found. Cannot set input mapping contexts."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
AddExpectedMessagePlain(TEXT("EnhancedInput subsystem not found."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
UGameInstance* GameInstance = NewObject<UGameInstance>(GEngine);
|
||||
if (!GameInstance)
|
||||
{
|
||||
AddInfo(TEXT("Could not create a game instance; skipping Enhanced Input active-path assertions."));
|
||||
return true;
|
||||
}
|
||||
GameInstance->AddToRoot();
|
||||
GameInstance->InitializeStandalone();
|
||||
|
||||
UWorld* World = GameInstance->GetWorld();
|
||||
|
||||
// CreateLocalPlayer ensures when there is no game viewport client (headless), so build the local
|
||||
// player directly via AddLocalPlayer, which initializes the local-player subsystems without one.
|
||||
UClass* LocalPlayerClass = GEngine->LocalPlayerClass ? GEngine->LocalPlayerClass.Get() : ULocalPlayer::StaticClass();
|
||||
ULocalPlayer* LocalPlayer = NewObject<ULocalPlayer>(GEngine, LocalPlayerClass);
|
||||
if (World && LocalPlayer)
|
||||
{
|
||||
GameInstance->AddLocalPlayer(LocalPlayer, FPlatformMisc::GetPlatformUserForUserIndex(0));
|
||||
}
|
||||
|
||||
if (!World || !LocalPlayer)
|
||||
{
|
||||
AddInfo(TEXT("Local player unavailable in the headless harness; skipping Enhanced Input active-path assertions."));
|
||||
GameInstance->Shutdown();
|
||||
GameInstance->RemoveFromRoot();
|
||||
return true;
|
||||
}
|
||||
|
||||
APlayerController* PlayerController = World->SpawnActor<APlayerController>();
|
||||
if (!PlayerController)
|
||||
{
|
||||
AddInfo(TEXT("Could not spawn a player controller; skipping Enhanced Input active-path assertions."));
|
||||
GameInstance->Shutdown();
|
||||
GameInstance->RemoveFromRoot();
|
||||
return true;
|
||||
}
|
||||
PlayerController->SetPlayer(LocalPlayer);
|
||||
|
||||
UEnhancedInputLocalPlayerSubsystem* Subsystem = UDirectiveUtilInputFunctionLibrary::GetEnhancedInputSubsystem(PlayerController);
|
||||
if (!Subsystem)
|
||||
{
|
||||
AddInfo(TEXT("Enhanced Input subsystem unavailable for the synthetic local player; skipping active-path assertions."));
|
||||
GameInstance->Shutdown();
|
||||
GameInstance->RemoveFromRoot();
|
||||
return true;
|
||||
}
|
||||
|
||||
// We have a live subsystem: exercise the active add/active/swap/remove/clear paths for real.
|
||||
TestNotNull("GetEnhancedInputSubsystem returns the subsystem for a live local player", Subsystem);
|
||||
|
||||
UInputMappingContext* ContextA = NewObject<UInputMappingContext>(GetTransientPackage());
|
||||
UInputMappingContext* ContextB = NewObject<UInputMappingContext>(GetTransientPackage());
|
||||
ContextA->AddToRoot();
|
||||
ContextB->AddToRoot();
|
||||
const TSoftObjectPtr<UInputMappingContext> SoftA(ContextA);
|
||||
const TSoftObjectPtr<UInputMappingContext> SoftB(ContextB);
|
||||
|
||||
// Add context A.
|
||||
FDirectiveUtilEnhancedInputContextData DataA;
|
||||
DataA.InputContext = SoftA;
|
||||
DataA.Priority = 0;
|
||||
TArray<FDirectiveUtilEnhancedInputContextData> ToAdd;
|
||||
ToAdd.Add(DataA);
|
||||
TestEqual("AddInputMappingContexts succeeds for a live controller",
|
||||
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(PlayerController, ToAdd, false),
|
||||
EDirectiveUtilSuccessStatus::Success);
|
||||
TestTrue("Added context is reported active",
|
||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftA));
|
||||
|
||||
// Swap A -> B.
|
||||
TestEqual("SwapInputMappingContexts succeeds",
|
||||
UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(PlayerController, SoftA, SoftB, 0, false),
|
||||
EDirectiveUtilSuccessStatus::Success);
|
||||
TestFalse("Swapped-out context is inactive",
|
||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftA));
|
||||
TestTrue("Swapped-in context is active",
|
||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftB));
|
||||
|
||||
// Remove B.
|
||||
TArray<TSoftObjectPtr<UInputMappingContext>> ToRemove;
|
||||
ToRemove.Add(SoftB);
|
||||
TestEqual("RemoveInputMappingContexts succeeds",
|
||||
UDirectiveUtilInputFunctionLibrary::RemoveInputMappingContexts(PlayerController, ToRemove),
|
||||
EDirectiveUtilSuccessStatus::Success);
|
||||
TestFalse("Removed context is inactive",
|
||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftB));
|
||||
|
||||
// Clear all (after re-adding A).
|
||||
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(PlayerController, ToAdd, false);
|
||||
TestEqual("ClearAllInputMappingContexts succeeds",
|
||||
UDirectiveUtilInputFunctionLibrary::ClearAllInputMappingContexts(PlayerController),
|
||||
EDirectiveUtilSuccessStatus::Success);
|
||||
TestFalse("Context is inactive after clear-all",
|
||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftA));
|
||||
|
||||
// A clearing add whose every context fails to load must leave the existing mappings untouched.
|
||||
AddExpectedMessagePlain(TEXT("Input Mapping Contexts failed to load and were not added!"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(PlayerController, ToAdd, false);
|
||||
FDirectiveUtilEnhancedInputContextData UnresolvableData;
|
||||
UnresolvableData.Priority = 0;
|
||||
TArray<FDirectiveUtilEnhancedInputContextData> UnresolvableToAdd;
|
||||
UnresolvableToAdd.Add(UnresolvableData);
|
||||
TestEqual("AddInputMappingContexts fails when no context loads",
|
||||
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(PlayerController, UnresolvableToAdd, true),
|
||||
EDirectiveUtilSuccessStatus::Failure);
|
||||
TestTrue("Previously active context survives a failed clearing add",
|
||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftA));
|
||||
|
||||
ContextA->RemoveFromRoot();
|
||||
ContextB->RemoveFromRoot();
|
||||
GameInstance->Shutdown();
|
||||
GameInstance->RemoveFromRoot();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // WITH_EDITOR
|
||||
@@ -0,0 +1,62 @@
|
||||
#include "Libraries/DirectiveUtilInputFunctionLibrary.h"
|
||||
#include "Types/DirectiveUtilTypes.h"
|
||||
#include "InputMappingContext.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilInputFunctionLibraryTest, "DirectiveUtilities.InputFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilInputFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// The null-controller and invalid-context paths intentionally log warnings. Register them as
|
||||
// expected (plain match, negative count = consume if present, never required) so the run is clean.
|
||||
AddExpectedMessagePlain(TEXT("PlayerController is null. Cannot set input mapping contexts."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
AddExpectedMessagePlain(TEXT("Both the previous and new input mapping contexts must be valid."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
// AddInputMappingContexts should return Failure with null controller
|
||||
TArray<FDirectiveUtilEnhancedInputContextData> Contexts;
|
||||
FDirectiveUtilEnhancedInputContextData Context;
|
||||
Contexts.Add(Context);
|
||||
TestEqual("AddInputMappingContexts should fail with null controller",
|
||||
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(nullptr, Contexts, false),
|
||||
EDirectiveUtilSuccessStatus::Failure);
|
||||
|
||||
// RemoveInputMappingContexts should return Failure with null controller
|
||||
TArray<TSoftObjectPtr<UInputMappingContext>> RemoveContexts;
|
||||
RemoveContexts.Add(nullptr);
|
||||
TestEqual("RemoveInputMappingContexts should fail with null controller",
|
||||
UDirectiveUtilInputFunctionLibrary::RemoveInputMappingContexts(nullptr, RemoveContexts),
|
||||
EDirectiveUtilSuccessStatus::Failure);
|
||||
|
||||
// AddInputMappingContexts should return Failure with empty contexts
|
||||
TArray<FDirectiveUtilEnhancedInputContextData> EmptyContexts;
|
||||
TestEqual("AddInputMappingContexts should fail with empty contexts",
|
||||
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(nullptr, EmptyContexts, false),
|
||||
EDirectiveUtilSuccessStatus::Failure);
|
||||
|
||||
// RemoveInputMappingContexts should fail with empty contexts (early-return before touching the controller)
|
||||
TArray<TSoftObjectPtr<UInputMappingContext>> EmptyRemoveContexts;
|
||||
TestEqual("RemoveInputMappingContexts should fail with empty contexts",
|
||||
UDirectiveUtilInputFunctionLibrary::RemoveInputMappingContexts(nullptr, EmptyRemoveContexts),
|
||||
EDirectiveUtilSuccessStatus::Failure);
|
||||
|
||||
// SwapInputMappingContexts loads both contexts before using the controller, so invalid
|
||||
// (unset) soft pointers must return Failure regardless of the (null) controller.
|
||||
const TSoftObjectPtr<UInputMappingContext> NullContext;
|
||||
TestEqual("SwapInputMappingContexts should fail when both contexts are invalid",
|
||||
UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(nullptr, NullContext, NullContext, 0, false),
|
||||
EDirectiveUtilSuccessStatus::Failure);
|
||||
TestEqual("SwapInputMappingContexts should fail with invalid contexts even when using previous priority",
|
||||
UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(nullptr, NullContext, NullContext, 5, true),
|
||||
EDirectiveUtilSuccessStatus::Failure);
|
||||
|
||||
// New subsystem-getter helpers: null-controller safety (the active paths require a live player controller).
|
||||
TestNull("GetEnhancedInputSubsystem should return null for a null controller",
|
||||
UDirectiveUtilInputFunctionLibrary::GetEnhancedInputSubsystem(nullptr));
|
||||
TestFalse("IsInputMappingContextActive should return false for a null controller",
|
||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(nullptr, NullContext));
|
||||
TestEqual("ClearAllInputMappingContexts should fail for a null controller",
|
||||
UDirectiveUtilInputFunctionLibrary::ClearAllInputMappingContexts(nullptr),
|
||||
EDirectiveUtilSuccessStatus::Failure);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
#include "Libraries/DirectiveUtilMapFunctionLibrary.h"
|
||||
#include "Tests/DirectiveUtilTestObject.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMapFunctionLibraryTest, "DirectiveUtilities.MapFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilMapFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UDirectiveUtilTestObject* TestObject = NewObject<UDirectiveUtilTestObject>();
|
||||
|
||||
FMapProperty* MapProperty = FindFProperty<FMapProperty>(UDirectiveUtilTestObject::StaticClass(), GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestMap));
|
||||
TestNotNull("TestMap property should be found", MapProperty);
|
||||
if (!MapProperty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TestObject->TestMap = {{1, 100}, {2, 200}};
|
||||
int32 ExistingKey = 2;
|
||||
int32 FoundValue = -1;
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_FindOrAdd(&TestObject->TestMap, MapProperty, &ExistingKey, &FoundValue);
|
||||
TestEqual("FindOrAdd should return the existing value for a present key", FoundValue, 200);
|
||||
TestEqual("FindOrAdd should not change the map size for a present key", TestObject->TestMap.Num(), 2);
|
||||
|
||||
int32 MissingKey = 3;
|
||||
int32 AddedValue = -1;
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_FindOrAdd(&TestObject->TestMap, MapProperty, &MissingKey, &AddedValue);
|
||||
TestEqual("FindOrAdd should return the default value for a missing key", AddedValue, 0);
|
||||
TestEqual("FindOrAdd should grow the map for a missing key", TestObject->TestMap.Num(), 3);
|
||||
TestTrue("FindOrAdd should insert the missing key", TestObject->TestMap.Contains(3));
|
||||
if (const int32* AddedEntry = TestObject->TestMap.Find(3))
|
||||
{
|
||||
TestEqual("FindOrAdd should store a default value for the new key", *AddedEntry, 0);
|
||||
}
|
||||
|
||||
TestObject->TestMap = {{1, 100}, {2, 200}, {3, 300}};
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_ClearValues(&TestObject->TestMap, MapProperty);
|
||||
TestEqual("ClearValues should preserve the number of entries", TestObject->TestMap.Num(), 3);
|
||||
TestTrue("ClearValues should preserve key 1", TestObject->TestMap.Contains(1));
|
||||
TestTrue("ClearValues should preserve key 2", TestObject->TestMap.Contains(2));
|
||||
TestTrue("ClearValues should preserve key 3", TestObject->TestMap.Contains(3));
|
||||
for (const TPair<int32, int32>& Pair : TestObject->TestMap)
|
||||
{
|
||||
TestEqual("ClearValues should reset every value to its default", Pair.Value, 0);
|
||||
}
|
||||
|
||||
TestObject->TestMap.Empty();
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_ClearValues(&TestObject->TestMap, MapProperty);
|
||||
TestEqual("ClearValues on an empty map should leave it empty", TestObject->TestMap.Num(), 0);
|
||||
|
||||
FArrayProperty* ArrayProperty = FindFProperty<FArrayProperty>(UDirectiveUtilTestObject::StaticClass(), GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestArray));
|
||||
FMapProperty* MapProperty2 = FindFProperty<FMapProperty>(UDirectiveUtilTestObject::StaticClass(), GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestMap2));
|
||||
FMapProperty* StringKeyMapProperty = FindFProperty<FMapProperty>(UDirectiveUtilTestObject::StaticClass(), GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestStringKeyMap));
|
||||
TestNotNull("TestArray property should be found", ArrayProperty);
|
||||
TestNotNull("TestMap2 property should be found", MapProperty2);
|
||||
TestNotNull("TestStringKeyMap property should be found", StringKeyMapProperty);
|
||||
if (!ArrayProperty || !MapProperty2 || !StringKeyMapProperty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TestObject->TestMap = {{1, 10}, {2, 20}, {3, 10}};
|
||||
TestObject->TestArray = {42};
|
||||
int32 SearchValue = 10;
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_GetKeysByValue(&TestObject->TestMap, MapProperty, &SearchValue, &TestObject->TestArray, ArrayProperty);
|
||||
TestEqual("GetKeysByValue should find two keys for a shared value", TestObject->TestArray.Num(), 2);
|
||||
if (TestObject->TestArray.Num() == 2)
|
||||
{
|
||||
TestEqual("GetKeysByValue should return the first matching key", TestObject->TestArray[0], 1);
|
||||
TestEqual("GetKeysByValue should return the second matching key", TestObject->TestArray[1], 3);
|
||||
}
|
||||
|
||||
SearchValue = 99;
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_GetKeysByValue(&TestObject->TestMap, MapProperty, &SearchValue, &TestObject->TestArray, ArrayProperty);
|
||||
TestEqual("GetKeysByValue should return no keys for an absent value", TestObject->TestArray.Num(), 0);
|
||||
|
||||
TestObject->TestArray = {42};
|
||||
SearchValue = 10;
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_GetKeysByValue(nullptr, MapProperty, &SearchValue, &TestObject->TestArray, ArrayProperty);
|
||||
TestEqual("GetKeysByValue with a null map should empty the output array", TestObject->TestArray.Num(), 0);
|
||||
TestObject->TestArray = {42};
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_GetKeysByValue(&TestObject->TestMap, nullptr, &SearchValue, &TestObject->TestArray, ArrayProperty);
|
||||
TestEqual("GetKeysByValue with a null map property should empty the output array", TestObject->TestArray.Num(), 0);
|
||||
TestObject->TestStringKeyMap = {{TEXT("One"), 10}};
|
||||
TestObject->TestArray = {42};
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_GetKeysByValue(&TestObject->TestStringKeyMap, StringKeyMapProperty, &SearchValue, &TestObject->TestArray, ArrayProperty);
|
||||
TestEqual("GetKeysByValue with a mismatched key type should empty the output array", TestObject->TestArray.Num(), 0);
|
||||
|
||||
TestObject->TestMap = {{1, 10}, {2, 20}, {3, 10}};
|
||||
int32 QueryValue = 20;
|
||||
TestTrue("HasValue should find a present value", UDirectiveUtilMapFunctionLibrary::GenericMap_HasValue(&TestObject->TestMap, MapProperty, &QueryValue));
|
||||
QueryValue = 99;
|
||||
TestFalse("HasValue should not find an absent value", UDirectiveUtilMapFunctionLibrary::GenericMap_HasValue(&TestObject->TestMap, MapProperty, &QueryValue));
|
||||
TestObject->TestMap.Empty();
|
||||
QueryValue = 20;
|
||||
TestFalse("HasValue on an empty map should be false", UDirectiveUtilMapFunctionLibrary::GenericMap_HasValue(&TestObject->TestMap, MapProperty, &QueryValue));
|
||||
TestFalse("HasValue with a null map should be false", UDirectiveUtilMapFunctionLibrary::GenericMap_HasValue(nullptr, MapProperty, &QueryValue));
|
||||
TestFalse("HasValue with a null map property should be false", UDirectiveUtilMapFunctionLibrary::GenericMap_HasValue(&TestObject->TestMap, nullptr, &QueryValue));
|
||||
|
||||
TestObject->TestMap = {{1, 10}, {2, 20}, {3, 10}};
|
||||
TestObject->TestArray = {1, 3, 99};
|
||||
int32 NumRemoved = UDirectiveUtilMapFunctionLibrary::GenericMap_RemoveKeys(&TestObject->TestMap, MapProperty, &TestObject->TestArray, ArrayProperty);
|
||||
TestEqual("RemoveKeys should report two removed entries", NumRemoved, 2);
|
||||
TestEqual("RemoveKeys should leave one entry behind", TestObject->TestMap.Num(), 1);
|
||||
TestTrue("RemoveKeys should preserve the untouched key", TestObject->TestMap.Contains(2));
|
||||
|
||||
TestEqual("RemoveKeys with a null map should remove nothing", UDirectiveUtilMapFunctionLibrary::GenericMap_RemoveKeys(nullptr, MapProperty, &TestObject->TestArray, ArrayProperty), 0);
|
||||
TestEqual("RemoveKeys with a null map property should remove nothing", UDirectiveUtilMapFunctionLibrary::GenericMap_RemoveKeys(&TestObject->TestMap, nullptr, &TestObject->TestArray, ArrayProperty), 0);
|
||||
TestEqual("RemoveKeys with a mismatched key type should remove nothing", UDirectiveUtilMapFunctionLibrary::GenericMap_RemoveKeys(&TestObject->TestStringKeyMap, StringKeyMapProperty, &TestObject->TestArray, ArrayProperty), 0);
|
||||
TestEqual("RemoveKeys with a mismatched key type should not change the map", TestObject->TestStringKeyMap.Num(), 1);
|
||||
|
||||
TestObject->TestMap = {{1, 10}};
|
||||
TestObject->TestMap2 = {{1, 99}, {4, 40}};
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_Append(&TestObject->TestMap, MapProperty, &TestObject->TestMap2, MapProperty2, true);
|
||||
TestEqual("Append with overwrite should end with two entries", TestObject->TestMap.Num(), 2);
|
||||
if (const int32* OverwrittenValue = TestObject->TestMap.Find(1))
|
||||
{
|
||||
TestEqual("Append with overwrite should replace the existing value", *OverwrittenValue, 99);
|
||||
}
|
||||
if (const int32* AppendedValue = TestObject->TestMap.Find(4))
|
||||
{
|
||||
TestEqual("Append with overwrite should copy the new pair", *AppendedValue, 40);
|
||||
}
|
||||
|
||||
TestObject->TestMap = {{1, 10}};
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_Append(&TestObject->TestMap, MapProperty, &TestObject->TestMap2, MapProperty2, false);
|
||||
TestEqual("Append without overwrite should end with two entries", TestObject->TestMap.Num(), 2);
|
||||
if (const int32* PreservedValue = TestObject->TestMap.Find(1))
|
||||
{
|
||||
TestEqual("Append without overwrite should preserve the existing value", *PreservedValue, 10);
|
||||
}
|
||||
if (const int32* NewValue = TestObject->TestMap.Find(4))
|
||||
{
|
||||
TestEqual("Append without overwrite should still copy the new pair", *NewValue, 40);
|
||||
}
|
||||
|
||||
TestObject->TestMap = {{1, 10}};
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_Append(nullptr, MapProperty, &TestObject->TestMap2, MapProperty2, true);
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_Append(&TestObject->TestMap, nullptr, &TestObject->TestMap2, MapProperty2, true);
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_Append(&TestObject->TestMap, MapProperty, &TestObject->TestStringKeyMap, StringKeyMapProperty, true);
|
||||
TestEqual("Append with null or mismatched properties should not change the target", TestObject->TestMap.Num(), 1);
|
||||
if (const int32* UntouchedValue = TestObject->TestMap.Find(1))
|
||||
{
|
||||
TestEqual("Append with null or mismatched properties should preserve the value", *UntouchedValue, 10);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMathFunctionLibraryTest, "DirectiveUtilities.MathFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
TestEqual("AngleBetweenVectors should return 0 for parallel vectors",
|
||||
UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ForwardVector, FVector::ForwardVector), 0.0f);
|
||||
TestTrue("AngleBetweenVectors should return ~90 for perpendicular vectors",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ForwardVector, FVector::RightVector), 90.0f, 0.01f));
|
||||
TestTrue("AngleBetweenVectors should return ~180 for opposite vectors",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ForwardVector, -FVector::ForwardVector), 180.0f, 0.01f));
|
||||
TestTrue("AngleBetweenVectors should handle zero vector gracefully",
|
||||
FMath::IsFinite(UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ZeroVector, FVector::ForwardVector)));
|
||||
|
||||
{
|
||||
const FVector2D Sample2D(12.34f, 56.78f);
|
||||
const float Noise2DFirst = UDirectiveUtilMathFunctionLibrary::PerlinNoise2D(Sample2D);
|
||||
const float Noise2DSecond = UDirectiveUtilMathFunctionLibrary::PerlinNoise2D(Sample2D);
|
||||
TestEqual("PerlinNoise2D should be deterministic for the same input", Noise2DFirst, Noise2DSecond);
|
||||
TestTrue("PerlinNoise2D should return a finite value", FMath::IsFinite(Noise2DFirst));
|
||||
TestTrue("PerlinNoise2D should be within [-1, 1]",
|
||||
Noise2DFirst >= -1.0f - 1.e-4f && Noise2DFirst <= 1.0f + 1.e-4f);
|
||||
}
|
||||
|
||||
{
|
||||
const FVector2D Samples2D[] = {
|
||||
FVector2D(0.5f, 0.5f),
|
||||
FVector2D(-3.25f, 7.1f),
|
||||
FVector2D(100.123f, -200.456f),
|
||||
FVector2D(0.0f, 0.0f)
|
||||
};
|
||||
for (const FVector2D& Sample : Samples2D)
|
||||
{
|
||||
const float Value = UDirectiveUtilMathFunctionLibrary::PerlinNoise2D(Sample);
|
||||
TestTrue(FString::Printf(TEXT("PerlinNoise2D should be finite at %s"), *Sample.ToString()), FMath::IsFinite(Value));
|
||||
TestTrue(FString::Printf(TEXT("PerlinNoise2D should be within [-1,1] at %s"), *Sample.ToString()),
|
||||
Value >= -1.0f - 1.e-4f && Value <= 1.0f + 1.e-4f);
|
||||
}
|
||||
}
|
||||
|
||||
TestTrue("PerlinNoise2D should be ~0 at an integer lattice point",
|
||||
FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::PerlinNoise2D(FVector2D(3.0f, 4.0f)), 1.e-4f));
|
||||
|
||||
{
|
||||
const FVector Sample3D(12.34f, 56.78f, 90.12f);
|
||||
const float Noise3DFirst = UDirectiveUtilMathFunctionLibrary::PerlinNoise3D(Sample3D);
|
||||
const float Noise3DSecond = UDirectiveUtilMathFunctionLibrary::PerlinNoise3D(Sample3D);
|
||||
TestEqual("PerlinNoise3D should be deterministic for the same input", Noise3DFirst, Noise3DSecond);
|
||||
TestTrue("PerlinNoise3D should return a finite value", FMath::IsFinite(Noise3DFirst));
|
||||
TestTrue("PerlinNoise3D should be within [-1, 1]",
|
||||
Noise3DFirst >= -1.0f - 1.e-4f && Noise3DFirst <= 1.0f + 1.e-4f);
|
||||
}
|
||||
|
||||
{
|
||||
const FVector Samples3D[] = {
|
||||
FVector(0.5f, 0.5f, 0.5f),
|
||||
FVector(-3.25f, 7.1f, -1.9f),
|
||||
FVector(100.123f, -200.456f, 33.7f),
|
||||
FVector(0.0f, 0.0f, 0.0f)
|
||||
};
|
||||
for (const FVector& Sample : Samples3D)
|
||||
{
|
||||
const float Value = UDirectiveUtilMathFunctionLibrary::PerlinNoise3D(Sample);
|
||||
TestTrue(FString::Printf(TEXT("PerlinNoise3D should be finite at %s"), *Sample.ToString()), FMath::IsFinite(Value));
|
||||
TestTrue(FString::Printf(TEXT("PerlinNoise3D should be within [-1,1] at %s"), *Sample.ToString()),
|
||||
Value >= -1.0f - 1.e-4f && Value <= 1.0f + 1.e-4f);
|
||||
}
|
||||
}
|
||||
|
||||
TestTrue("PerlinNoise3D should be ~0 at an integer lattice point",
|
||||
FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::PerlinNoise3D(FVector(3.0f, 4.0f, 5.0f)), 1.e-4f));
|
||||
|
||||
const TArray<EDirectiveUtilEaseType> AllEaseTypes = {
|
||||
EDirectiveUtilEaseType::BackIn, EDirectiveUtilEaseType::BackOut, EDirectiveUtilEaseType::BackInOut,
|
||||
EDirectiveUtilEaseType::ElasticIn, EDirectiveUtilEaseType::ElasticOut, EDirectiveUtilEaseType::ElasticInOut,
|
||||
EDirectiveUtilEaseType::BounceIn, EDirectiveUtilEaseType::BounceOut, EDirectiveUtilEaseType::BounceInOut
|
||||
};
|
||||
for (const EDirectiveUtilEaseType EaseType : AllEaseTypes)
|
||||
{
|
||||
const FString TypeName = FString::FromInt(static_cast<int32>(EaseType));
|
||||
TestTrue(FString::Printf(TEXT("EaseAlpha(0) should be ~0 for type %s"), *TypeName),
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::EaseAlpha(0.0f, EaseType), 0.0f, 1.e-3f));
|
||||
TestTrue(FString::Printf(TEXT("EaseAlpha(1) should be ~1 for type %s"), *TypeName),
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::EaseAlpha(1.0f, EaseType), 1.0f, 1.e-3f));
|
||||
for (const float Sample : {0.0f, 0.25f, 0.5f, 0.75f, 1.0f})
|
||||
{
|
||||
TestTrue(FString::Printf(TEXT("EaseAlpha(%.2f) should be finite for type %s"), Sample, *TypeName),
|
||||
FMath::IsFinite(UDirectiveUtilMathFunctionLibrary::EaseAlpha(Sample, EaseType)));
|
||||
}
|
||||
}
|
||||
|
||||
TestEqual("EaseAlpha should clamp alpha below 0",
|
||||
UDirectiveUtilMathFunctionLibrary::EaseAlpha(-1.0f, EDirectiveUtilEaseType::BounceOut),
|
||||
UDirectiveUtilMathFunctionLibrary::EaseAlpha(0.0f, EDirectiveUtilEaseType::BounceOut));
|
||||
TestEqual("EaseAlpha should clamp alpha above 1",
|
||||
UDirectiveUtilMathFunctionLibrary::EaseAlpha(2.0f, EDirectiveUtilEaseType::BounceOut),
|
||||
UDirectiveUtilMathFunctionLibrary::EaseAlpha(1.0f, EDirectiveUtilEaseType::BounceOut));
|
||||
|
||||
for (const EDirectiveUtilEaseType BounceType : {EDirectiveUtilEaseType::BounceIn, EDirectiveUtilEaseType::BounceOut, EDirectiveUtilEaseType::BounceInOut})
|
||||
{
|
||||
for (const float Sample : {0.1f, 0.3f, 0.6f, 0.9f})
|
||||
{
|
||||
const float Eased = UDirectiveUtilMathFunctionLibrary::EaseAlpha(Sample, BounceType);
|
||||
TestTrue("Bounce easing should stay within [0,1]", Eased >= -1.e-3f && Eased <= 1.0f + 1.e-3f);
|
||||
}
|
||||
}
|
||||
|
||||
TestTrue("EaseFloat at alpha 0 returns A",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::EaseFloat(10.0f, 20.0f, 0.0f, EDirectiveUtilEaseType::BounceOut), 10.0f, 1.e-3f));
|
||||
TestTrue("EaseFloat at alpha 1 returns B",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::EaseFloat(10.0f, 20.0f, 1.0f, EDirectiveUtilEaseType::BounceOut), 20.0f, 1.e-3f));
|
||||
TestTrue("EaseVector at alpha 1 returns B",
|
||||
UDirectiveUtilMathFunctionLibrary::EaseVector(FVector::ZeroVector, FVector(1, 2, 3), 1.0f, EDirectiveUtilEaseType::BounceOut).Equals(FVector(1, 2, 3), 1.e-2f));
|
||||
TestTrue("EaseRotator at alpha 0 returns A",
|
||||
UDirectiveUtilMathFunctionLibrary::EaseRotator(FRotator(10, 20, 30), FRotator(40, 50, 60), 0.0f, EDirectiveUtilEaseType::BounceOut).Equals(FRotator(10, 20, 30), 1.e-1f));
|
||||
TestTrue("EaseColor at alpha 1 returns B",
|
||||
UDirectiveUtilMathFunctionLibrary::EaseColor(FLinearColor::Black, FLinearColor::White, 1.0f, EDirectiveUtilEaseType::BounceOut).Equals(FLinearColor::White, 1.e-2f));
|
||||
|
||||
TestTrue("RoundToDecimals(3.14159, 2) ~= 3.14",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(3.14159f, 2), 3.14f, 1.e-4f));
|
||||
TestTrue("RoundToDecimals(2.71828, 2) ~= 2.72 (rounds up)",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(2.71828f, 2), 2.72f, 1.e-4f));
|
||||
TestTrue("RoundToDecimals(-1.2367, 2) ~= -1.24",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(-1.2367f, 2), -1.24f, 1.e-4f));
|
||||
TestTrue("RoundToDecimals with 0 decimals rounds to integer",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(1.6f, 0), 2.0f, 1.e-4f));
|
||||
TestTrue("RoundToDecimals(10.0, 5) ~= 10.0",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(10.0f, 5), 10.0f, 1.e-4f));
|
||||
TestTrue("RoundToDecimals clamps excessive decimals without crashing",
|
||||
FMath::IsFinite(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(1.23456789f, 50)));
|
||||
TestTrue("RoundToDecimalsAsText(3.14159, 2) reads ~3.14",
|
||||
FMath::IsNearlyEqual(FCString::Atof(*UDirectiveUtilMathFunctionLibrary::RoundToDecimalsAsText(3.14159f, 2).ToString()), 3.14f, 1.e-2f));
|
||||
TestTrue("RoundToDecimals(2.5, 0) rounds half away from zero to 3",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(2.5f, 0), 3.0f, 1.e-4f));
|
||||
TestTrue("RoundToDecimals(-2.5, 0) rounds half away from zero to -3",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(-2.5f, 0), -3.0f, 1.e-4f));
|
||||
TestTrue("RoundToDecimals(0.125, 2) rounds half away from zero to 0.13",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(0.125f, 2), 0.13f, 0.0001f));
|
||||
TestEqual("RoundToDecimalsAsText(2.5, 0) rounds half away from zero to \"3\"",
|
||||
UDirectiveUtilMathFunctionLibrary::RoundToDecimalsAsText(2.5f, 0).ToString(), FString(TEXT("3")));
|
||||
TestEqual("RoundToDecimalsAsText(-2.5, 0) rounds half away from zero to \"-3\"",
|
||||
UDirectiveUtilMathFunctionLibrary::RoundToDecimalsAsText(-2.5f, 0).ToString(), FString(TEXT("-3")));
|
||||
|
||||
TestEqual("Weighted random on an empty array returns INDEX_NONE",
|
||||
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights(TArray<float>()), static_cast<int32>(INDEX_NONE));
|
||||
TestEqual("Weighted random with all-zero weights returns INDEX_NONE",
|
||||
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights({0.0f, 0.0f, 0.0f}), static_cast<int32>(INDEX_NONE));
|
||||
for (int32 Iteration = 0; Iteration < 25; ++Iteration)
|
||||
{
|
||||
TestEqual("Weighted random {0,1,0} always selects index 1",
|
||||
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights({0.0f, 1.0f, 0.0f}), 1);
|
||||
TestEqual("Weighted random {5,0,0} always selects index 0",
|
||||
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights({5.0f, 0.0f, 0.0f}), 0);
|
||||
}
|
||||
|
||||
TestEqual("FormatBytes(532) is \"532 B\"",
|
||||
UDirectiveUtilMathFunctionLibrary::FormatBytes(532).ToString(), FString(TEXT("532 B")));
|
||||
TestEqual("FormatBytes(1536) is \"1.5 KB\"",
|
||||
UDirectiveUtilMathFunctionLibrary::FormatBytes(1536).ToString(), FString(TEXT("1.5 KB")));
|
||||
TestEqual("FormatBytes(1450000, 1) is \"1.4 MB\"",
|
||||
UDirectiveUtilMathFunctionLibrary::FormatBytes(1450000, 1).ToString(), FString(TEXT("1.4 MB")));
|
||||
TestEqual("FormatBytes(0) is \"0 B\"",
|
||||
UDirectiveUtilMathFunctionLibrary::FormatBytes(0).ToString(), FString(TEXT("0 B")));
|
||||
|
||||
TestEqual("FormatDuration(3785) is \"1h 03m 05s\"",
|
||||
UDirectiveUtilMathFunctionLibrary::FormatDuration(3785.0f).ToString(), FString(TEXT("1h 03m 05s")));
|
||||
TestEqual("FormatDuration(3785, false) is \"1h 03m\"",
|
||||
UDirectiveUtilMathFunctionLibrary::FormatDuration(3785.0f, false).ToString(), FString(TEXT("1h 03m")));
|
||||
TestEqual("FormatDuration(45) is \"45s\"",
|
||||
UDirectiveUtilMathFunctionLibrary::FormatDuration(45.0f).ToString(), FString(TEXT("45s")));
|
||||
TestEqual("FormatDuration(-90) is \"-1m 30s\"",
|
||||
UDirectiveUtilMathFunctionLibrary::FormatDuration(-90.0f).ToString(), FString(TEXT("-1m 30s")));
|
||||
|
||||
TestEqual("FormatRelativeTime 5 minutes back reads \"5 minutes ago\"",
|
||||
UDirectiveUtilMathFunctionLibrary::FormatRelativeTime(FDateTime::Now() - FTimespan::FromMinutes(5)).ToString(), FString(TEXT("5 minutes ago")));
|
||||
TestEqual("FormatRelativeTime 10 seconds back reads \"just now\"",
|
||||
UDirectiveUtilMathFunctionLibrary::FormatRelativeTime(FDateTime::Now() - FTimespan::FromSeconds(10)).ToString(), FString(TEXT("just now")));
|
||||
TestEqual("FormatRelativeTime 2 hours ahead reads \"in 2 hours\"",
|
||||
UDirectiveUtilMathFunctionLibrary::FormatRelativeTime(FDateTime::Now() + FTimespan::FromHours(2)).ToString(), FString(TEXT("in 2 hours")));
|
||||
TestEqual("FormatRelativeTime 1 minute back reads \"1 minute ago\"",
|
||||
UDirectiveUtilMathFunctionLibrary::FormatRelativeTime(FDateTime::Now() - FTimespan::FromMinutes(1)).ToString(), FString(TEXT("1 minute ago")));
|
||||
|
||||
{
|
||||
const TArray<int32> IntValues = {1, 2, 3, 4};
|
||||
TestEqual("GetIntArraySum({1,2,3,4}) is 10",
|
||||
UDirectiveUtilMathFunctionLibrary::GetIntArraySum(IntValues), static_cast<int64>(10));
|
||||
TestTrue("GetIntArrayAverage({1,2,3,4}) is 2.5",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetIntArrayAverage(IntValues), 2.5f, 1.e-4f));
|
||||
TestTrue("GetIntArrayMedian({1,2,3,4}) averages the two middle values to 2.5",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian(IntValues), 2.5f, 1.e-4f));
|
||||
TestTrue("GetIntArrayMedian({1,2,3}) is 2",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian({1, 2, 3}), 2.0f, 1.e-4f));
|
||||
TestTrue("GetIntArrayMedian handles an unsorted input",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian({3, 1, 2}), 2.0f, 1.e-4f));
|
||||
TestTrue("GetIntArrayStandardDeviation({2,4,4,4,5,5,7,9}) is the population value 2",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetIntArrayStandardDeviation({2, 4, 4, 4, 5, 5, 7, 9}), 2.0f, 1.e-4f));
|
||||
TestEqual("GetIntArraySum({MAX_int32, MAX_int32}) does not overflow",
|
||||
UDirectiveUtilMathFunctionLibrary::GetIntArraySum({MAX_int32, MAX_int32}), static_cast<int64>(MAX_int32) * 2);
|
||||
|
||||
const TArray<float> FloatValues = {1.0f, 2.0f, 3.0f, 4.0f};
|
||||
TestTrue("GetFloatArraySum({1,2,3,4}) is 10",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetFloatArraySum(FloatValues), 10.0f, 1.e-4f));
|
||||
TestTrue("GetFloatArrayAverage({1,2,3,4}) is 2.5",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetFloatArrayAverage(FloatValues), 2.5f, 1.e-4f));
|
||||
TestTrue("GetFloatArrayMedian({1,2,3,4}) averages the two middle values to 2.5",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian(FloatValues), 2.5f, 1.e-4f));
|
||||
TestTrue("GetFloatArrayMedian({1,2,3}) is 2",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian({1.0f, 2.0f, 3.0f}), 2.0f, 1.e-4f));
|
||||
TestTrue("GetFloatArrayStandardDeviation({2,4,4,4,5,5,7,9}) is the population value 2",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetFloatArrayStandardDeviation({2.0f, 4.0f, 4.0f, 4.0f, 5.0f, 5.0f, 7.0f, 9.0f}), 2.0f, 1.e-4f));
|
||||
|
||||
const TArray<int32> EmptyInts;
|
||||
const TArray<float> EmptyFloats;
|
||||
TestEqual("GetIntArraySum of an empty array is 0",
|
||||
UDirectiveUtilMathFunctionLibrary::GetIntArraySum(EmptyInts), static_cast<int64>(0));
|
||||
TestEqual("GetIntArrayAverage of an empty array is 0",
|
||||
UDirectiveUtilMathFunctionLibrary::GetIntArrayAverage(EmptyInts), 0.0f);
|
||||
TestEqual("GetIntArrayMedian of an empty array is 0",
|
||||
UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian(EmptyInts), 0.0f);
|
||||
TestEqual("GetIntArrayStandardDeviation of an empty array is 0",
|
||||
UDirectiveUtilMathFunctionLibrary::GetIntArrayStandardDeviation(EmptyInts), 0.0f);
|
||||
TestEqual("GetFloatArraySum of an empty array is 0",
|
||||
UDirectiveUtilMathFunctionLibrary::GetFloatArraySum(EmptyFloats), 0.0f);
|
||||
TestEqual("GetFloatArrayAverage of an empty array is 0",
|
||||
UDirectiveUtilMathFunctionLibrary::GetFloatArrayAverage(EmptyFloats), 0.0f);
|
||||
TestEqual("GetFloatArrayMedian of an empty array is 0",
|
||||
UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian(EmptyFloats), 0.0f);
|
||||
TestEqual("GetFloatArrayStandardDeviation of an empty array is 0",
|
||||
UDirectiveUtilMathFunctionLibrary::GetFloatArrayStandardDeviation(EmptyFloats), 0.0f);
|
||||
}
|
||||
|
||||
FRandomStream StreamA(12345);
|
||||
FRandomStream StreamB(12345);
|
||||
const int32 StreamIndexA = UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeightsFromStream(StreamA, {1.0f, 1.0f, 1.0f, 1.0f});
|
||||
const int32 StreamIndexB = UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeightsFromStream(StreamB, {1.0f, 1.0f, 1.0f, 1.0f});
|
||||
TestEqual("Weighted random from stream is deterministic for the same seed", StreamIndexA, StreamIndexB);
|
||||
TestTrue("Weighted random from stream returns a valid index", StreamIndexA >= 0 && StreamIndexA < 4);
|
||||
FRandomStream EmptyStream(1);
|
||||
TestEqual("Weighted random from stream with all-zero weights returns INDEX_NONE",
|
||||
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeightsFromStream(EmptyStream, {0.0f, 0.0f}), static_cast<int32>(INDEX_NONE));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "Libraries/DirectiveUtilRegexFunctionLibrary.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilRegexFunctionLibraryTest, "DirectiveUtilities.RegexFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilRegexFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
TestTrue("RegexMatches should match digits in 'Hello123'", UDirectiveUtilRegexFunctionLibrary::RegexMatches(TEXT("Hello123"), TEXT("[0-9]+")));
|
||||
TestFalse("RegexMatches should not match digits in 'Hello'", UDirectiveUtilRegexFunctionLibrary::RegexMatches(TEXT("Hello"), TEXT("[0-9]+")));
|
||||
TestFalse("RegexMatches should be case-sensitive by default", UDirectiveUtilRegexFunctionLibrary::RegexMatches(TEXT("HELLO"), TEXT("hello")));
|
||||
TestTrue("RegexMatches should honor the case-insensitive flag", UDirectiveUtilRegexFunctionLibrary::RegexMatches(TEXT("HELLO"), TEXT("hello"), false));
|
||||
TestFalse("RegexMatches should return false for an empty pattern", UDirectiveUtilRegexFunctionLibrary::RegexMatches(TEXT("Hello"), TEXT("")));
|
||||
|
||||
{
|
||||
FString Match;
|
||||
int32 Start = 0;
|
||||
int32 End = 0;
|
||||
const bool bFound = UDirectiveUtilRegexFunctionLibrary::RegexFindFirst(TEXT("abc123def456"), TEXT("[0-9]+"), Match, Start, End);
|
||||
TestTrue("RegexFindFirst should find a match", bFound);
|
||||
TestEqual("RegexFindFirst should return the first match", Match, FString(TEXT("123")));
|
||||
TestEqual("RegexFindFirst should return the match start", Start, 3);
|
||||
TestEqual("RegexFindFirst should return the match end (exclusive)", End, 6);
|
||||
}
|
||||
{
|
||||
FString Match = TEXT("sentinel");
|
||||
int32 Start = 5;
|
||||
int32 End = 5;
|
||||
const bool bFound = UDirectiveUtilRegexFunctionLibrary::RegexFindFirst(TEXT("abc"), TEXT("[0-9]+"), Match, Start, End);
|
||||
TestFalse("RegexFindFirst should report no match", bFound);
|
||||
TestTrue("RegexFindFirst should clear the match on failure", Match.IsEmpty());
|
||||
TestEqual("RegexFindFirst should set start to INDEX_NONE on failure", Start, static_cast<int32>(INDEX_NONE));
|
||||
TestEqual("RegexFindFirst should set end to INDEX_NONE on failure", End, static_cast<int32>(INDEX_NONE));
|
||||
}
|
||||
|
||||
TestEqual("RegexFindAll should return every match",
|
||||
UDirectiveUtilRegexFunctionLibrary::RegexFindAll(TEXT("a1b2c3"), TEXT("[0-9]")),
|
||||
TArray<FString>({TEXT("1"), TEXT("2"), TEXT("3")}));
|
||||
TestEqual("RegexFindAll should return nothing when there are no matches",
|
||||
UDirectiveUtilRegexFunctionLibrary::RegexFindAll(TEXT("abc"), TEXT("[0-9]")).Num(), 0);
|
||||
TestEqual("RegexFindAll should ignore zero-width matches",
|
||||
UDirectiveUtilRegexFunctionLibrary::RegexFindAll(TEXT("abc"), TEXT("x*")).Num(), 0);
|
||||
TestEqual("RegexFindAll should return only the non-empty matches of a star pattern",
|
||||
UDirectiveUtilRegexFunctionLibrary::RegexFindAll(TEXT("bab"), TEXT("a*")),
|
||||
TArray<FString>({TEXT("a")}));
|
||||
|
||||
TestEqual("RegexReplaceAll should replace every match",
|
||||
UDirectiveUtilRegexFunctionLibrary::RegexReplaceAll(TEXT("a1b2c3"), TEXT("[0-9]"), TEXT("#")), FString(TEXT("a#b#c#")));
|
||||
TestEqual("RegexReplaceAll should return the input unchanged when nothing matches",
|
||||
UDirectiveUtilRegexFunctionLibrary::RegexReplaceAll(TEXT("abc"), TEXT("[0-9]"), TEXT("#")), FString(TEXT("abc")));
|
||||
TestEqual("RegexReplaceAll should return the input unchanged for an empty pattern",
|
||||
UDirectiveUtilRegexFunctionLibrary::RegexReplaceAll(TEXT("abc"), TEXT(""), TEXT("#")), FString(TEXT("abc")));
|
||||
TestEqual("RegexReplaceAll should only replace the non-empty matches of a star pattern",
|
||||
UDirectiveUtilRegexFunctionLibrary::RegexReplaceAll(TEXT("bab"), TEXT("a*"), TEXT("X")), FString(TEXT("bXb")));
|
||||
TestEqual("RegexReplaceAll should still replace every non-empty match",
|
||||
UDirectiveUtilRegexFunctionLibrary::RegexReplaceAll(TEXT("aaa"), TEXT("a"), TEXT("b")), FString(TEXT("bbb")));
|
||||
|
||||
{
|
||||
FString Group;
|
||||
TestTrue("RegexGetCaptureGroup should return the whole match for group 0",
|
||||
UDirectiveUtilRegexFunctionLibrary::RegexGetCaptureGroup(TEXT("2024-06-24"), TEXT("([0-9]+)-([0-9]+)-([0-9]+)"), 0, Group));
|
||||
TestEqual("RegexGetCaptureGroup group 0 should be the whole match", Group, FString(TEXT("2024-06-24")));
|
||||
|
||||
TestTrue("RegexGetCaptureGroup should return group 1",
|
||||
UDirectiveUtilRegexFunctionLibrary::RegexGetCaptureGroup(TEXT("2024-06-24"), TEXT("([0-9]+)-([0-9]+)-([0-9]+)"), 1, Group));
|
||||
TestEqual("RegexGetCaptureGroup group 1 should be the year", Group, FString(TEXT("2024")));
|
||||
|
||||
TestTrue("RegexGetCaptureGroup should return group 2",
|
||||
UDirectiveUtilRegexFunctionLibrary::RegexGetCaptureGroup(TEXT("2024-06-24"), TEXT("([0-9]+)-([0-9]+)-([0-9]+)"), 2, Group));
|
||||
TestEqual("RegexGetCaptureGroup group 2 should be the month", Group, FString(TEXT("06")));
|
||||
|
||||
Group = TEXT("sentinel");
|
||||
TestFalse("RegexGetCaptureGroup should fail for a non-existent group",
|
||||
UDirectiveUtilRegexFunctionLibrary::RegexGetCaptureGroup(TEXT("2024-06-24"), TEXT("([0-9]+)-([0-9]+)-([0-9]+)"), 9, Group));
|
||||
TestTrue("RegexGetCaptureGroup should clear the output for a non-existent group", Group.IsEmpty());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
#include "Libraries/DirectiveUtilSaveGameFunctionLibrary.h"
|
||||
#include "Tests/DirectiveUtilTestObject.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "GameFramework/SaveGame.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilSaveGameFunctionLibraryTest, "DirectiveUtilities.SaveGameFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilSaveGameFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
{
|
||||
USaveGame* Save = NewObject<UDirectiveUtilTestSaveGame>();
|
||||
TArray<uint8> Bytes;
|
||||
const bool bSaved = UDirectiveUtilSaveGameFunctionLibrary::SaveGameToBytes(Save, Bytes);
|
||||
TestTrue("SaveGameToBytes should succeed for a valid object", bSaved);
|
||||
TestTrue("SaveGameToBytes should produce non-empty data", Bytes.Num() > 0);
|
||||
|
||||
USaveGame* Loaded = UDirectiveUtilSaveGameFunctionLibrary::LoadGameFromBytes(Bytes);
|
||||
TestNotNull("LoadGameFromBytes should return a valid object", Loaded);
|
||||
}
|
||||
|
||||
{
|
||||
TArray<uint8> Bytes;
|
||||
Bytes.Add(1);
|
||||
TestFalse("SaveGameToBytes should fail for a null object", UDirectiveUtilSaveGameFunctionLibrary::SaveGameToBytes(nullptr, Bytes));
|
||||
TestEqual("SaveGameToBytes should clear the output for a null object", Bytes.Num(), 0);
|
||||
TestNull("LoadGameFromBytes should return null for empty data", UDirectiveUtilSaveGameFunctionLibrary::LoadGameFromBytes(TArray<uint8>()));
|
||||
}
|
||||
|
||||
{
|
||||
const FString TestSlot = TEXT("DirectiveUtilitiesAutomationTestSlot");
|
||||
UGameplayStatics::DeleteGameInSlot(TestSlot, 0);
|
||||
|
||||
USaveGame* Save = NewObject<UDirectiveUtilTestSaveGame>();
|
||||
const bool bWritten = UGameplayStatics::SaveGameToSlot(Save, TestSlot, 0);
|
||||
TestTrue("SaveGameToSlot should write the temporary test slot", bWritten);
|
||||
|
||||
if (bWritten)
|
||||
{
|
||||
TestTrue("GetAllSaveSlotNames should include the written slot",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::GetAllSaveSlotNames().Contains(TestSlot));
|
||||
|
||||
FDateTime Timestamp;
|
||||
TestTrue("GetSaveSlotTimestamp should succeed for an existing slot",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::GetSaveSlotTimestamp(TestSlot, Timestamp));
|
||||
TestTrue("GetSaveSlotTimestamp should return local time for a fresh save",
|
||||
FMath::Abs((FDateTime::Now() - Timestamp).GetTotalMinutes()) < 5.0);
|
||||
}
|
||||
|
||||
// Clean up
|
||||
UGameplayStatics::DeleteGameInSlot(TestSlot, 0);
|
||||
TestFalse("A deleted slot should no longer be listed",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::GetAllSaveSlotNames().Contains(TestSlot));
|
||||
|
||||
FDateTime MissingTimestamp;
|
||||
TestFalse("GetSaveSlotTimestamp should fail for a missing slot",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::GetSaveSlotTimestamp(TestSlot, MissingTimestamp));
|
||||
}
|
||||
|
||||
{
|
||||
const FString SlotA = TEXT("DirectiveUtilitiesAutomationTestSlotA");
|
||||
const FString SlotB = TEXT("DirectiveUtilitiesAutomationTestSlotB");
|
||||
const FString SlotC = TEXT("DirectiveUtilitiesAutomationTestSlotC");
|
||||
UGameplayStatics::DeleteGameInSlot(SlotA, 0);
|
||||
UGameplayStatics::DeleteGameInSlot(SlotB, 0);
|
||||
UGameplayStatics::DeleteGameInSlot(SlotC, 0);
|
||||
|
||||
USaveGame* Save = NewObject<UDirectiveUtilTestSaveGame>();
|
||||
const bool bWritten = UGameplayStatics::SaveGameToSlot(Save, SlotA, 0);
|
||||
TestTrue("SaveGameToSlot should write the slot-management test slot", bWritten);
|
||||
|
||||
if (bWritten)
|
||||
{
|
||||
TestTrue("DoesSaveSlotExist should find the written slot",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotA));
|
||||
TestFalse("DoesSaveSlotExist should not find a missing slot",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotB));
|
||||
|
||||
TestTrue("RenameSaveSlot should rename onto a free slot name",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(SlotA, SlotB));
|
||||
TestFalse("RenameSaveSlot should remove the old slot",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotA));
|
||||
TestTrue("RenameSaveSlot should create the new slot",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotB));
|
||||
TestNotNull("A renamed slot should still deserialize",
|
||||
UGameplayStatics::LoadGameFromSlot(SlotB, 0));
|
||||
|
||||
USaveGame* OtherSave = NewObject<UDirectiveUtilTestSaveGame>();
|
||||
TestTrue("SaveGameToSlot should write the collision test slot",
|
||||
UGameplayStatics::SaveGameToSlot(OtherSave, SlotC, 0));
|
||||
TestFalse("RenameSaveSlot should refuse to rename onto an existing slot",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(SlotB, SlotC));
|
||||
TestTrue("A refused rename should keep the source slot",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotB));
|
||||
TestTrue("A refused rename should keep the target slot",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotC));
|
||||
|
||||
TestFalse("DoesSaveSlotExist should reject an invalid slot name",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(TEXT("../escape")));
|
||||
TestFalse("DeleteSaveSlot should reject an invalid slot name",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DeleteSaveSlot(TEXT("../escape")));
|
||||
TestFalse("RenameSaveSlot should reject an invalid source slot name",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(TEXT("../escape"), SlotC));
|
||||
TestFalse("RenameSaveSlot should reject an invalid destination slot name",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(SlotC, TEXT("../escape")));
|
||||
TestTrue("A rejected rename should leave the source slot intact",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotC));
|
||||
|
||||
TestTrue("DeleteSaveSlot should delete an existing slot",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DeleteSaveSlot(SlotB));
|
||||
TestFalse("A deleted slot should no longer exist",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotB));
|
||||
}
|
||||
|
||||
// Clean up
|
||||
UGameplayStatics::DeleteGameInSlot(SlotA, 0);
|
||||
UGameplayStatics::DeleteGameInSlot(SlotB, 0);
|
||||
UGameplayStatics::DeleteGameInSlot(SlotC, 0);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
#include "Libraries/DirectiveUtilStringFunctionLibrary.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilStringFunctionLibraryTest, "DirectiveUtilities.StringFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilStringFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
TestTrue("ContainsLetters should return true for 'Hello'", UDirectiveUtilStringFunctionLibrary::ContainsLetters(TEXT("Hello")));
|
||||
TestFalse("ContainsLetters should return false for '1234'", UDirectiveUtilStringFunctionLibrary::ContainsLetters(TEXT("1234")));
|
||||
|
||||
TestTrue("ContainsNumbers should return true for '1234'", UDirectiveUtilStringFunctionLibrary::ContainsNumbers(TEXT("1234")));
|
||||
TestFalse("ContainsNumbers should return false for 'Hello'", UDirectiveUtilStringFunctionLibrary::ContainsNumbers(TEXT("Hello")));
|
||||
|
||||
TestTrue("ContainsSpaces should return true for 'Hello World'", UDirectiveUtilStringFunctionLibrary::ContainsSpaces(TEXT("Hello World")));
|
||||
TestFalse("ContainsSpaces should return false for 'HelloWorld'", UDirectiveUtilStringFunctionLibrary::ContainsSpaces(TEXT("HelloWorld")));
|
||||
|
||||
TestTrue("ContainsSpecialCharacters should return true for 'Hello!'", UDirectiveUtilStringFunctionLibrary::ContainsSpecialCharacters(TEXT("Hello!")));
|
||||
TestFalse("ContainsSpecialCharacters should return false for 'Hello'", UDirectiveUtilStringFunctionLibrary::ContainsSpecialCharacters(TEXT("Hello")));
|
||||
|
||||
const FString FilteredString = UDirectiveUtilStringFunctionLibrary::FilterCharacters(TEXT("Hello123! "), true, true, true, true);
|
||||
TestEqual("FilterCharacters should return an empty string", FilteredString, TEXT(""));
|
||||
|
||||
const TArray<FString> UnsortedArray = { TEXT("Banana"), TEXT("Apple"), TEXT("Cherry") };
|
||||
const TArray<FString> SortedArray = UDirectiveUtilStringFunctionLibrary::GetSortedStringArray(UnsortedArray);
|
||||
TestEqual("GetSortedStringArray should return a sorted array", SortedArray, TArray<FString>({ TEXT("Apple"), TEXT("Banana"), TEXT("Cherry") }));
|
||||
|
||||
TestEqual("TruncateString should not truncate short strings",
|
||||
UDirectiveUtilStringFunctionLibrary::TruncateString(TEXT("Hello"), 10), FString(TEXT("Hello")));
|
||||
TestEqual("TruncateString should truncate long strings with suffix",
|
||||
UDirectiveUtilStringFunctionLibrary::TruncateString(TEXT("Hello World"), 8), FString(TEXT("Hello...")));
|
||||
TestEqual("TruncateString should not truncate strings at exact max length",
|
||||
UDirectiveUtilStringFunctionLibrary::TruncateString(TEXT("Hello"), 5), FString(TEXT("Hello")));
|
||||
|
||||
TestEqual("ToTitleCase should capitalize first letter of each word",
|
||||
UDirectiveUtilStringFunctionLibrary::ToTitleCase(TEXT("hello world")), FString(TEXT("Hello World")));
|
||||
TestEqual("ToTitleCase should handle all caps",
|
||||
UDirectiveUtilStringFunctionLibrary::ToTitleCase(TEXT("HELLO WORLD")), FString(TEXT("Hello World")));
|
||||
TestEqual("ToTitleCase should handle single word",
|
||||
UDirectiveUtilStringFunctionLibrary::ToTitleCase(TEXT("hello")), FString(TEXT("Hello")));
|
||||
TestEqual("ToTitleCase should handle empty string",
|
||||
UDirectiveUtilStringFunctionLibrary::ToTitleCase(TEXT("")), FString(TEXT("")));
|
||||
|
||||
// When MaxLength < Suffix.Len(), MaxLength - Suffix.Len() is negative; FString::Left clamps
|
||||
// negative counts to 0, so the result is just the suffix (no crash).
|
||||
TestEqual("TruncateString should return only the suffix when MaxLength is less than the suffix length",
|
||||
UDirectiveUtilStringFunctionLibrary::TruncateString(TEXT("Hello World"), 2), FString(TEXT("...")));
|
||||
TestEqual("TruncateString should return only the suffix when MaxLength is zero",
|
||||
UDirectiveUtilStringFunctionLibrary::TruncateString(TEXT("Hello World"), 0), FString(TEXT("...")));
|
||||
TestEqual("TruncateString should return only the suffix when MaxLength equals the suffix length",
|
||||
UDirectiveUtilStringFunctionLibrary::TruncateString(TEXT("Hello World"), 3), FString(TEXT("...")));
|
||||
TestEqual("TruncateString should not truncate an empty input string",
|
||||
UDirectiveUtilStringFunctionLibrary::TruncateString(TEXT(""), 5), FString(TEXT("")));
|
||||
TestEqual("TruncateString should honor a custom suffix",
|
||||
UDirectiveUtilStringFunctionLibrary::TruncateString(TEXT("Hello World"), 6, TEXT(">>")), FString(TEXT("Hell>>")));
|
||||
TestEqual("TruncateString should return only a custom suffix when MaxLength is less than its length",
|
||||
UDirectiveUtilStringFunctionLibrary::TruncateString(TEXT("Hello World"), 1, TEXT(">>")), FString(TEXT(">>")));
|
||||
TestEqual("TruncateString should handle an empty suffix by returning a hard cut",
|
||||
UDirectiveUtilStringFunctionLibrary::TruncateString(TEXT("Hello World"), 5, TEXT("")), FString(TEXT("Hello")));
|
||||
|
||||
TestFalse("ContainsLetters should return false for an empty string", UDirectiveUtilStringFunctionLibrary::ContainsLetters(TEXT("")));
|
||||
TestFalse("ContainsNumbers should return false for an empty string", UDirectiveUtilStringFunctionLibrary::ContainsNumbers(TEXT("")));
|
||||
TestFalse("ContainsSpaces should return false for an empty string", UDirectiveUtilStringFunctionLibrary::ContainsSpaces(TEXT("")));
|
||||
TestFalse("ContainsSpecialCharacters should return false for an empty string", UDirectiveUtilStringFunctionLibrary::ContainsSpecialCharacters(TEXT("")));
|
||||
|
||||
TestTrue("ContainsSpaces should treat a tab as whitespace", UDirectiveUtilStringFunctionLibrary::ContainsSpaces(TEXT("Hello\tWorld")));
|
||||
TestFalse("ContainsSpecialCharacters should return false for letters and digits only", UDirectiveUtilStringFunctionLibrary::ContainsSpecialCharacters(TEXT("Hello123")));
|
||||
|
||||
TestEqual("FilterCharacters should strip only letters",
|
||||
UDirectiveUtilStringFunctionLibrary::FilterCharacters(TEXT("Hello123! "), true, false, false, false), FString(TEXT("123! ")));
|
||||
TestEqual("FilterCharacters should strip only numbers",
|
||||
UDirectiveUtilStringFunctionLibrary::FilterCharacters(TEXT("Hello123! "), false, true, false, false), FString(TEXT("Hello! ")));
|
||||
TestEqual("FilterCharacters should strip only special characters",
|
||||
UDirectiveUtilStringFunctionLibrary::FilterCharacters(TEXT("Hello123! "), false, false, true, false), FString(TEXT("Hello123 ")));
|
||||
TestEqual("FilterCharacters should strip only spaces",
|
||||
UDirectiveUtilStringFunctionLibrary::FilterCharacters(TEXT("Hello123! "), false, false, false, true), FString(TEXT("Hello123!")));
|
||||
TestEqual("FilterCharacters should return the original string when no flags are set",
|
||||
UDirectiveUtilStringFunctionLibrary::FilterCharacters(TEXT("Hello123! "), false, false, false, false), FString(TEXT("Hello123! ")));
|
||||
TestEqual("FilterCharacters should return an empty string for an empty input",
|
||||
UDirectiveUtilStringFunctionLibrary::FilterCharacters(TEXT(""), true, true, true, true), FString(TEXT("")));
|
||||
|
||||
{
|
||||
const FString EquivalentInputs[] = {
|
||||
TEXT("my var name"), TEXT("my_var_name"), TEXT("my-var-name"), TEXT("MyVarName"), TEXT("myVarName")
|
||||
};
|
||||
for (const FString& Input : EquivalentInputs)
|
||||
{
|
||||
TestEqual(FString::Printf(TEXT("ToCamelCase(\"%s\") is \"myVarName\""), *Input),
|
||||
UDirectiveUtilStringFunctionLibrary::ToCamelCase(Input), FString(TEXT("myVarName")));
|
||||
TestEqual(FString::Printf(TEXT("ToPascalCase(\"%s\") is \"MyVarName\""), *Input),
|
||||
UDirectiveUtilStringFunctionLibrary::ToPascalCase(Input), FString(TEXT("MyVarName")));
|
||||
TestEqual(FString::Printf(TEXT("ToSnakeCase(\"%s\") is \"my_var_name\""), *Input),
|
||||
UDirectiveUtilStringFunctionLibrary::ToSnakeCase(Input), FString(TEXT("my_var_name")));
|
||||
TestEqual(FString::Printf(TEXT("ToKebabCase(\"%s\") is \"my-var-name\""), *Input),
|
||||
UDirectiveUtilStringFunctionLibrary::ToKebabCase(Input), FString(TEXT("my-var-name")));
|
||||
}
|
||||
|
||||
TestEqual("SplitIntoWords keeps acronyms together",
|
||||
UDirectiveUtilStringFunctionLibrary::SplitIntoWords(TEXT("XMLParser")),
|
||||
TArray<FString>({ TEXT("XML"), TEXT("Parser") }));
|
||||
TestEqual("SplitIntoWords splits on letter/digit transitions",
|
||||
UDirectiveUtilStringFunctionLibrary::SplitIntoWords(TEXT("version2Beta")),
|
||||
TArray<FString>({ TEXT("version"), TEXT("2"), TEXT("Beta") }));
|
||||
TestEqual("SplitIntoWords of an empty string is an empty array",
|
||||
UDirectiveUtilStringFunctionLibrary::SplitIntoWords(TEXT("")), TArray<FString>());
|
||||
|
||||
TestEqual("ToCamelCase(\"XMLParser\") is \"xmlParser\"",
|
||||
UDirectiveUtilStringFunctionLibrary::ToCamelCase(TEXT("XMLParser")), FString(TEXT("xmlParser")));
|
||||
TestEqual("ToSnakeCase(\"XMLParser\") is \"xml_parser\"",
|
||||
UDirectiveUtilStringFunctionLibrary::ToSnakeCase(TEXT("XMLParser")), FString(TEXT("xml_parser")));
|
||||
TestEqual("ToSnakeCase(\"version2Beta\") is \"version_2_beta\"",
|
||||
UDirectiveUtilStringFunctionLibrary::ToSnakeCase(TEXT("version2Beta")), FString(TEXT("version_2_beta")));
|
||||
|
||||
TestEqual("ToCamelCase of an empty string is empty",
|
||||
UDirectiveUtilStringFunctionLibrary::ToCamelCase(TEXT("")), FString(TEXT("")));
|
||||
TestEqual("ToPascalCase of an empty string is empty",
|
||||
UDirectiveUtilStringFunctionLibrary::ToPascalCase(TEXT("")), FString(TEXT("")));
|
||||
TestEqual("ToSnakeCase of an empty string is empty",
|
||||
UDirectiveUtilStringFunctionLibrary::ToSnakeCase(TEXT("")), FString(TEXT("")));
|
||||
TestEqual("ToKebabCase of an empty string is empty",
|
||||
UDirectiveUtilStringFunctionLibrary::ToKebabCase(TEXT("")), FString(TEXT("")));
|
||||
}
|
||||
|
||||
const TArray<FString> UnsortedForLegacy = { TEXT("Banana"), TEXT("Apple"), TEXT("Cherry") };
|
||||
TestEqual("SortStringArray should return a sorted array",
|
||||
UDirectiveUtilStringFunctionLibrary::SortStringArray(UnsortedForLegacy),
|
||||
TArray<FString>({ TEXT("Apple"), TEXT("Banana"), TEXT("Cherry") }));
|
||||
TestEqual("SortStringArray should return an empty array for empty input",
|
||||
UDirectiveUtilStringFunctionLibrary::SortStringArray(TArray<FString>()), TArray<FString>());
|
||||
|
||||
TestEqual("GetSortedStringArray should return an empty array for empty input",
|
||||
UDirectiveUtilStringFunctionLibrary::GetSortedStringArray(TArray<FString>()), TArray<FString>());
|
||||
|
||||
TestEqual("Levenshtein of identical strings is 0",
|
||||
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(TEXT("abc"), TEXT("abc")), 0);
|
||||
TestEqual("Levenshtein kitten->sitting is 3",
|
||||
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(TEXT("kitten"), TEXT("sitting")), 3);
|
||||
TestEqual("Levenshtein from empty equals the other length",
|
||||
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(TEXT(""), TEXT("abc")), 3);
|
||||
TestEqual("Levenshtein is case-sensitive by default",
|
||||
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(TEXT("ABC"), TEXT("abc")), 3);
|
||||
TestEqual("Levenshtein honors the case-insensitive flag",
|
||||
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(TEXT("ABC"), TEXT("abc"), /*bCaseSensitive*/ false), 0);
|
||||
|
||||
TestTrue("Similarity of identical strings is 1",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilStringFunctionLibrary::GetStringSimilarity(TEXT("abc"), TEXT("abc")), 1.0f, 1.e-4f));
|
||||
TestTrue("Similarity of two empty strings is 1",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilStringFunctionLibrary::GetStringSimilarity(TEXT(""), TEXT("")), 1.0f, 1.e-4f));
|
||||
TestTrue("Similarity of completely different equal-length strings is 0",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilStringFunctionLibrary::GetStringSimilarity(TEXT("abc"), TEXT("xyz")), 0.0f, 1.e-4f));
|
||||
TestTrue("Similarity kitten/sitting is ~0.571",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilStringFunctionLibrary::GetStringSimilarity(TEXT("kitten"), TEXT("sitting")), 1.0f - 3.0f / 7.0f, 1.e-3f));
|
||||
|
||||
TestTrue("ContainsAny finds a present term",
|
||||
UDirectiveUtilStringFunctionLibrary::ContainsAny(TEXT("Hello World"), {TEXT("foo"), TEXT("World")}));
|
||||
TestFalse("ContainsAny returns false when no term is present",
|
||||
UDirectiveUtilStringFunctionLibrary::ContainsAny(TEXT("Hello"), {TEXT("foo"), TEXT("bar")}));
|
||||
TestTrue("ContainsAny honors the case-insensitive flag",
|
||||
UDirectiveUtilStringFunctionLibrary::ContainsAny(TEXT("HELLO"), {TEXT("hello")}, /*bCaseSensitive*/ false));
|
||||
TestFalse("ContainsAny ignores empty terms",
|
||||
UDirectiveUtilStringFunctionLibrary::ContainsAny(TEXT("Hello"), {TEXT("")}));
|
||||
|
||||
{
|
||||
int32 FoundIndex = 0;
|
||||
int32 TermIndex = 0;
|
||||
const bool bFound = UDirectiveUtilStringFunctionLibrary::FindFirstOfAny(TEXT("abcXYZ123"), {TEXT("123"), TEXT("XYZ")}, true, FoundIndex, TermIndex);
|
||||
TestTrue("FindFirstOfAny should find a term", bFound);
|
||||
TestEqual("FindFirstOfAny should return the earliest match index", FoundIndex, 3);
|
||||
TestEqual("FindFirstOfAny should return which term matched earliest", TermIndex, 1);
|
||||
}
|
||||
{
|
||||
int32 FoundIndex = 5;
|
||||
int32 TermIndex = 5;
|
||||
const bool bFound = UDirectiveUtilStringFunctionLibrary::FindFirstOfAny(TEXT("abc"), {TEXT("x"), TEXT("y")}, true, FoundIndex, TermIndex);
|
||||
TestFalse("FindFirstOfAny should report no match", bFound);
|
||||
TestEqual("FindFirstOfAny should set found index to INDEX_NONE on failure", FoundIndex, static_cast<int32>(INDEX_NONE));
|
||||
TestEqual("FindFirstOfAny should set term index to INDEX_NONE on failure", TermIndex, static_cast<int32>(INDEX_NONE));
|
||||
}
|
||||
|
||||
{
|
||||
const FString Original = TEXT("Hello, DirectiveUtilities! 123");
|
||||
const FString Encoded = UDirectiveUtilStringFunctionLibrary::Base64Encode(Original);
|
||||
TestFalse("Base64Encode should produce a non-empty string", Encoded.IsEmpty());
|
||||
FString Decoded;
|
||||
const bool bDecoded = UDirectiveUtilStringFunctionLibrary::Base64Decode(Encoded, Decoded);
|
||||
TestTrue("Base64Decode should succeed for valid input", bDecoded);
|
||||
TestEqual("Base64 should round-trip the original string", Decoded, Original);
|
||||
}
|
||||
|
||||
{
|
||||
TestEqual("HexEncode(\"Hi!\") is \"486921\"",
|
||||
UDirectiveUtilStringFunctionLibrary::HexEncode(TEXT("Hi!")), FString(TEXT("486921")));
|
||||
TestEqual("HexEncode of an empty string is empty",
|
||||
UDirectiveUtilStringFunctionLibrary::HexEncode(TEXT("")), FString(TEXT("")));
|
||||
|
||||
FString Decoded;
|
||||
TestTrue("HexDecode should decode valid hex", UDirectiveUtilStringFunctionLibrary::HexDecode(TEXT("486921"), Decoded));
|
||||
TestEqual("HexDecode should round-trip HexEncode", Decoded, FString(TEXT("Hi!")));
|
||||
TestTrue("HexDecode should accept uppercase hex", UDirectiveUtilStringFunctionLibrary::HexDecode(TEXT("4A"), Decoded));
|
||||
TestEqual("HexDecode of \"4A\" is \"J\"", Decoded, FString(TEXT("J")));
|
||||
TestFalse("HexDecode should reject non-hex characters", UDirectiveUtilStringFunctionLibrary::HexDecode(TEXT("XYZ"), Decoded));
|
||||
TestFalse("HexDecode should reject odd-length input", UDirectiveUtilStringFunctionLibrary::HexDecode(TEXT("48692"), Decoded));
|
||||
|
||||
TestEqual("HexEncodeBytes({0x00, 0xFF}) is \"00ff\"",
|
||||
UDirectiveUtilStringFunctionLibrary::HexEncodeBytes({0x00, 0xFF}), FString(TEXT("00ff")));
|
||||
TArray<uint8> DecodedBytes;
|
||||
TestTrue("HexDecodeBytes should decode valid hex", UDirectiveUtilStringFunctionLibrary::HexDecodeBytes(TEXT("00ff"), DecodedBytes));
|
||||
TestEqual("HexDecodeBytes should round-trip HexEncodeBytes", DecodedBytes, TArray<uint8>({0x00, 0xFF}));
|
||||
TestFalse("HexDecodeBytes should reject non-hex characters", UDirectiveUtilStringFunctionLibrary::HexDecodeBytes(TEXT("zz"), DecodedBytes));
|
||||
}
|
||||
|
||||
TestEqual("Md5HashString(\"abc\") matches the known vector",
|
||||
UDirectiveUtilStringFunctionLibrary::Md5HashString(TEXT("abc")), FString(TEXT("900150983cd24fb0d6963f7d28e17f72")));
|
||||
TestEqual("Md5HashString of an empty string matches the known vector",
|
||||
UDirectiveUtilStringFunctionLibrary::Md5HashString(TEXT("")), FString(TEXT("d41d8cd98f00b204e9800998ecf8427e")));
|
||||
TestEqual("Sha1HashString(\"abc\") matches the known vector",
|
||||
UDirectiveUtilStringFunctionLibrary::Sha1HashString(TEXT("abc")), FString(TEXT("a9993e364706816aba3e25717850c26c9cd0d89d")));
|
||||
TestEqual("Sha1HashBytes of {0x61,0x62,0x63} matches the known vector",
|
||||
UDirectiveUtilStringFunctionLibrary::Sha1HashBytes({0x61, 0x62, 0x63}), FString(TEXT("a9993e364706816aba3e25717850c26c9cd0d89d")));
|
||||
TestEqual("Crc32String(\"abc\") equals Crc32Bytes of its ASCII bytes",
|
||||
UDirectiveUtilStringFunctionLibrary::Crc32String(TEXT("abc")),
|
||||
UDirectiveUtilStringFunctionLibrary::Crc32Bytes({0x61, 0x62, 0x63}));
|
||||
TestEqual("Crc32String of an empty string is 0",
|
||||
UDirectiveUtilStringFunctionLibrary::Crc32String(TEXT("")), 0);
|
||||
TestEqual("Md5HashString hashes the UTF-8 bytes of non-ASCII input",
|
||||
UDirectiveUtilStringFunctionLibrary::Md5HashString(TEXT("\u00E9")), // e-acute; UTF-8 bytes C3 A9
|
||||
UDirectiveUtilStringFunctionLibrary::Md5HashBytes({0xC3, 0xA9}));
|
||||
|
||||
TestTrue("IsValidFileName should accept a simple name", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("Save01")));
|
||||
TestTrue("IsValidFileName should accept a name with a space and extension", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("my save.sav")));
|
||||
TestFalse("IsValidFileName should reject a relative segment", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("../escape")));
|
||||
TestFalse("IsValidFileName should reject a forward slash", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("a/b")));
|
||||
TestFalse("IsValidFileName should reject a backslash", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("a\\b")));
|
||||
TestFalse("IsValidFileName should reject an empty string", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("")));
|
||||
TestFalse("IsValidFileName should reject a reserved character", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("a:b")));
|
||||
|
||||
{
|
||||
const FString Sanitized = UDirectiveUtilStringFunctionLibrary::SanitizeFileName(TEXT("../a/b?.sav"));
|
||||
TestTrue("SanitizeFileName should produce a name that IsValidFileName accepts",
|
||||
UDirectiveUtilStringFunctionLibrary::IsValidFileName(Sanitized));
|
||||
const FString Replaced = UDirectiveUtilStringFunctionLibrary::SanitizeFileName(TEXT("a/b"), TEXT("_"));
|
||||
TestTrue("SanitizeFileName should substitute the replacement character for stripped characters",
|
||||
Replaced.Contains(TEXT("_")));
|
||||
TestTrue("SanitizeFileName with a replacement should still produce a valid file name",
|
||||
UDirectiveUtilStringFunctionLibrary::IsValidFileName(Replaced));
|
||||
}
|
||||
|
||||
{
|
||||
float Similarity = -1.0f;
|
||||
TestEqual("FindBestStringMatch should return the exact match",
|
||||
UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(TEXT("color"), {TEXT("colour"), TEXT("color")}, Similarity), 1);
|
||||
TestTrue("FindBestStringMatch should report similarity 1 for an exact match",
|
||||
FMath::IsNearlyEqual(Similarity, 1.0f, 1.e-4f));
|
||||
|
||||
TestEqual("FindBestStringMatch should return the nearest match",
|
||||
UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(TEXT("colr"), {TEXT("color"), TEXT("colour"), TEXT("colt")}, Similarity), 0);
|
||||
|
||||
TestEqual("FindBestStringMatch should be case-insensitive by default",
|
||||
UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(TEXT("COLOR"), {TEXT("apple"), TEXT("color")}, Similarity), 1);
|
||||
TestTrue("FindBestStringMatch should report similarity 1 for a case-insensitive match",
|
||||
FMath::IsNearlyEqual(Similarity, 1.0f, 1.e-4f));
|
||||
|
||||
TestEqual("FindBestStringMatch should honor the case-sensitive flag",
|
||||
UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(TEXT("COLOR"), {TEXT("color"), TEXT("COLT")}, Similarity, /*bCaseSensitive*/ true), 1);
|
||||
|
||||
TestEqual("FindBestStringMatch should return -1 for an empty array",
|
||||
UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(TEXT("color"), TArray<FString>(), Similarity), static_cast<int32>(INDEX_NONE));
|
||||
TestTrue("FindBestStringMatch should report similarity 0 for an empty array",
|
||||
FMath::IsNearlyEqual(Similarity, 0.0f, 1.e-4f));
|
||||
|
||||
TestEqual("FindBestStringMatch should still return an index for an empty input",
|
||||
UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(TEXT(""), {TEXT("abc")}, Similarity), 0);
|
||||
TestTrue("FindBestStringMatch should report similarity 0 for an empty input against a non-empty candidate",
|
||||
FMath::IsNearlyEqual(Similarity, 0.0f, 1.e-4f));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#include "Libraries/DirectiveUtilTextFunctionLibrary.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilTextFunctionLibraryTest, "DirectiveUtilities.TextFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilTextFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
const FText NonEmptyText = FText::FromString(TEXT("Hello"));
|
||||
TestTrue("IsNotEmpty should return true for non-empty text", UDirectiveUtilTextFunctionLibrary::IsNotEmpty(NonEmptyText));
|
||||
|
||||
const FText EmptyText = FText::GetEmpty();
|
||||
TestFalse("IsNotEmpty should return false for empty text", UDirectiveUtilTextFunctionLibrary::IsNotEmpty(EmptyText));
|
||||
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user