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

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;
}