1519 lines
68 KiB
C++
1519 lines
68 KiB
C++
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
|
|
|
#include "Libraries/DirectiveUtilArrayFunctionLibrary.h"
|
|
#include "Async/ParallelFor.h"
|
|
#include "Tests/DirectiveUtilTestObject.h"
|
|
|
|
#include <limits>
|
|
#include "Misc/AutomationTest.h"
|
|
|
|
namespace
|
|
{
|
|
TArray<int32> BuildReferenceSample(const int32 SourceCount, const int32 Count, const int32 Seed)
|
|
{
|
|
TArray<int32> AvailableIndices;
|
|
AvailableIndices.SetNumUninitialized(SourceCount);
|
|
for (int32 Index = 0; Index < SourceCount; ++Index)
|
|
{
|
|
AvailableIndices[Index] = Index;
|
|
}
|
|
|
|
FRandomStream RandomStream(Seed);
|
|
TArray<int32> Result;
|
|
const int32 SampleCount = FMath::Min(SourceCount, Count);
|
|
Result.Reserve(SampleCount);
|
|
for (int32 SampleIndex = 0; SampleIndex < SampleCount; ++SampleIndex)
|
|
{
|
|
const int32 SelectedIndex = RandomStream.RandRange(0, AvailableIndices.Num() - 1);
|
|
Result.Add(AvailableIndices[SelectedIndex]);
|
|
AvailableIndices.RemoveAtSwap(SelectedIndex, 1, EAllowShrinking::No);
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
TArray<int32> BuildReferenceDistinct(const TArray<int32>& Values)
|
|
{
|
|
TArray<int32> Result;
|
|
for (const int32 Value : Values)
|
|
{
|
|
if (!Result.Contains(Value))
|
|
{
|
|
Result.Add(Value);
|
|
}
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
TArray<int32> BuildReferenceRemoval(
|
|
const TArray<int32>& Values,
|
|
const TArray<int32>& Indices)
|
|
{
|
|
TArray<int32> Result;
|
|
Result.Reserve(Values.Num());
|
|
for (int32 Index = 0; Index < Values.Num(); ++Index)
|
|
{
|
|
if (!Indices.Contains(Index))
|
|
{
|
|
Result.Add(Values[Index]);
|
|
}
|
|
}
|
|
return Result;
|
|
}
|
|
}
|
|
|
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilArrayFunctionLibraryTest, "DirectiveUtilities.ArrayFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | 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);
|
|
TestEqual("Array_NextIndex should handle the maximum integer index",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, MAX_int32, false), 4);
|
|
TestEqual("Array_PreviousIndex should handle the minimum integer index",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, MIN_int32, false), 0);
|
|
|
|
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);
|
|
|
|
int32 ItemToRemove = 2;
|
|
TestObject->TestArray = {1, 2, 3, 2, 4, 2};
|
|
TestTrue(
|
|
"RemoveAllOccurrences should report matching values",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences(&TestObject->TestArray, ArrayProperty, &ItemToRemove));
|
|
TestEqual(
|
|
"RemoveAllOccurrences should remove every match and preserve survivor order",
|
|
TestObject->TestArray,
|
|
TArray<int32>({1, 3, 4}));
|
|
|
|
ItemToRemove = 9;
|
|
const TArray<int32> UnchangedValues = TestObject->TestArray;
|
|
TestFalse(
|
|
"RemoveAllOccurrences should report an absent value",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences(&TestObject->TestArray, ArrayProperty, &ItemToRemove));
|
|
TestEqual("RemoveAllOccurrences should not change an array without matches", TestObject->TestArray, UnchangedValues);
|
|
|
|
TestObject->TestArray.Empty();
|
|
TestFalse(
|
|
"RemoveAllOccurrences should report false for an empty array",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences(&TestObject->TestArray, ArrayProperty, &ItemToRemove));
|
|
|
|
ItemToRemove = 5;
|
|
TestObject->TestArray = {5, 5, 5};
|
|
TestTrue(
|
|
"RemoveAllOccurrences should remove an all-matching array",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences(&TestObject->TestArray, ArrayProperty, &ItemToRemove));
|
|
TestTrue("RemoveAllOccurrences should leave an all-matching array empty", TestObject->TestArray.IsEmpty());
|
|
|
|
TestObject->TestArray = {0, 1, 2, 3, 4, 5, 6};
|
|
TestEqual(
|
|
"RemoveAtIndices should remove each valid index once",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtIndices(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
TArray<int32>({5, 1, 5, -1, 99, 3})),
|
|
3);
|
|
TestEqual(
|
|
"RemoveAtIndices should preserve survivor order",
|
|
TestObject->TestArray,
|
|
TArray<int32>({0, 2, 4, 6}));
|
|
|
|
const TArray<int32> BeforeInvalidIndices = TestObject->TestArray;
|
|
TestEqual(
|
|
"RemoveAtIndices should report zero for invalid indices",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtIndices(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
TArray<int32>({-5, 10, 10})),
|
|
0);
|
|
TestEqual(
|
|
"RemoveAtIndices should not modify the target when no indices are valid",
|
|
TestObject->TestArray,
|
|
BeforeInvalidIndices);
|
|
|
|
TestObject->TestArray = {10, 20, 30};
|
|
TestEqual(
|
|
"RemoveAtIndices should remove the full array",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtIndices(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
TArray<int32>({2, 0, 1, 1})),
|
|
3);
|
|
TestTrue("RemoveAtIndices should leave the array empty when every index is removed", TestObject->TestArray.IsEmpty());
|
|
|
|
TestObject->TestArray = {0, 2, 4};
|
|
TestEqual(
|
|
"RemoveAtIndices should support using the target array as the index array",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtIndices(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
TestObject->TestArray),
|
|
2);
|
|
TestEqual(
|
|
"RemoveAtIndices should read aliased indices before modifying the target",
|
|
TestObject->TestArray,
|
|
TArray<int32>({2}));
|
|
|
|
TestTrue("Integer arrays should use the bulk append path", ArrayProperty->Inner->HasAnyPropertyFlags(CPF_IsPlainOldData));
|
|
const TArray<int32> IntegerAppendSource = {3, 4, 5};
|
|
TestObject->TestArray = {1, 2};
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
&IntegerAppendSource,
|
|
ArrayProperty);
|
|
TestEqual(
|
|
"AppendOptimized should append POD values in order",
|
|
TestObject->TestArray,
|
|
TArray<int32>({1, 2, 3, 4, 5}));
|
|
TestEqual(
|
|
"AppendOptimized should not modify the source array",
|
|
IntegerAppendSource,
|
|
TArray<int32>({3, 4, 5}));
|
|
|
|
TestObject->TestArray.Empty();
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
&IntegerAppendSource,
|
|
ArrayProperty);
|
|
TestEqual(
|
|
"AppendOptimized should append into an empty target array",
|
|
TestObject->TestArray,
|
|
IntegerAppendSource);
|
|
|
|
const TArray<int32> EmptyAppendSource;
|
|
const TArray<int32> BeforeEmptyAppend = TestObject->TestArray;
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
&EmptyAppendSource,
|
|
ArrayProperty);
|
|
TestEqual("AppendOptimized should ignore an empty source array", TestObject->TestArray, BeforeEmptyAppend);
|
|
|
|
TestObject->TestArray = {7, 8, 9};
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
&TestObject->TestArray,
|
|
ArrayProperty);
|
|
TestEqual(
|
|
"AppendOptimized should support appending an array to itself",
|
|
TestObject->TestArray,
|
|
TArray<int32>({7, 8, 9, 7, 8, 9}));
|
|
|
|
const TArray<int32> IntegerInsertSource = {7, 8};
|
|
TestObject->TestArray = {1, 2, 3};
|
|
TestTrue(
|
|
"InsertOptimized should insert POD values",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
&IntegerInsertSource,
|
|
ArrayProperty,
|
|
1));
|
|
TestEqual(
|
|
"InsertOptimized should preserve target and source order",
|
|
TestObject->TestArray,
|
|
TArray<int32>({1, 7, 8, 2, 3}));
|
|
TestEqual(
|
|
"InsertOptimized should not modify the source array",
|
|
IntegerInsertSource,
|
|
TArray<int32>({7, 8}));
|
|
|
|
TestObject->TestArray = {1, 2, 3};
|
|
TestTrue(
|
|
"InsertOptimized should insert at the front",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
&IntegerInsertSource,
|
|
ArrayProperty,
|
|
0));
|
|
TestEqual(
|
|
"InsertOptimized should preserve order at the front",
|
|
TestObject->TestArray,
|
|
TArray<int32>({7, 8, 1, 2, 3}));
|
|
|
|
TestObject->TestArray = {1, 2, 3};
|
|
TestTrue(
|
|
"InsertOptimized should insert at the end",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
&IntegerInsertSource,
|
|
ArrayProperty,
|
|
TestObject->TestArray.Num()));
|
|
TestEqual(
|
|
"InsertOptimized should preserve order at the end",
|
|
TestObject->TestArray,
|
|
TArray<int32>({1, 2, 3, 7, 8}));
|
|
|
|
const TArray<int32> EmptyInsertSource;
|
|
const TArray<int32> BeforeEmptyInsert = TestObject->TestArray;
|
|
TestFalse(
|
|
"InsertOptimized should report an empty source",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
&EmptyInsertSource,
|
|
ArrayProperty,
|
|
1));
|
|
TestEqual("InsertOptimized should ignore an empty source", TestObject->TestArray, BeforeEmptyInsert);
|
|
|
|
TestFalse(
|
|
"InsertOptimized should reject a negative index",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
&IntegerInsertSource,
|
|
ArrayProperty,
|
|
-1));
|
|
TestEqual("InsertOptimized should not modify the target for a negative index", TestObject->TestArray, BeforeEmptyInsert);
|
|
|
|
TestFalse(
|
|
"InsertOptimized should reject an index beyond the array end",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
&IntegerInsertSource,
|
|
ArrayProperty,
|
|
TestObject->TestArray.Num() + 1));
|
|
TestEqual("InsertOptimized should not modify the target for a large index", TestObject->TestArray, BeforeEmptyInsert);
|
|
|
|
TestObject->TestArray = {4, 5, 6};
|
|
TestTrue(
|
|
"InsertOptimized should support inserting an array into itself",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
1));
|
|
TestEqual(
|
|
"InsertOptimized should preserve self-inserted values",
|
|
TestObject->TestArray,
|
|
TArray<int32>({4, 4, 5, 6, 5, 6}));
|
|
|
|
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 = {10, 20, 30, 40};
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(&TestObject->TestArray, ArrayProperty, 1, 2, &TestObject->TestArray, ArrayProperty);
|
|
TestEqual("Slice should support using the source array as its output", TestObject->TestArray, TArray<int32>({20, 30}));
|
|
|
|
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 = {1, 2, 2, 3, 1};
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct(&TestObject->TestArray, ArrayProperty, &TestObject->TestArray, ArrayProperty);
|
|
TestEqual("GetDistinct should support using the source array as its output", TestObject->TestArray, TArray<int32>({1, 2, 3}));
|
|
|
|
FArrayProperty* DistinctStringArrayProperty = FindFProperty<FArrayProperty>(UDirectiveUtilTestObject::StaticClass(), GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestStringArray));
|
|
TestNotNull("TestStringArray property should be found", DistinctStringArrayProperty);
|
|
if (DistinctStringArrayProperty)
|
|
{
|
|
TestObject->TestStringArray = {TEXT("Alpha"), TEXT("Beta"), TEXT("Beta")};
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct(
|
|
&TestObject->TestStringArray,
|
|
DistinctStringArrayProperty,
|
|
&TestObject->TestStringArray,
|
|
DistinctStringArrayProperty);
|
|
TestEqual("GetDistinct should preserve in-place string values", TestObject->TestStringArray, TArray<FString>({TEXT("Alpha"), TEXT("Beta")}));
|
|
}
|
|
|
|
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);
|
|
|
|
const TArray<FName> MutatingFunctions = {
|
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveDuplicates),
|
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_AppendOptimized),
|
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_InsertOptimized),
|
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_Pop),
|
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_PopFirst),
|
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveAtSwap),
|
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveAtIndices),
|
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveAllOccurrences),
|
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_Rotate)
|
|
};
|
|
for (const FName FunctionName : MutatingFunctions)
|
|
{
|
|
const UFunction* Function = UDirectiveUtilArrayFunctionLibrary::StaticClass()->FindFunctionByName(FunctionName);
|
|
const FArrayProperty* TargetArrayProperty = Function ? FindFProperty<FArrayProperty>(Function, TEXT("TargetArray")) : nullptr;
|
|
TestNotNull(*FString::Printf(TEXT("%s should expose a TargetArray parameter"), *FunctionName.ToString()), TargetArrayProperty);
|
|
if (TargetArrayProperty)
|
|
{
|
|
TestFalse(*FString::Printf(TEXT("%s should expose TargetArray as mutable"), *FunctionName.ToString()), TargetArrayProperty->HasAnyPropertyFlags(CPF_ConstParm));
|
|
}
|
|
#if WITH_EDITOR
|
|
if (Function)
|
|
{
|
|
TestFalse(
|
|
*FString::Printf(TEXT("%s should not advertise inert Blueprint thread safety"), *FunctionName.ToString()),
|
|
Function->HasMetaData(TEXT("BlueprintThreadSafe")));
|
|
}
|
|
#endif
|
|
}
|
|
|
|
TArray<int32> ParallelResults;
|
|
ParallelResults.SetNumZeroed(64);
|
|
ParallelFor(ParallelResults.Num(), [&ParallelResults, ArrayProperty](const int32 TaskIndex)
|
|
{
|
|
const int32 RemovedValue = TaskIndex + 1000;
|
|
TArray<int32> Values = {RemovedValue, 7, RemovedValue, 11};
|
|
const TArray<int32> Appended = {13, RemovedValue};
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized(
|
|
&Values, ArrayProperty, &Appended, ArrayProperty);
|
|
const bool bRemoved = UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences(
|
|
&Values, ArrayProperty, &RemovedValue);
|
|
ParallelResults[TaskIndex] = bRemoved && Values == TArray<int32>({7, 11, 13}) ? 1 : 0;
|
|
});
|
|
TestTrue("Independent array operations should remain correct on worker tasks", ParallelResults.Find(0) == INDEX_NONE);
|
|
|
|
TestObject->TestArray = {10, 20, 30, 40, 50};
|
|
TArray<int32> SampledValues;
|
|
FRandomStream FirstStream(1337);
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 3, false, &FirstStream, &SampledValues, ArrayProperty);
|
|
TestEqual("Sample without replacement should return the requested count", SampledValues.Num(), 3);
|
|
TestEqual("Sample without replacement should contain unique values", TSet<int32>(SampledValues).Num(), 3);
|
|
|
|
TArray<int32> RepeatedSample;
|
|
FRandomStream SecondStream(1337);
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 3, false, &SecondStream, &RepeatedSample, ArrayProperty);
|
|
TestEqual("Sample from stream should be deterministic", RepeatedSample, SampledValues);
|
|
|
|
FRandomStream ClampedStream(42);
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 20, false, &ClampedStream, &SampledValues, ArrayProperty);
|
|
TestEqual("Sample without replacement should clamp to the source length", SampledValues.Num(), TestObject->TestArray.Num());
|
|
|
|
FRandomStream ReplacementStream(7);
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 20, true, &ReplacementStream, &SampledValues, ArrayProperty);
|
|
TestEqual("Sample with replacement should return the requested count", SampledValues.Num(), 20);
|
|
|
|
TestObject->TestArray = {99};
|
|
FRandomStream SingleValueStream(7);
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 4, true, &SingleValueStream, &SampledValues, ArrayProperty);
|
|
TestEqual("Sample with replacement should repeat the only available value", SampledValues, TArray<int32>({99, 99, 99, 99}));
|
|
|
|
TestObject->TestArray.Empty();
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 4, false, nullptr, &SampledValues, ArrayProperty);
|
|
TestTrue("Sampling an empty array should return an empty array", SampledValues.IsEmpty());
|
|
|
|
TestObject->TestArray = {10, 20, 30, 40, 50};
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 0, false, nullptr, &SampledValues, ArrayProperty);
|
|
TestTrue("Sampling zero values should return an empty array", SampledValues.IsEmpty());
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 1'000'001, true, nullptr, &SampledValues, ArrayProperty);
|
|
TestTrue("Sampling should reject an unsafe output count", SampledValues.IsEmpty());
|
|
|
|
const TArray<float> DeterministicWeights = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f};
|
|
TArray<int32> WeightedSample;
|
|
TArray<int32> RepeatedWeightedSample;
|
|
FRandomStream FirstWeightedStream(90210);
|
|
FRandomStream SecondWeightedStream(90210);
|
|
TestTrue(
|
|
"Weighted sampling should accept matching inputs",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
DeterministicWeights,
|
|
4,
|
|
true,
|
|
&FirstWeightedStream,
|
|
&WeightedSample,
|
|
ArrayProperty));
|
|
TestTrue(
|
|
"Weighted sampling from a stream should accept matching inputs",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
DeterministicWeights,
|
|
4,
|
|
true,
|
|
&SecondWeightedStream,
|
|
&RepeatedWeightedSample,
|
|
ArrayProperty));
|
|
TestEqual("Weighted sampling from a stream should be deterministic", RepeatedWeightedSample, WeightedSample);
|
|
TestFalse(
|
|
"Weighted sampling should reject an unsafe output count",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
DeterministicWeights,
|
|
1'000'001,
|
|
true,
|
|
nullptr,
|
|
&WeightedSample,
|
|
ArrayProperty));
|
|
TestTrue("Rejected weighted sampling should clear the output", WeightedSample.IsEmpty());
|
|
|
|
FRandomStream FirstUniqueWeightedStream(31415);
|
|
FRandomStream SecondUniqueWeightedStream(31415);
|
|
TestTrue(
|
|
"Weighted sampling without replacement should accept matching inputs",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
DeterministicWeights,
|
|
5,
|
|
false,
|
|
&FirstUniqueWeightedStream,
|
|
&WeightedSample,
|
|
ArrayProperty));
|
|
TestTrue(
|
|
"Repeated weighted sampling without replacement should accept matching inputs",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
DeterministicWeights,
|
|
5,
|
|
false,
|
|
&SecondUniqueWeightedStream,
|
|
&RepeatedWeightedSample,
|
|
ArrayProperty));
|
|
TestEqual("Weighted sampling without replacement should be deterministic", RepeatedWeightedSample, WeightedSample);
|
|
TestEqual("Weighted sampling without replacement should select each available value once", TSet<int32>(WeightedSample).Num(), 5);
|
|
|
|
FRandomStream UnchangedWeightedStream(8675309);
|
|
const int32 InitialWeightedSeed = UnchangedWeightedStream.GetCurrentSeed();
|
|
TestTrue(
|
|
"A zero weighted sample should succeed",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
DeterministicWeights,
|
|
0,
|
|
false,
|
|
&UnchangedWeightedStream,
|
|
&WeightedSample,
|
|
ArrayProperty));
|
|
TestEqual("A zero weighted sample should not advance its random stream", UnchangedWeightedStream.GetCurrentSeed(), InitialWeightedSeed);
|
|
|
|
TestObject->TestArray = {0, 1};
|
|
FRandomStream DistributionStream(1187);
|
|
TestTrue(
|
|
"Weighted sampling should accept proportional weights",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
TArray<float>({1.0f, 9.0f}),
|
|
10000,
|
|
true,
|
|
&DistributionStream,
|
|
&WeightedSample,
|
|
ArrayProperty));
|
|
int32 HeavySelectionCount = 0;
|
|
for (const int32 Value : WeightedSample)
|
|
{
|
|
HeavySelectionCount += Value == 1 ? 1 : 0;
|
|
}
|
|
TestTrue("Weighted sampling should follow the supplied proportions", HeavySelectionCount > 8500 && HeavySelectionCount < 9500);
|
|
|
|
TestObject->TestArray = {10, 20, 30, 40, 50};
|
|
const TArray<float> SparseWeights = {
|
|
0.0f,
|
|
1.0f,
|
|
-1.0f,
|
|
std::numeric_limits<float>::quiet_NaN(),
|
|
std::numeric_limits<float>::infinity()
|
|
};
|
|
FRandomStream SparseStream(17);
|
|
TestTrue(
|
|
"Weighted sampling should ignore non-positive and non-finite weights",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
SparseWeights,
|
|
8,
|
|
true,
|
|
&SparseStream,
|
|
&WeightedSample,
|
|
ArrayProperty));
|
|
TestEqual("Weighted sampling should select the only positive-weight value", WeightedSample, TArray<int32>({20, 20, 20, 20, 20, 20, 20, 20}));
|
|
|
|
const TArray<float> TwoPositiveWeights = {1.0f, 0.0f, 2.0f, 0.0f, 0.0f};
|
|
FRandomStream UniqueWeightedStream(44);
|
|
TestTrue(
|
|
"Weighted sampling without replacement should succeed",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
TwoPositiveWeights,
|
|
8,
|
|
false,
|
|
&UniqueWeightedStream,
|
|
&WeightedSample,
|
|
ArrayProperty));
|
|
TestEqual("Weighted sampling without replacement should clamp to positive-weight entries", WeightedSample.Num(), 2);
|
|
TestEqual("Weighted sampling without replacement should not repeat source indices", TSet<int32>(WeightedSample).Num(), 2);
|
|
TestTrue("Weighted sampling without replacement should include the first weighted value", WeightedSample.Contains(10));
|
|
TestTrue("Weighted sampling without replacement should include the second weighted value", WeightedSample.Contains(30));
|
|
|
|
WeightedSample = {999};
|
|
TestFalse(
|
|
"Weighted sampling should reject a mismatched weights array",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
TArray<float>({1.0f}),
|
|
1,
|
|
false,
|
|
nullptr,
|
|
&WeightedSample,
|
|
ArrayProperty));
|
|
TestTrue("Weighted sampling should clear output after a weights mismatch", WeightedSample.IsEmpty());
|
|
|
|
TestFalse(
|
|
"Weighted sampling should reject a negative count",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
TwoPositiveWeights,
|
|
-1,
|
|
false,
|
|
nullptr,
|
|
&WeightedSample,
|
|
ArrayProperty));
|
|
TestTrue("Weighted sampling should clear output after a negative count", WeightedSample.IsEmpty());
|
|
|
|
TestTrue(
|
|
"Weighted sampling should accept a zero count",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
TwoPositiveWeights,
|
|
0,
|
|
false,
|
|
nullptr,
|
|
&WeightedSample,
|
|
ArrayProperty));
|
|
TestTrue("Weighted sampling should return an empty zero-count result", WeightedSample.IsEmpty());
|
|
|
|
const TArray<float> EmptyWeights = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
|
|
TestFalse(
|
|
"Weighted sampling should reject a positive count when all weights are zero",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
EmptyWeights,
|
|
1,
|
|
false,
|
|
nullptr,
|
|
&WeightedSample,
|
|
ArrayProperty));
|
|
TestTrue("Weighted sampling should clear output when no selectable entries exist", WeightedSample.IsEmpty());
|
|
|
|
TestObject->TestArray.Empty();
|
|
TestTrue(
|
|
"Weighted sampling should accept an empty source for a zero count",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
TArray<float>(),
|
|
0,
|
|
false,
|
|
nullptr,
|
|
&WeightedSample,
|
|
ArrayProperty));
|
|
TestFalse(
|
|
"Weighted sampling should reject a positive count for an empty source",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
TArray<float>(),
|
|
1,
|
|
false,
|
|
nullptr,
|
|
&WeightedSample,
|
|
ArrayProperty));
|
|
TestTrue("A failed empty weighted sample should leave an empty output", WeightedSample.IsEmpty());
|
|
|
|
TestObject->TestArray = {10, 20, 30, 40};
|
|
FRandomStream LeadingZeroStream(101);
|
|
TestTrue(
|
|
"Weighted sampling should handle leading zero weights",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
TArray<float>({0.0f, 0.0f, 0.0f, 1.0f}),
|
|
4,
|
|
true,
|
|
&LeadingZeroStream,
|
|
&WeightedSample,
|
|
ArrayProperty));
|
|
TestEqual("Leading zero weights should not select an unavailable value", WeightedSample, TArray<int32>({40, 40, 40, 40}));
|
|
|
|
FRandomStream ExtremeWeightStream(2026);
|
|
TestTrue(
|
|
"Weighted sampling should handle maximum finite weights",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
TArray<float>({std::numeric_limits<float>::max(), std::numeric_limits<float>::max(), 0.0f, 0.0f}),
|
|
2,
|
|
false,
|
|
&ExtremeWeightStream,
|
|
&WeightedSample,
|
|
ArrayProperty));
|
|
TestEqual("Maximum finite weights should return both selectable values", WeightedSample.Num(), 2);
|
|
TestTrue("Maximum finite weights should include the first value", WeightedSample.Contains(10));
|
|
TestTrue("Maximum finite weights should include the second value", WeightedSample.Contains(20));
|
|
|
|
TestObject->TestArray = {10, 20, 30};
|
|
FRandomStream MixedRangeWeightStream(2027);
|
|
TestTrue(
|
|
"Weighted sampling should handle mixed finite weight ranges",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
TArray<float>({std::numeric_limits<float>::max(), 1.0f, 2.0f}),
|
|
3,
|
|
false,
|
|
&MixedRangeWeightStream,
|
|
&WeightedSample,
|
|
ArrayProperty));
|
|
TestEqual("Mixed finite weights should return every selectable value", WeightedSample.Num(), 3);
|
|
TestEqual("Mixed finite weights should not repeat values", TSet<int32>(WeightedSample).Num(), 3);
|
|
TestTrue("Mixed finite weights should include the first value", WeightedSample.Contains(10));
|
|
TestTrue("Mixed finite weights should include the second value", WeightedSample.Contains(20));
|
|
TestTrue("Mixed finite weights should include the third value", WeightedSample.Contains(30));
|
|
|
|
TestObject->TestArray = {1, 2, 3, 4};
|
|
FRandomStream AliasedWeightedStream(81);
|
|
TestTrue(
|
|
"Weighted sampling should support the source array as output",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
TArray<float>({1.0f, 1.0f, 1.0f, 1.0f}),
|
|
2,
|
|
false,
|
|
&AliasedWeightedStream,
|
|
&TestObject->TestArray,
|
|
ArrayProperty));
|
|
TestEqual("Aliased weighted sampling should return the requested count", TestObject->TestArray.Num(), 2);
|
|
|
|
TestObject->TestArray = {10, 20, 30, 40, 50};
|
|
TArray<int32> Page;
|
|
int32 PageCount = 0;
|
|
TestTrue("GetPage should return an available page", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetPage(&TestObject->TestArray, ArrayProperty, 1, 2, &Page, ArrayProperty, &PageCount));
|
|
TestEqual("GetPage should return the second page", Page, TArray<int32>({30, 40}));
|
|
TestEqual("GetPage should return the total page count", PageCount, 3);
|
|
TestFalse("GetPage should reject a negative page", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetPage(&TestObject->TestArray, ArrayProperty, -1, 2, &Page, ArrayProperty, &PageCount));
|
|
TestTrue("GetPage should clear output for a negative page", Page.IsEmpty());
|
|
TestFalse("GetPage should reject a non-positive page size", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetPage(&TestObject->TestArray, ArrayProperty, 0, 0, &Page, ArrayProperty, &PageCount));
|
|
TestEqual("GetPage should report zero pages for an invalid page size", PageCount, 0);
|
|
|
|
TestObject->TestArray = {1, 2, 3, 4};
|
|
FRandomStream InPlaceStream(11);
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(&TestObject->TestArray, ArrayProperty, 2, false, &InPlaceStream, &TestObject->TestArray, ArrayProperty);
|
|
TestEqual("Sample should support the same source and output array", TestObject->TestArray.Num(), 2);
|
|
TestObject->TestArray = {1, 2, 3, 4};
|
|
TestTrue("GetPage should support the same source and output array", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetPage(&TestObject->TestArray, ArrayProperty, 1, 2, &TestObject->TestArray, ArrayProperty, &PageCount));
|
|
TestEqual("In-place GetPage should return the requested values", TestObject->TestArray, TArray<int32>({3, 4}));
|
|
|
|
TArray<FString> NaturalStrings = {TEXT("Item10"), TEXT("Item2"), TEXT("Item1"), TEXT("アイテム2")};
|
|
UDirectiveUtilArrayFunctionLibrary::NaturalSortStringArray(NaturalStrings);
|
|
TestEqual("Natural string sort should compare embedded numbers", NaturalStrings[0], FString(TEXT("Item1")));
|
|
TestEqual("Natural string sort should place Item2 before Item10", NaturalStrings[1], FString(TEXT("Item2")));
|
|
TestEqual("Natural string sort should place Item10 after Item2", NaturalStrings[2], FString(TEXT("Item10")));
|
|
TestEqual("Natural string sort should preserve Unicode strings", NaturalStrings[3], FString(TEXT("アイテム2")));
|
|
UDirectiveUtilArrayFunctionLibrary::NaturalSortStringArray(NaturalStrings, true);
|
|
TestEqual("Natural string sort should support descending order", NaturalStrings.Last(), FString(TEXT("Item1")));
|
|
|
|
TArray<FName> NaturalNames = {FName(TEXT("Actor12")), FName(TEXT("Actor3")), FName(TEXT("Actor1"))};
|
|
UDirectiveUtilArrayFunctionLibrary::NaturalSortNameArray(NaturalNames);
|
|
TestEqual("Natural name sort should compare embedded numbers", NaturalNames, TArray<FName>({FName(TEXT("Actor1")), FName(TEXT("Actor3")), FName(TEXT("Actor12"))}));
|
|
|
|
TestObject->TestArray = {1, 2, 3, 4, 5};
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(&TestObject->TestArray, ArrayProperty, 1, 3, &TestObject->TestArray, ArrayProperty);
|
|
TestEqual("Slice should support the same source and output array", TestObject->TestArray, TArray<int32>({2, 3, 4}));
|
|
|
|
TestObject->TestArray = {3, 1, 3, 2, 1};
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct(&TestObject->TestArray, ArrayProperty, &TestObject->TestArray, ArrayProperty);
|
|
TestEqual("GetDistinct should support the same source and output array", TestObject->TestArray, TArray<int32>({3, 1, 2}));
|
|
|
|
FArrayProperty* StringArrayProperty = FindFProperty<FArrayProperty>(
|
|
UDirectiveUtilTestObject::StaticClass(),
|
|
GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestStringArray));
|
|
FArrayProperty* TextArrayProperty = FindFProperty<FArrayProperty>(
|
|
UDirectiveUtilTestObject::StaticClass(),
|
|
GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestTextArray));
|
|
FArrayProperty* BoolArrayProperty = FindFProperty<FArrayProperty>(
|
|
UDirectiveUtilTestObject::StaticClass(),
|
|
GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestBoolArray));
|
|
FArrayProperty* CollisionArrayProperty = FindFProperty<FArrayProperty>(
|
|
UDirectiveUtilTestObject::StaticClass(),
|
|
GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestCollisionArray));
|
|
FArrayProperty* PodArrayProperty = FindFProperty<FArrayProperty>(
|
|
UDirectiveUtilTestObject::StaticClass(),
|
|
GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestPodArray));
|
|
FArrayProperty* ObjectArrayProperty = FindFProperty<FArrayProperty>(
|
|
UDirectiveUtilTestObject::StaticClass(),
|
|
GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestObjectArray));
|
|
TestNotNull("String array property should be available", StringArrayProperty);
|
|
TestNotNull("Text array property should be available", TextArrayProperty);
|
|
TestNotNull("Boolean array property should be available", BoolArrayProperty);
|
|
TestNotNull("Collision array property should be available", CollisionArrayProperty);
|
|
TestNotNull("POD struct array property should be available", PodArrayProperty);
|
|
TestNotNull("Object array property should be available", ObjectArrayProperty);
|
|
|
|
TestFalse("String arrays should use the property-aware append path", StringArrayProperty->Inner->HasAnyPropertyFlags(CPF_IsPlainOldData));
|
|
const TArray<FString> StringAppendSource = {TEXT("Three"), TEXT("Four")};
|
|
TestObject->TestStringArray = {TEXT("One"), TEXT("Two")};
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized(
|
|
&TestObject->TestStringArray,
|
|
StringArrayProperty,
|
|
&StringAppendSource,
|
|
StringArrayProperty);
|
|
TestEqual(
|
|
"AppendOptimized should preserve non-trivial values",
|
|
TestObject->TestStringArray,
|
|
TArray<FString>({TEXT("One"), TEXT("Two"), TEXT("Three"), TEXT("Four")}));
|
|
|
|
TestObject->TestStringArray = {TEXT("Alpha"), TEXT("Beta")};
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized(
|
|
&TestObject->TestStringArray,
|
|
StringArrayProperty,
|
|
&TestObject->TestStringArray,
|
|
StringArrayProperty);
|
|
TestEqual(
|
|
"AppendOptimized should support self-appending non-trivial values",
|
|
TestObject->TestStringArray,
|
|
TArray<FString>({TEXT("Alpha"), TEXT("Beta"), TEXT("Alpha"), TEXT("Beta")}));
|
|
|
|
const TArray<FString> StringInsertSource = {TEXT("Two"), TEXT("Three")};
|
|
TestObject->TestStringArray = {TEXT("One"), TEXT("Four")};
|
|
TestTrue(
|
|
"InsertOptimized should support non-trivial values",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized(
|
|
&TestObject->TestStringArray,
|
|
StringArrayProperty,
|
|
&StringInsertSource,
|
|
StringArrayProperty,
|
|
1));
|
|
TestEqual(
|
|
"InsertOptimized should preserve non-trivial value order",
|
|
TestObject->TestStringArray,
|
|
TArray<FString>({TEXT("One"), TEXT("Two"), TEXT("Three"), TEXT("Four")}));
|
|
|
|
TestObject->TestStringArray = {TEXT("Alpha"), TEXT("Beta"), TEXT("Gamma")};
|
|
TestTrue(
|
|
"InsertOptimized should self-insert non-trivial values",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized(
|
|
&TestObject->TestStringArray,
|
|
StringArrayProperty,
|
|
&TestObject->TestStringArray,
|
|
StringArrayProperty,
|
|
2));
|
|
TestEqual(
|
|
"InsertOptimized should preserve self-inserted non-trivial values",
|
|
TestObject->TestStringArray,
|
|
TArray<FString>({
|
|
TEXT("Alpha"),
|
|
TEXT("Beta"),
|
|
TEXT("Alpha"),
|
|
TEXT("Beta"),
|
|
TEXT("Gamma"),
|
|
TEXT("Gamma")
|
|
}));
|
|
|
|
TestObject->TestStringArray = {
|
|
TEXT("Zero"),
|
|
TEXT("One"),
|
|
TEXT("Two"),
|
|
TEXT("Three"),
|
|
TEXT("Four")
|
|
};
|
|
TestEqual(
|
|
"RemoveAtIndices should remove non-trivial values",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtIndices(
|
|
&TestObject->TestStringArray,
|
|
StringArrayProperty,
|
|
TArray<int32>({3, 1, 3})),
|
|
2);
|
|
TestEqual(
|
|
"RemoveAtIndices should preserve non-trivial survivor order",
|
|
TestObject->TestStringArray,
|
|
TArray<FString>({TEXT("Zero"), TEXT("Two"), TEXT("Four")}));
|
|
|
|
const TArray<bool> BoolAppendSource = {false, true, false};
|
|
TestObject->TestBoolArray = {true};
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized(
|
|
&TestObject->TestBoolArray,
|
|
BoolArrayProperty,
|
|
&BoolAppendSource,
|
|
BoolArrayProperty);
|
|
TestEqual(
|
|
"AppendOptimized should preserve Boolean values",
|
|
TestObject->TestBoolArray,
|
|
TArray<bool>({true, false, true, false}));
|
|
|
|
TestObject->TestBoolArray = {true, false, false};
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized(
|
|
&TestObject->TestBoolArray,
|
|
BoolArrayProperty,
|
|
&TestObject->TestBoolArray,
|
|
BoolArrayProperty);
|
|
TestEqual(
|
|
"AppendOptimized should support self-appending Boolean values",
|
|
TestObject->TestBoolArray,
|
|
TArray<bool>({true, false, false, true, false, false}));
|
|
|
|
auto MakePodValue = [](const int32 Index, const float Weight)
|
|
{
|
|
FDirectiveUtilPodValue Value;
|
|
Value.Index = Index;
|
|
Value.Weight = Weight;
|
|
return Value;
|
|
};
|
|
TestTrue("POD struct arrays should use the bulk append path", PodArrayProperty->Inner->HasAnyPropertyFlags(CPF_IsPlainOldData));
|
|
const TArray<FDirectiveUtilPodValue> PodAppendSource = {
|
|
MakePodValue(2, 2.5f),
|
|
MakePodValue(3, 3.5f)
|
|
};
|
|
TestObject->TestPodArray = {MakePodValue(1, 1.5f)};
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized(
|
|
&TestObject->TestPodArray,
|
|
PodArrayProperty,
|
|
&PodAppendSource,
|
|
PodArrayProperty);
|
|
TestEqual("AppendOptimized should append POD structs", TestObject->TestPodArray.Num(), 3);
|
|
for (int32 Index = 0; Index < TestObject->TestPodArray.Num(); ++Index)
|
|
{
|
|
TestEqual("AppendOptimized should preserve POD struct indices", TestObject->TestPodArray[Index].Index, Index + 1);
|
|
TestEqual("AppendOptimized should preserve POD struct weights", TestObject->TestPodArray[Index].Weight, static_cast<float>(Index) + 1.5f);
|
|
}
|
|
TestEqual("AppendOptimized should not modify the POD struct source", PodAppendSource.Num(), 2);
|
|
if (PodAppendSource.Num() == 2)
|
|
{
|
|
TestEqual("AppendOptimized should preserve the first POD source index", PodAppendSource[0].Index, 2);
|
|
TestEqual("AppendOptimized should preserve the second POD source index", PodAppendSource[1].Index, 3);
|
|
}
|
|
|
|
TestObject->TestPodArray = {MakePodValue(4, 4.5f), MakePodValue(5, 5.5f)};
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized(
|
|
&TestObject->TestPodArray,
|
|
PodArrayProperty,
|
|
&TestObject->TestPodArray,
|
|
PodArrayProperty);
|
|
TestEqual("AppendOptimized should self-append POD structs", TestObject->TestPodArray.Num(), 4);
|
|
if (TestObject->TestPodArray.Num() == 4)
|
|
{
|
|
TestEqual("AppendOptimized should preserve the first self-appended POD struct", TestObject->TestPodArray[2].Index, 4);
|
|
TestEqual("AppendOptimized should preserve the second self-appended POD struct", TestObject->TestPodArray[3].Index, 5);
|
|
}
|
|
|
|
TestFalse("POD struct grouping should use the reflected-value hash path", PodArrayProperty->Inner->HasAllPropertyFlags(CPF_HasGetValueTypeHash));
|
|
TestObject->TestPodArray = {
|
|
MakePodValue(1, 1.5f),
|
|
MakePodValue(2, 2.5f),
|
|
MakePodValue(1, 1.5f),
|
|
MakePodValue(3, 3.5f),
|
|
MakePodValue(1, 1.5f)
|
|
};
|
|
UDirectiveUtilTestObject* PodResultObject = NewObject<UDirectiveUtilTestObject>();
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct(
|
|
&TestObject->TestPodArray,
|
|
PodArrayProperty,
|
|
&PodResultObject->TestPodArray,
|
|
PodArrayProperty);
|
|
TestEqual("GetDistinct should group unhashable POD structs", PodResultObject->TestPodArray.Num(), 3);
|
|
if (PodResultObject->TestPodArray.Num() == 3)
|
|
{
|
|
TestEqual("GetDistinct should retain the first POD value", PodResultObject->TestPodArray[0].Index, 1);
|
|
TestEqual("GetDistinct should retain the second POD value", PodResultObject->TestPodArray[1].Index, 2);
|
|
TestEqual("GetDistinct should retain the third POD value", PodResultObject->TestPodArray[2].Index, 3);
|
|
}
|
|
FDirectiveUtilPodValue MostCommonPod = MakePodValue(INDEX_NONE, 0.0f);
|
|
int32 MostCommonPodCount = 0;
|
|
TestTrue(
|
|
"GetMostCommon should group unhashable POD structs",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_GetMostCommon(
|
|
&TestObject->TestPodArray,
|
|
PodArrayProperty,
|
|
&MostCommonPod,
|
|
&MostCommonPodCount));
|
|
TestEqual("GetMostCommon should return the repeated POD value", MostCommonPod.Index, 1);
|
|
TestEqual("GetMostCommon should report the POD value count", MostCommonPodCount, 3);
|
|
|
|
TestObject->TestStringArray = {TEXT("Zero"), TEXT("Selected"), TEXT("Never")};
|
|
TArray<FString> WeightedStrings;
|
|
FRandomStream WeightedStringStream(55);
|
|
TestTrue(
|
|
"Weighted sampling should preserve non-trivial array values",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestStringArray,
|
|
StringArrayProperty,
|
|
TArray<float>({0.0f, 1.0f, 0.0f}),
|
|
3,
|
|
true,
|
|
&WeightedStringStream,
|
|
&WeightedStrings,
|
|
StringArrayProperty));
|
|
TestEqual(
|
|
"Weighted sampling should copy the selected non-trivial value",
|
|
WeightedStrings,
|
|
TArray<FString>({TEXT("Selected"), TEXT("Selected"), TEXT("Selected")}));
|
|
|
|
TestObject->TestStringArray = {TEXT("One"), TEXT("Two"), TEXT("Three"), TEXT("Four")};
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(&TestObject->TestStringArray, StringArrayProperty, 2);
|
|
TestEqual(
|
|
"Rotate should preserve non-trivial values",
|
|
TestObject->TestStringArray,
|
|
TArray<FString>({TEXT("Three"), TEXT("Four"), TEXT("One"), TEXT("Two")}));
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(&TestObject->TestStringArray, StringArrayProperty, -2);
|
|
TestEqual(
|
|
"Rotate should support negative shifts for non-trivial values",
|
|
TestObject->TestStringArray,
|
|
TArray<FString>({TEXT("One"), TEXT("Two"), TEXT("Three"), TEXT("Four")}));
|
|
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(
|
|
&TestObject->TestStringArray,
|
|
StringArrayProperty,
|
|
1,
|
|
2,
|
|
&TestObject->TestStringArray,
|
|
StringArrayProperty);
|
|
TestEqual("Aliased Slice should preserve string values", TestObject->TestStringArray, TArray<FString>({TEXT("Two"), TEXT("Three")}));
|
|
|
|
TestObject->TestStringArray = {TEXT("Remove"), TEXT("Keep A"), TEXT("Remove"), TEXT("Keep B")};
|
|
const FString StringToRemove = TEXT("Remove");
|
|
TestTrue(
|
|
"RemoveAllOccurrences should support strings",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences(
|
|
&TestObject->TestStringArray,
|
|
StringArrayProperty,
|
|
&StringToRemove));
|
|
TestEqual(
|
|
"RemoveAllOccurrences should preserve string survivor order",
|
|
TestObject->TestStringArray,
|
|
TArray<FString>({TEXT("Keep A"), TEXT("Keep B")}));
|
|
|
|
UObject* FirstObject = NewObject<UDirectiveUtilTestObject>(TestObject);
|
|
UObject* SecondObject = NewObject<UDirectiveUtilTestObject>(TestObject);
|
|
UObject* ThirdObject = NewObject<UDirectiveUtilTestObject>(TestObject);
|
|
const TArray<TObjectPtr<UObject>> ObjectAppendSource = {SecondObject, ThirdObject};
|
|
TestObject->TestObjectArray = {FirstObject};
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized(
|
|
&TestObject->TestObjectArray,
|
|
ObjectArrayProperty,
|
|
&ObjectAppendSource,
|
|
ObjectArrayProperty);
|
|
TestEqual("AppendOptimized should preserve object references", TestObject->TestObjectArray.Num(), 3);
|
|
if (TestObject->TestObjectArray.Num() == 3)
|
|
{
|
|
TestEqual("AppendOptimized should retain the target object", TestObject->TestObjectArray[0].Get(), FirstObject);
|
|
TestEqual("AppendOptimized should append the first source object", TestObject->TestObjectArray[1].Get(), SecondObject);
|
|
TestEqual("AppendOptimized should append the second source object", TestObject->TestObjectArray[2].Get(), ThirdObject);
|
|
}
|
|
|
|
TestObject->TestObjectArray = {FirstObject, SecondObject, FirstObject};
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestObjectArray, ObjectArrayProperty);
|
|
TestEqual("RemoveDuplicates should preserve object references", TestObject->TestObjectArray.Num(), 2);
|
|
if (TestObject->TestObjectArray.Num() == 2)
|
|
{
|
|
TestEqual("RemoveDuplicates should retain the first object", TestObject->TestObjectArray[0].Get(), FirstObject);
|
|
TestEqual("RemoveDuplicates should retain the second object", TestObject->TestObjectArray[1].Get(), SecondObject);
|
|
}
|
|
|
|
TestObject->TestObjectArray = {FirstObject, SecondObject, ThirdObject};
|
|
TestTrue("Aliased object GetPage should succeed", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetPage(
|
|
&TestObject->TestObjectArray,
|
|
ObjectArrayProperty,
|
|
1,
|
|
2,
|
|
&TestObject->TestObjectArray,
|
|
ObjectArrayProperty,
|
|
&PageCount));
|
|
TestEqual("Aliased object GetPage should preserve the selected reference", TestObject->TestObjectArray.Num(), 1);
|
|
if (TestObject->TestObjectArray.Num() == 1)
|
|
{
|
|
TestEqual("Aliased object GetPage should return the final object", TestObject->TestObjectArray[0].Get(), ThirdObject);
|
|
}
|
|
|
|
TestObject->TestObjectArray = {FirstObject, nullptr, SecondObject, FirstObject, nullptr};
|
|
TestTrue(
|
|
"RemoveAllOccurrences should support object references",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences(
|
|
&TestObject->TestObjectArray,
|
|
ObjectArrayProperty,
|
|
&FirstObject));
|
|
TestEqual("RemoveAllOccurrences should retain object and null survivors", TestObject->TestObjectArray.Num(), 3);
|
|
if (TestObject->TestObjectArray.Num() == 3)
|
|
{
|
|
TestNull("RemoveAllOccurrences should retain the first null", TestObject->TestObjectArray[0].Get());
|
|
TestEqual("RemoveAllOccurrences should retain the unmatched object", TestObject->TestObjectArray[1].Get(), SecondObject);
|
|
TestNull("RemoveAllOccurrences should retain the second null", TestObject->TestObjectArray[2].Get());
|
|
}
|
|
UObject* NullObject = nullptr;
|
|
TestTrue(
|
|
"RemoveAllOccurrences should remove null object references",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences(
|
|
&TestObject->TestObjectArray,
|
|
ObjectArrayProperty,
|
|
&NullObject));
|
|
TestEqual("RemoveAllOccurrences should leave the non-null object", TestObject->TestObjectArray.Num(), 1);
|
|
|
|
TestObject->TestObjectArray = {FirstObject, nullptr, SecondObject, ThirdObject};
|
|
TestEqual(
|
|
"RemoveAtIndices should preserve object references",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtIndices(
|
|
&TestObject->TestObjectArray,
|
|
ObjectArrayProperty,
|
|
TArray<int32>({1, 3})),
|
|
2);
|
|
TestEqual("RemoveAtIndices should retain two object references", TestObject->TestObjectArray.Num(), 2);
|
|
if (TestObject->TestObjectArray.Num() == 2)
|
|
{
|
|
TestEqual("RemoveAtIndices should retain the first object", TestObject->TestObjectArray[0].Get(), FirstObject);
|
|
TestEqual("RemoveAtIndices should retain the second object", TestObject->TestObjectArray[1].Get(), SecondObject);
|
|
}
|
|
|
|
TestObject->TestStringArray = {TEXT("unchanged")};
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized(
|
|
&TestObject->TestStringArray,
|
|
StringArrayProperty,
|
|
&TestObject->TestArray,
|
|
ArrayProperty);
|
|
TestEqual(
|
|
"AppendOptimized should reject mismatched array types without changing the target",
|
|
TestObject->TestStringArray,
|
|
TArray<FString>({TEXT("unchanged")}));
|
|
|
|
TestFalse(
|
|
"InsertOptimized should reject mismatched array types",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized(
|
|
&TestObject->TestStringArray,
|
|
StringArrayProperty,
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
0));
|
|
TestEqual(
|
|
"InsertOptimized should not change a target with a mismatched source type",
|
|
TestObject->TestStringArray,
|
|
TArray<FString>({TEXT("unchanged")}));
|
|
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
0,
|
|
1,
|
|
&TestObject->TestStringArray,
|
|
StringArrayProperty);
|
|
TestEqual("Slice should reject mismatched array types without changing output", TestObject->TestStringArray, TArray<FString>({TEXT("unchanged")}));
|
|
TestFalse(
|
|
"Weighted sampling should reject mismatched output types",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
TArray<float>({1.0f, 1.0f, 1.0f}),
|
|
1,
|
|
false,
|
|
nullptr,
|
|
&TestObject->TestStringArray,
|
|
StringArrayProperty));
|
|
TestEqual("Weighted sampling should leave a mismatched output unchanged", TestObject->TestStringArray, TArray<FString>({TEXT("unchanged")}));
|
|
|
|
const FText AlphaText = FText::FromString(TEXT("Alpha"));
|
|
const FText BetaText = FText::FromString(TEXT("Beta"));
|
|
TestObject->TestTextArray = {AlphaText, BetaText, AlphaText};
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestTextArray, TextArrayProperty);
|
|
TestEqual("RemoveDuplicates should support unhashable property types", TestObject->TestTextArray.Num(), 2);
|
|
if (TestObject->TestTextArray.Num() == 2)
|
|
{
|
|
TestTrue("RemoveDuplicates should retain the first unhashable value", TestObject->TestTextArray[0].IdenticalTo(AlphaText));
|
|
TestTrue("RemoveDuplicates should retain the second unhashable value", TestObject->TestTextArray[1].IdenticalTo(BetaText));
|
|
}
|
|
|
|
TestObject->TestTextArray = {AlphaText, BetaText, AlphaText, BetaText};
|
|
TestTrue(
|
|
"RemoveAllOccurrences should support text values",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences(
|
|
&TestObject->TestTextArray,
|
|
TextArrayProperty,
|
|
&AlphaText));
|
|
TestEqual("RemoveAllOccurrences should remove matching text values", TestObject->TestTextArray.Num(), 2);
|
|
if (TestObject->TestTextArray.Num() == 2)
|
|
{
|
|
TestTrue("RemoveAllOccurrences should retain the first text survivor", TestObject->TestTextArray[0].IdenticalTo(BetaText));
|
|
TestTrue("RemoveAllOccurrences should retain the second text survivor", TestObject->TestTextArray[1].IdenticalTo(BetaText));
|
|
}
|
|
|
|
TestObject->TestBoolArray = {true, false, true, false, true};
|
|
const bool bBoolToRemove = true;
|
|
TestTrue(
|
|
"RemoveAllOccurrences should support Boolean values",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences(
|
|
&TestObject->TestBoolArray,
|
|
BoolArrayProperty,
|
|
&bBoolToRemove));
|
|
TestEqual("RemoveAllOccurrences should remove matching Boolean values", TestObject->TestBoolArray, TArray<bool>({false, false}));
|
|
|
|
auto AddCollisionValue = [&TestObject](const int32 Value)
|
|
{
|
|
FDirectiveUtilCollisionValue& Entry = TestObject->TestCollisionArray.AddDefaulted_GetRef();
|
|
Entry.Value = Value;
|
|
};
|
|
TestObject->TestCollisionArray.Reset();
|
|
for (const int32 Value : {1, 2, 1, 3, 2})
|
|
{
|
|
AddCollisionValue(Value);
|
|
}
|
|
TestTrue("Collision test property should expose a value hash", CollisionArrayProperty->Inner->HasAllPropertyFlags(CPF_HasGetValueTypeHash));
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestCollisionArray, CollisionArrayProperty);
|
|
TestEqual("RemoveDuplicates should resolve hash collisions", TestObject->TestCollisionArray.Num(), 3);
|
|
if (TestObject->TestCollisionArray.Num() == 3)
|
|
{
|
|
TestEqual("Hash collision result should retain the first value", TestObject->TestCollisionArray[0].Value, 1);
|
|
TestEqual("Hash collision result should retain the second value", TestObject->TestCollisionArray[1].Value, 2);
|
|
TestEqual("Hash collision result should retain the third value", TestObject->TestCollisionArray[2].Value, 3);
|
|
}
|
|
|
|
TestObject->TestCollisionArray.Reset();
|
|
for (const int32 Value : {1, 2, 1, 3, 1, 4})
|
|
{
|
|
AddCollisionValue(Value);
|
|
}
|
|
FDirectiveUtilCollisionValue CollisionToRemove;
|
|
CollisionToRemove.Value = 1;
|
|
TestTrue(
|
|
"RemoveAllOccurrences should support struct equality",
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences(
|
|
&TestObject->TestCollisionArray,
|
|
CollisionArrayProperty,
|
|
&CollisionToRemove));
|
|
TestEqual("RemoveAllOccurrences should keep struct survivors", TestObject->TestCollisionArray.Num(), 3);
|
|
if (TestObject->TestCollisionArray.Num() == 3)
|
|
{
|
|
TestEqual("RemoveAllOccurrences should retain the first struct survivor", TestObject->TestCollisionArray[0].Value, 2);
|
|
TestEqual("RemoveAllOccurrences should retain the second struct survivor", TestObject->TestCollisionArray[1].Value, 3);
|
|
TestEqual("RemoveAllOccurrences should retain the third struct survivor", TestObject->TestCollisionArray[2].Value, 4);
|
|
}
|
|
|
|
TestObject->TestCollisionArray.Reset();
|
|
for (const int32 Value : {4, 5, 5, 4})
|
|
{
|
|
AddCollisionValue(Value);
|
|
}
|
|
FDirectiveUtilCollisionValue MostCommonCollision;
|
|
MostCommonCount = 0;
|
|
TestTrue("GetMostCommon should resolve hash collisions", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetMostCommon(
|
|
&TestObject->TestCollisionArray,
|
|
CollisionArrayProperty,
|
|
&MostCommonCollision,
|
|
&MostCommonCount));
|
|
TestEqual("GetMostCommon should use first occurrence to break collision ties", MostCommonCollision.Value, 4);
|
|
TestEqual("GetMostCommon should report the collision value count", MostCommonCount, 2);
|
|
|
|
TestObject->TestArray.SetNumUninitialized(100);
|
|
for (int32 Index = 0; Index < TestObject->TestArray.Num(); ++Index)
|
|
{
|
|
TestObject->TestArray[Index] = Index;
|
|
}
|
|
for (const int32 Count : {10, 75})
|
|
{
|
|
FRandomStream CompatibilityStream(9917);
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
Count,
|
|
false,
|
|
&CompatibilityStream,
|
|
&SampledValues,
|
|
ArrayProperty);
|
|
TestEqual(
|
|
FString::Printf(TEXT("Sampling %d values should match dense Fisher-Yates"), Count),
|
|
SampledValues,
|
|
BuildReferenceSample(TestObject->TestArray.Num(), Count, 9917));
|
|
}
|
|
|
|
TArray<int32> SelectionCounts;
|
|
SelectionCounts.Init(0, 5);
|
|
TestObject->TestArray = {0, 1, 2, 3, 4};
|
|
FRandomStream UniformityStream(77123);
|
|
for (int32 Iteration = 0; Iteration < 5000; ++Iteration)
|
|
{
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
1,
|
|
false,
|
|
&UniformityStream,
|
|
&SampledValues,
|
|
ArrayProperty);
|
|
if (SampledValues.Num() == 1 && SelectionCounts.IsValidIndex(SampledValues[0]))
|
|
{
|
|
++SelectionCounts[SampledValues[0]];
|
|
}
|
|
}
|
|
for (int32 Value = 0; Value < SelectionCounts.Num(); ++Value)
|
|
{
|
|
TestTrue(
|
|
FString::Printf(TEXT("Sampling frequency for value %d should remain within tolerance"), Value),
|
|
FMath::Abs(SelectionCounts[Value] - 1000) <= 150);
|
|
}
|
|
|
|
FRandomStream FuzzStream(18181);
|
|
for (int32 Iteration = 0; Iteration < 100; ++Iteration)
|
|
{
|
|
TArray<int32> SourceValues;
|
|
const int32 ValueCount = FuzzStream.RandRange(0, 128);
|
|
SourceValues.Reserve(ValueCount);
|
|
for (int32 Index = 0; Index < ValueCount; ++Index)
|
|
{
|
|
SourceValues.Add(FuzzStream.RandRange(-12, 12));
|
|
}
|
|
const TArray<int32> ExpectedDistinct = BuildReferenceDistinct(SourceValues);
|
|
TestObject->TestArray = SourceValues;
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestArray, ArrayProperty);
|
|
TestEqual(FString::Printf(TEXT("RemoveDuplicates fuzz case %d"), Iteration), TestObject->TestArray, ExpectedDistinct);
|
|
TestObject->TestArray = SourceValues;
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct(&TestObject->TestArray, ArrayProperty, &DistinctOut, ArrayProperty);
|
|
TestEqual(FString::Printf(TEXT("GetDistinct fuzz case %d"), Iteration), DistinctOut, ExpectedDistinct);
|
|
|
|
TArray<int32> InsertValues;
|
|
const int32 InsertCount = FuzzStream.RandRange(0, 32);
|
|
InsertValues.Reserve(InsertCount);
|
|
for (int32 Index = 0; Index < InsertCount; ++Index)
|
|
{
|
|
InsertValues.Add(FuzzStream.RandRange(-20, 20));
|
|
}
|
|
const int32 InsertIndex = FuzzStream.RandRange(0, SourceValues.Num());
|
|
TArray<int32> ExpectedInsert = SourceValues;
|
|
ExpectedInsert.Insert(InsertValues, InsertIndex);
|
|
TestObject->TestArray = SourceValues;
|
|
TestEqual(
|
|
FString::Printf(TEXT("InsertOptimized fuzz result %d"), Iteration),
|
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_InsertOptimized(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
&InsertValues,
|
|
ArrayProperty,
|
|
InsertIndex),
|
|
!InsertValues.IsEmpty());
|
|
TestEqual(
|
|
FString::Printf(TEXT("InsertOptimized fuzz values %d"), Iteration),
|
|
TestObject->TestArray,
|
|
ExpectedInsert);
|
|
|
|
TArray<int32> RemovalIndices;
|
|
const int32 RemovalCount = FuzzStream.RandRange(0, 48);
|
|
RemovalIndices.Reserve(RemovalCount);
|
|
for (int32 Index = 0; Index < RemovalCount; ++Index)
|
|
{
|
|
RemovalIndices.Add(FuzzStream.RandRange(-8, SourceValues.Num() + 8));
|
|
}
|
|
const TArray<int32> ExpectedRemoval = BuildReferenceRemoval(SourceValues, RemovalIndices);
|
|
TestObject->TestArray = SourceValues;
|
|
const int32 RemovedCount = UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtIndices(
|
|
&TestObject->TestArray,
|
|
ArrayProperty,
|
|
RemovalIndices);
|
|
TestEqual(
|
|
FString::Printf(TEXT("RemoveAtIndices fuzz count %d"), Iteration),
|
|
RemovedCount,
|
|
SourceValues.Num() - ExpectedRemoval.Num());
|
|
TestEqual(
|
|
FString::Printf(TEXT("RemoveAtIndices fuzz values %d"), Iteration),
|
|
TestObject->TestArray,
|
|
ExpectedRemoval);
|
|
}
|
|
|
|
TArray<FString> StableNaturalStrings = {TEXT("ItemA"), TEXT("itema"), TEXT("ItemB"), TEXT("itemb")};
|
|
UDirectiveUtilArrayFunctionLibrary::NaturalSortStringArray(StableNaturalStrings);
|
|
TestEqual(
|
|
"Natural string sort should preserve equivalent-key order",
|
|
StableNaturalStrings,
|
|
TArray<FString>({TEXT("ItemA"), TEXT("itema"), TEXT("ItemB"), TEXT("itemb")}));
|
|
UDirectiveUtilArrayFunctionLibrary::NaturalSortStringArray(StableNaturalStrings, true);
|
|
TestEqual(
|
|
"Descending natural string sort should preserve equivalent-key order",
|
|
StableNaturalStrings,
|
|
TArray<FString>({TEXT("ItemB"), TEXT("itemb"), TEXT("ItemA"), TEXT("itema")}));
|
|
|
|
return true;
|
|
}
|