Update directive utilities plugin
This commit is contained in:
@@ -23,13 +23,19 @@ public class DirectiveUtilitiesTests : ModuleRules
|
||||
"DirectiveUtilitiesRuntime",
|
||||
"AutomationTest",
|
||||
"EnhancedInput",
|
||||
"GameplayTags"
|
||||
"GameplayTags",
|
||||
"Projects"
|
||||
}
|
||||
);
|
||||
|
||||
if (Target.bBuildEditor)
|
||||
{
|
||||
PrivateIncludePaths.Add(System.IO.Path.Combine(ModuleDirectory, "../DirectiveUtilitiesEditor/Private"));
|
||||
PrivateDependencyModuleNames.Add("AssetRegistry");
|
||||
PrivateDependencyModuleNames.Add("BlueprintGraph");
|
||||
PrivateDependencyModuleNames.Add("DirectiveUtilitiesBlueprintNodes");
|
||||
PrivateDependencyModuleNames.Add("DirectiveUtilitiesEditor");
|
||||
PrivateDependencyModuleNames.Add("UnrealEd");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilArrayFunctionLibrary.h"
|
||||
#include "Tests/DirectiveUtilTestObject.h"
|
||||
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphPin.h"
|
||||
#include "EdGraphSchema_K2.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "K2Node_CallArrayFunction.h"
|
||||
#include "K2Node_Event.h"
|
||||
#include "K2Node_VariableGet.h"
|
||||
#include "K2Node_VariableSet.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "Kismet2/KismetEditorUtilities.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "UObject/Package.h"
|
||||
|
||||
namespace DirectiveUtilArrayBlueprintVmTest
|
||||
{
|
||||
template <typename NodeType>
|
||||
NodeType* AddNode(UEdGraph& Graph)
|
||||
{
|
||||
NodeType* Node = NewObject<NodeType>(&Graph);
|
||||
Graph.AddNode(Node);
|
||||
Node->CreateNewGuid();
|
||||
Node->PostPlacedNewNode();
|
||||
return Node;
|
||||
}
|
||||
|
||||
UK2Node_VariableGet* AddVariableGet(UEdGraph& Graph, const FName PropertyName)
|
||||
{
|
||||
UK2Node_VariableGet* Node = AddNode<UK2Node_VariableGet>(Graph);
|
||||
Node->VariableReference.SetSelfMember(PropertyName);
|
||||
Node->AllocateDefaultPins();
|
||||
return Node;
|
||||
}
|
||||
|
||||
UK2Node_VariableSet* AddVariableSet(UEdGraph& Graph, const FName PropertyName)
|
||||
{
|
||||
UK2Node_VariableSet* Node = AddNode<UK2Node_VariableSet>(Graph);
|
||||
Node->VariableReference.SetSelfMember(PropertyName);
|
||||
Node->AllocateDefaultPins();
|
||||
return Node;
|
||||
}
|
||||
|
||||
UK2Node_CallArrayFunction* AddArrayCall(UEdGraph& Graph, const FName FunctionName)
|
||||
{
|
||||
UK2Node_CallArrayFunction* Node = AddNode<UK2Node_CallArrayFunction>(Graph);
|
||||
Node->FunctionReference.SetExternalMember(FunctionName, UDirectiveUtilArrayFunctionLibrary::StaticClass());
|
||||
Node->AllocateDefaultPins();
|
||||
return Node;
|
||||
}
|
||||
|
||||
bool Connect(UEdGraph& Graph, UEdGraphNode& FromNode, const FName FromPinName,
|
||||
UEdGraphNode& ToNode, const FName ToPinName)
|
||||
{
|
||||
UEdGraphPin* FromPin = FromNode.FindPin(FromPinName);
|
||||
UEdGraphPin* ToPin = ToNode.FindPin(ToPinName);
|
||||
const UEdGraphSchema_K2* Schema = CastChecked<UEdGraphSchema_K2>(Graph.GetSchema());
|
||||
return FromPin && ToPin && Schema->TryCreateConnection(FromPin, ToPin);
|
||||
}
|
||||
|
||||
bool ConnectVariable(UEdGraph& Graph, const FName PropertyName,
|
||||
UEdGraphNode& ToNode, const FName ToPinName)
|
||||
{
|
||||
UK2Node_VariableGet* VariableGet = AddVariableGet(Graph, PropertyName);
|
||||
return Connect(Graph, *VariableGet, PropertyName, ToNode, ToPinName);
|
||||
}
|
||||
|
||||
UBlueprint* BuildScenarioBlueprint(FAutomationTestBase& Test, const FName TargetProperty,
|
||||
const FName SourceProperty, const FName ItemProperty)
|
||||
{
|
||||
const FName BlueprintName(*FString::Printf(
|
||||
TEXT("BP_ArrayThunkVm_%s_%s"),
|
||||
*TargetProperty.ToString(),
|
||||
*FGuid::NewGuid().ToString(EGuidFormats::Digits)));
|
||||
UPackage* Package = CreatePackage(
|
||||
*FString::Printf(TEXT("/Temp/DirectiveUtilitiesTests/%s"), *BlueprintName.ToString()));
|
||||
Package->SetFlags(RF_Transient);
|
||||
UBlueprint* Blueprint = FKismetEditorUtilities::CreateBlueprint(
|
||||
UDirectiveUtilTestObject::StaticClass(),
|
||||
Package,
|
||||
BlueprintName,
|
||||
BPTYPE_Normal,
|
||||
TEXT("DirectiveUtilities.ArrayBlueprintVmTests"));
|
||||
if (!Test.TestNotNull(TEXT("The array VM test Blueprint should be created"), Blueprint))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UEdGraph* Graph = FBlueprintEditorUtils::FindEventGraph(Blueprint);
|
||||
if (!Test.TestNotNull(TEXT("The array VM test Blueprint should have an event graph"), Graph))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UK2Node_Event* Event = AddNode<UK2Node_Event>(*Graph);
|
||||
Event->EventReference.SetExternalMember(
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilTestObject, RunArrayThunkScenario),
|
||||
UDirectiveUtilTestObject::StaticClass());
|
||||
Event->bOverrideFunction = true;
|
||||
Event->AllocateDefaultPins();
|
||||
|
||||
UK2Node_CallArrayFunction* Append = AddArrayCall(
|
||||
*Graph,
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_AppendOptimized));
|
||||
UK2Node_CallArrayFunction* Insert = AddArrayCall(
|
||||
*Graph,
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_InsertOptimized));
|
||||
UK2Node_VariableSet* SetInsertResult = AddVariableSet(*Graph, TEXT("TestInsertResult"));
|
||||
UK2Node_CallArrayFunction* RemoveIndices = AddArrayCall(
|
||||
*Graph,
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveAtIndices));
|
||||
UK2Node_VariableSet* SetRemovedCount = AddVariableSet(*Graph, TEXT("TestRemovedCount"));
|
||||
UK2Node_CallArrayFunction* RemoveAll = AddArrayCall(
|
||||
*Graph,
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveAllOccurrences));
|
||||
UK2Node_VariableSet* SetRemoveAllResult = AddVariableSet(*Graph, TEXT("TestRemoveAllResult"));
|
||||
|
||||
bool bConnected = true;
|
||||
bConnected &= Connect(*Graph, *Event, UEdGraphSchema_K2::PN_Then, *Append, UEdGraphSchema_K2::PN_Execute);
|
||||
bConnected &= Connect(*Graph, *Append, UEdGraphSchema_K2::PN_Then, *Insert, UEdGraphSchema_K2::PN_Execute);
|
||||
bConnected &= Connect(*Graph, *Insert, UEdGraphSchema_K2::PN_Then, *SetInsertResult, UEdGraphSchema_K2::PN_Execute);
|
||||
bConnected &= Connect(*Graph, *Insert, UEdGraphSchema_K2::PN_ReturnValue, *SetInsertResult, TEXT("TestInsertResult"));
|
||||
bConnected &= Connect(*Graph, *SetInsertResult, UEdGraphSchema_K2::PN_Then, *RemoveIndices, UEdGraphSchema_K2::PN_Execute);
|
||||
bConnected &= Connect(*Graph, *RemoveIndices, UEdGraphSchema_K2::PN_Then, *SetRemovedCount, UEdGraphSchema_K2::PN_Execute);
|
||||
bConnected &= Connect(*Graph, *RemoveIndices, UEdGraphSchema_K2::PN_ReturnValue, *SetRemovedCount, TEXT("TestRemovedCount"));
|
||||
bConnected &= Connect(*Graph, *SetRemovedCount, UEdGraphSchema_K2::PN_Then, *RemoveAll, UEdGraphSchema_K2::PN_Execute);
|
||||
bConnected &= Connect(*Graph, *RemoveAll, UEdGraphSchema_K2::PN_Then, *SetRemoveAllResult, UEdGraphSchema_K2::PN_Execute);
|
||||
bConnected &= Connect(*Graph, *RemoveAll, UEdGraphSchema_K2::PN_ReturnValue, *SetRemoveAllResult, TEXT("TestRemoveAllResult"));
|
||||
|
||||
for (UK2Node_CallArrayFunction* Call : {Append, Insert, RemoveIndices, RemoveAll})
|
||||
{
|
||||
bConnected &= ConnectVariable(*Graph, TargetProperty, *Call, TEXT("TargetArray"));
|
||||
}
|
||||
bConnected &= ConnectVariable(*Graph, SourceProperty, *Append, TEXT("SourceArray"));
|
||||
bConnected &= ConnectVariable(*Graph, SourceProperty, *Insert, TEXT("SourceArray"));
|
||||
bConnected &= ConnectVariable(*Graph, TEXT("TestIndices"), *RemoveIndices, TEXT("Indices"));
|
||||
bConnected &= ConnectVariable(*Graph, ItemProperty, *RemoveAll, TEXT("Item"));
|
||||
Insert->FindPinChecked(TEXT("Index"))->DefaultValue = TEXT("1");
|
||||
|
||||
if (!Test.TestTrue(TEXT("The array VM test graph should connect every pin"), bConnected))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);
|
||||
FKismetEditorUtilities::CompileBlueprint(Blueprint, EBlueprintCompileOptions::SkipGarbageCollection);
|
||||
if (!Test.TestTrue(
|
||||
TEXT("The array VM test Blueprint should compile"),
|
||||
Blueprint->Status == BS_UpToDate || Blueprint->Status == BS_UpToDateWithWarnings))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return Blueprint;
|
||||
}
|
||||
|
||||
UDirectiveUtilTestObject* CreateScenarioInstance(FAutomationTestBase& Test, UBlueprint& Blueprint)
|
||||
{
|
||||
UDirectiveUtilTestObject* Instance = NewObject<UDirectiveUtilTestObject>(
|
||||
GetTransientPackage(), Blueprint.GeneratedClass);
|
||||
if (!Test.TestNotNull(TEXT("The compiled array VM test instance should be created"), Instance))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
Instance->TestIndices = {0, 4};
|
||||
return Instance;
|
||||
}
|
||||
|
||||
bool RunScenario(
|
||||
FAutomationTestBase& Test,
|
||||
UDirectiveUtilTestObject& Instance,
|
||||
const bool bExpectedInsertResult = true,
|
||||
const int32 ExpectedRemovedCount = 2,
|
||||
const bool bExpectedRemoveAllResult = true)
|
||||
{
|
||||
UFunction* Function = Instance.FindFunction(
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilTestObject, RunArrayThunkScenario));
|
||||
if (!Test.TestNotNull(TEXT("The compiled array VM event should exist"), Function))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Instance.ProcessEvent(Function, nullptr);
|
||||
Test.TestEqual(
|
||||
TEXT("Insert Array Optimized should marshal its Blueprint result"),
|
||||
Instance.TestInsertResult,
|
||||
bExpectedInsertResult);
|
||||
Test.TestEqual(
|
||||
TEXT("Remove At Indices should marshal its Blueprint result"),
|
||||
Instance.TestRemovedCount,
|
||||
ExpectedRemovedCount);
|
||||
Test.TestEqual(
|
||||
TEXT("Remove All Occurrences should marshal its Blueprint result"),
|
||||
Instance.TestRemoveAllResult,
|
||||
bExpectedRemoveAllResult);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilArrayBlueprintVmTest,
|
||||
"DirectiveUtilities.ArrayBlueprintVmTests",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilArrayBlueprintVmTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
using namespace DirectiveUtilArrayBlueprintVmTest;
|
||||
|
||||
if (UBlueprint* Blueprint = BuildScenarioBlueprint(
|
||||
*this, TEXT("TestBoolArray"), TEXT("TestBoolSourceArray"), TEXT("TestBoolItem")))
|
||||
{
|
||||
if (UDirectiveUtilTestObject* Instance = CreateScenarioInstance(*this, *Blueprint))
|
||||
{
|
||||
Instance->TestBoolArray = {true, false, true};
|
||||
Instance->TestBoolSourceArray = {false, true};
|
||||
Instance->TestBoolItem = false;
|
||||
if (RunScenario(*this, *Instance))
|
||||
{
|
||||
TestEqual(TEXT("Boolean wildcard values should survive Blueprint VM execution"),
|
||||
Instance->TestBoolArray, TArray<bool>({true, true}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (UBlueprint* Blueprint = BuildScenarioBlueprint(
|
||||
*this, TEXT("TestBoolArray"), TEXT("TestBoolArray"), TEXT("TestBoolItem")))
|
||||
{
|
||||
if (UDirectiveUtilTestObject* Instance = CreateScenarioInstance(*this, *Blueprint))
|
||||
{
|
||||
Instance->TestBoolArray = {true, false};
|
||||
Instance->TestBoolItem = true;
|
||||
Instance->TestIndices = {-1, 99};
|
||||
if (RunScenario(*this, *Instance, true, 0, true))
|
||||
{
|
||||
TestEqual(
|
||||
TEXT("Self-aliasing wildcard arrays should survive Blueprint VM execution"),
|
||||
Instance->TestBoolArray,
|
||||
TArray<bool>({false, false, false, false}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (UBlueprint* Blueprint = BuildScenarioBlueprint(
|
||||
*this, TEXT("TestBoolArray"), TEXT("TestBoolSourceArray"), TEXT("TestBoolItem")))
|
||||
{
|
||||
if (UDirectiveUtilTestObject* Instance = CreateScenarioInstance(*this, *Blueprint))
|
||||
{
|
||||
Instance->TestBoolArray = {true};
|
||||
Instance->TestBoolSourceArray.Reset();
|
||||
Instance->TestBoolItem = false;
|
||||
Instance->TestIndices = {-1, 99};
|
||||
if (RunScenario(*this, *Instance, false, 0, false))
|
||||
{
|
||||
TestEqual(
|
||||
TEXT("No-op wildcard calls should preserve the target through Blueprint VM execution"),
|
||||
Instance->TestBoolArray,
|
||||
TArray<bool>({true}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (UBlueprint* Blueprint = BuildScenarioBlueprint(
|
||||
*this, TEXT("TestStringArray"), TEXT("TestStringSourceArray"), TEXT("TestStringItem")))
|
||||
{
|
||||
if (UDirectiveUtilTestObject* Instance = CreateScenarioInstance(*this, *Blueprint))
|
||||
{
|
||||
Instance->TestStringArray = {TEXT("A"), TEXT("B"), TEXT("A")};
|
||||
Instance->TestStringSourceArray = {TEXT("C"), TEXT("A")};
|
||||
Instance->TestStringItem = TEXT("A");
|
||||
if (RunScenario(*this, *Instance))
|
||||
{
|
||||
TestEqual(TEXT("String wildcard values should survive Blueprint VM execution"),
|
||||
Instance->TestStringArray, TArray<FString>({TEXT("C"), TEXT("B"), TEXT("C")}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (UBlueprint* Blueprint = BuildScenarioBlueprint(
|
||||
*this, TEXT("TestCollisionArray"), TEXT("TestCollisionSourceArray"), TEXT("TestCollisionItem")))
|
||||
{
|
||||
if (UDirectiveUtilTestObject* Instance = CreateScenarioInstance(*this, *Blueprint))
|
||||
{
|
||||
Instance->TestCollisionArray = {{1}, {2}, {1}};
|
||||
Instance->TestCollisionSourceArray = {{3}, {1}};
|
||||
Instance->TestCollisionItem = {1};
|
||||
if (RunScenario(*this, *Instance))
|
||||
{
|
||||
TestEqual(TEXT("Struct wildcard values should survive Blueprint VM execution"),
|
||||
Instance->TestCollisionArray, TArray<FDirectiveUtilCollisionValue>({{3}, {2}, {3}}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (UBlueprint* Blueprint = BuildScenarioBlueprint(
|
||||
*this, TEXT("TestObjectArray"), TEXT("TestObjectSourceArray"), TEXT("TestObjectItem")))
|
||||
{
|
||||
if (UDirectiveUtilTestObject* Instance = CreateScenarioInstance(*this, *Blueprint))
|
||||
{
|
||||
UObject* A = NewObject<UDirectiveUtilTestObject>(Instance);
|
||||
UObject* B = NewObject<UDirectiveUtilTestObject>(Instance);
|
||||
UObject* C = NewObject<UDirectiveUtilTestObject>(Instance);
|
||||
Instance->TestObjectArray = {A, B, A};
|
||||
Instance->TestObjectSourceArray = {C, A};
|
||||
Instance->TestObjectItem = A;
|
||||
if (RunScenario(*this, *Instance))
|
||||
{
|
||||
TestEqual(TEXT("Object wildcard arrays should retain three values"), Instance->TestObjectArray.Num(), 3);
|
||||
if (Instance->TestObjectArray.Num() == 3)
|
||||
{
|
||||
TestEqual(TEXT("Object wildcard arrays should retain the first source object"), Instance->TestObjectArray[0].Get(), C);
|
||||
TestEqual(TEXT("Object wildcard arrays should retain the target object"), Instance->TestObjectArray[1].Get(), B);
|
||||
TestEqual(TEXT("Object wildcard arrays should retain the second source object"), Instance->TestObjectArray[2].Get(), C);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,207 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphNode.h"
|
||||
#include "EdGraph/EdGraphPin.h"
|
||||
#include "EdGraphSchema_K2.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "K2Node_CallArrayFunction.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "Kismet2/CompilerResultsLog.h"
|
||||
#include "Libraries/DirectiveUtilArrayFunctionLibrary.h"
|
||||
#include "Tests/DirectiveUtilTestObject.h"
|
||||
|
||||
namespace DirectiveUtilArrayNodeTest
|
||||
{
|
||||
struct FDependentPin
|
||||
{
|
||||
FName Name;
|
||||
EPinContainerType ContainerType;
|
||||
};
|
||||
|
||||
struct FArrayNodeCase
|
||||
{
|
||||
FName FunctionName;
|
||||
TArray<FDependentPin> DependentPins;
|
||||
};
|
||||
|
||||
FEdGraphPinType MakeArrayType(
|
||||
const FName Category,
|
||||
const FName SubCategory = NAME_None,
|
||||
UObject* SubCategoryObject = nullptr)
|
||||
{
|
||||
FEdGraphPinType PinType;
|
||||
PinType.PinCategory = Category;
|
||||
PinType.PinSubCategory = SubCategory;
|
||||
PinType.PinSubCategoryObject = SubCategoryObject;
|
||||
PinType.ContainerType = EPinContainerType::Array;
|
||||
return PinType;
|
||||
}
|
||||
|
||||
UEdGraph* MakeGraph()
|
||||
{
|
||||
UBlueprint* Blueprint = NewObject<UBlueprint>();
|
||||
Blueprint->GeneratedClass = UObject::StaticClass();
|
||||
Blueprint->SkeletonGeneratedClass = UObject::StaticClass();
|
||||
UEdGraph* Graph = NewObject<UEdGraph>(Blueprint);
|
||||
Graph->Schema = UEdGraphSchema_K2::StaticClass();
|
||||
Blueprint->UbergraphPages.Add(Graph);
|
||||
return Graph;
|
||||
}
|
||||
|
||||
UEdGraphPin* AddArrayOutput(UEdGraph& Graph, const FName Name, const FEdGraphPinType& PinType)
|
||||
{
|
||||
UEdGraphNode* SourceNode = NewObject<UEdGraphNode>(&Graph);
|
||||
Graph.AddNode(SourceNode);
|
||||
return SourceNode->CreatePin(EGPD_Output, PinType, Name);
|
||||
}
|
||||
|
||||
UK2Node_CallArrayFunction* AddFunctionNode(UEdGraph& Graph, const FName FunctionName)
|
||||
{
|
||||
UK2Node_CallArrayFunction* Node = NewObject<UK2Node_CallArrayFunction>(&Graph);
|
||||
Graph.AddNode(Node);
|
||||
const UFunction* Function = UDirectiveUtilArrayFunctionLibrary::StaticClass()->FindFunctionByName(FunctionName);
|
||||
Node->FunctionReference.SetExternalMember(FunctionName, UDirectiveUtilArrayFunctionLibrary::StaticClass());
|
||||
Node->bDefaultsToPureFunc = Function && Function->HasAnyFunctionFlags(FUNC_BlueprintPure);
|
||||
Node->AllocateDefaultPins();
|
||||
return Node;
|
||||
}
|
||||
|
||||
void Connect(UEdGraphPin& OutputPin, UK2Node& Node, UEdGraphPin& InputPin)
|
||||
{
|
||||
OutputPin.MakeLinkTo(&InputPin);
|
||||
Node.PinConnectionListChanged(&InputPin);
|
||||
}
|
||||
|
||||
void Disconnect(UEdGraphPin& OutputPin, UK2Node& Node, UEdGraphPin& InputPin)
|
||||
{
|
||||
OutputPin.BreakLinkTo(&InputPin);
|
||||
Node.PinConnectionListChanged(&InputPin);
|
||||
}
|
||||
|
||||
bool HasElementType(
|
||||
const UEdGraphPin& Pin,
|
||||
const FEdGraphPinType& ArrayType,
|
||||
const EPinContainerType ContainerType)
|
||||
{
|
||||
return Pin.PinType.ContainerType == ContainerType
|
||||
&& Pin.PinType.PinCategory == ArrayType.PinCategory
|
||||
&& Pin.PinType.PinSubCategory == ArrayType.PinSubCategory
|
||||
&& Pin.PinType.PinSubCategoryObject == ArrayType.PinSubCategoryObject;
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilArrayNodeWildcardTest,
|
||||
"DirectiveUtilities.ArrayNodeWildcardTests",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilArrayNodeWildcardTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
using namespace DirectiveUtilArrayNodeTest;
|
||||
|
||||
const TArray<FArrayNodeCase> NodeCases = {
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_NextIndex), {}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_PreviousIndex), {}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveDuplicates), {}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_AppendOptimized), {{TEXT("SourceArray"), EPinContainerType::Array}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_InsertOptimized), {{TEXT("SourceArray"), EPinContainerType::Array}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_GetValidFirstItemCopy), {{TEXT("OutItem"), EPinContainerType::None}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_GetValidLastItemCopy), {{TEXT("OutItem"), EPinContainerType::None}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_GetValidItemFromIndexCopy), {{TEXT("OutItem"), EPinContainerType::None}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_GetRandomItem), {{TEXT("OutItem"), EPinContainerType::None}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_LastValue), {{TEXT("OutItem"), EPinContainerType::None}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_Pop), {{TEXT("OutItem"), EPinContainerType::None}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_PopFirst), {{TEXT("OutItem"), EPinContainerType::None}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveAtSwap), {}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveAtIndices), {}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveAllOccurrences), {{TEXT("Item"), EPinContainerType::None}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_Slice), {{TEXT("OutArray"), EPinContainerType::Array}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_Rotate), {}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_GetDistinct), {{TEXT("OutArray"), EPinContainerType::Array}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_CountOccurrences), {{TEXT("ItemToCount"), EPinContainerType::None}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_GetMostCommon), {{TEXT("OutItem"), EPinContainerType::None}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_Sample), {{TEXT("OutArray"), EPinContainerType::Array}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_SampleFromStream), {{TEXT("OutArray"), EPinContainerType::Array}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_SampleWeighted), {{TEXT("OutArray"), EPinContainerType::Array}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_SampleWeightedFromStream), {{TEXT("OutArray"), EPinContainerType::Array}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_GetPage), {{TEXT("OutArray"), EPinContainerType::Array}}}
|
||||
};
|
||||
|
||||
const TArray<FEdGraphPinType> ArrayTypes = {
|
||||
MakeArrayType(UEdGraphSchema_K2::PC_Boolean),
|
||||
MakeArrayType(UEdGraphSchema_K2::PC_String),
|
||||
MakeArrayType(UEdGraphSchema_K2::PC_Object, NAME_None, UDirectiveUtilTestObject::StaticClass()),
|
||||
MakeArrayType(UEdGraphSchema_K2::PC_Struct, NAME_None, FDirectiveUtilCollisionValue::StaticStruct())
|
||||
};
|
||||
|
||||
for (const FArrayNodeCase& NodeCase : NodeCases)
|
||||
{
|
||||
UEdGraph* Graph = MakeGraph();
|
||||
UK2Node_CallArrayFunction* Node = AddFunctionNode(*Graph, NodeCase.FunctionName);
|
||||
UEdGraphPin* TargetArray = Node->FindPin(TEXT("TargetArray"));
|
||||
TestNotNull(*FString::Printf(TEXT("%s should have a TargetArray pin"), *NodeCase.FunctionName.ToString()), TargetArray);
|
||||
if (!TargetArray)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TestTrue(
|
||||
*FString::Printf(TEXT("%s should begin as a wildcard array"), *NodeCase.FunctionName.ToString()),
|
||||
TargetArray->PinType.IsArray() && TargetArray->PinType.PinCategory == UEdGraphSchema_K2::PC_Wildcard);
|
||||
if (NodeCase.FunctionName == GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveAtIndices))
|
||||
{
|
||||
const UEdGraphPin* IndicesPin = Node->FindPin(TEXT("Indices"));
|
||||
TestNotNull(TEXT("RemoveAtIndices should have an Indices pin"), IndicesPin);
|
||||
if (IndicesPin)
|
||||
{
|
||||
TestTrue(
|
||||
TEXT("RemoveAtIndices should keep Indices as an integer array"),
|
||||
IndicesPin->PinType.IsArray()
|
||||
&& IndicesPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Int);
|
||||
}
|
||||
}
|
||||
FCompilerResultsLog ModuleValidationLog;
|
||||
FBlueprintEditorUtils::ValidateEditorOnlyNodes(Node, ModuleValidationLog);
|
||||
TestEqual(
|
||||
*FString::Printf(TEXT("%s should be valid in a runtime Blueprint"), *NodeCase.FunctionName.ToString()),
|
||||
ModuleValidationLog.NumWarnings,
|
||||
0);
|
||||
|
||||
for (int32 TypeIndex = 0; TypeIndex < ArrayTypes.Num(); ++TypeIndex)
|
||||
{
|
||||
const FEdGraphPinType& ArrayType = ArrayTypes[TypeIndex];
|
||||
UEdGraphPin* ArrayOutput = AddArrayOutput(
|
||||
*Graph,
|
||||
*FString::Printf(TEXT("ArrayOutput%d"), TypeIndex),
|
||||
ArrayType);
|
||||
Connect(*ArrayOutput, *Node, *TargetArray);
|
||||
TestTrue(
|
||||
*FString::Printf(TEXT("%s should resolve TargetArray type %d"), *NodeCase.FunctionName.ToString(), TypeIndex),
|
||||
HasElementType(*TargetArray, ArrayType, EPinContainerType::Array));
|
||||
|
||||
for (const FDependentPin& ExpectedPin : NodeCase.DependentPins)
|
||||
{
|
||||
const UEdGraphPin* DependentPin = Node->FindPin(ExpectedPin.Name);
|
||||
TestNotNull(
|
||||
*FString::Printf(TEXT("%s should have a %s pin"), *NodeCase.FunctionName.ToString(), *ExpectedPin.Name.ToString()),
|
||||
DependentPin);
|
||||
if (DependentPin)
|
||||
{
|
||||
TestTrue(
|
||||
*FString::Printf(TEXT("%s should resolve %s type %d"), *NodeCase.FunctionName.ToString(), *ExpectedPin.Name.ToString(), TypeIndex),
|
||||
HasElementType(*DependentPin, ArrayType, ExpectedPin.ContainerType));
|
||||
}
|
||||
}
|
||||
|
||||
Disconnect(*ArrayOutput, *Node, *TargetArray);
|
||||
TestTrue(
|
||||
*FString::Printf(TEXT("%s should reset after type %d"), *NodeCase.FunctionName.ToString(), TypeIndex),
|
||||
TargetArray->PinType.IsArray() && TargetArray->PinType.PinCategory == UEdGraphSchema_K2::PC_Wildcard);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,801 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilArrayFunctionLibrary.h"
|
||||
#include "Tests/DirectiveUtilTestObject.h"
|
||||
|
||||
#include "Algo/Reverse.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
FArrayProperty* GetIntegerArrayProperty()
|
||||
{
|
||||
return FindFProperty<FArrayProperty>(
|
||||
UDirectiveUtilTestObject::StaticClass(),
|
||||
GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestArray));
|
||||
}
|
||||
|
||||
TArray<int32> MakeSequentialValues(const int32 Count)
|
||||
{
|
||||
TArray<int32> Values;
|
||||
Values.SetNumUninitialized(Count);
|
||||
for (int32 Index = 0; Index < Count; ++Index)
|
||||
{
|
||||
Values[Index] = Index;
|
||||
}
|
||||
return Values;
|
||||
}
|
||||
|
||||
TArray<int32> MakeRepeatingValues(const int32 Count, const int32 DistinctCount)
|
||||
{
|
||||
TArray<int32> Values;
|
||||
Values.Reserve(Count);
|
||||
for (int32 Index = 0; Index < Count; ++Index)
|
||||
{
|
||||
Values.Add(Index % DistinctCount);
|
||||
}
|
||||
return Values;
|
||||
}
|
||||
|
||||
TArray<int32> MakeDistinctReference(const TArray<int32>& Values)
|
||||
{
|
||||
TArray<int32> Result;
|
||||
for (const int32 Value : Values)
|
||||
{
|
||||
Result.AddUnique(Value);
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
bool FindMostCommonReference(const TArray<int32>& Values, int32& OutValue, int32& OutCount)
|
||||
{
|
||||
OutValue = 0;
|
||||
OutCount = 0;
|
||||
TMap<int32, int32> Counts;
|
||||
for (const int32 Value : Values)
|
||||
{
|
||||
++Counts.FindOrAdd(Value);
|
||||
}
|
||||
for (const TPair<int32, int32>& Pair : Counts)
|
||||
{
|
||||
OutCount = FMath::Max(OutCount, Pair.Value);
|
||||
}
|
||||
for (const int32 Value : Values)
|
||||
{
|
||||
if (Counts.FindRef(Value) == OutCount)
|
||||
{
|
||||
OutValue = Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return !Values.IsEmpty();
|
||||
}
|
||||
|
||||
int32 CountReference(const TArray<int32>& Values, const int32 QueryValue)
|
||||
{
|
||||
int32 Count = 0;
|
||||
for (const int32 Value : Values)
|
||||
{
|
||||
Count += Value == QueryValue ? 1 : 0;
|
||||
}
|
||||
return Count;
|
||||
}
|
||||
|
||||
TArray<int32> RemoveAllReference(const TArray<int32>& Values, const int32 QueryValue)
|
||||
{
|
||||
TArray<int32> Result;
|
||||
Result.Reserve(Values.Num());
|
||||
for (const int32 Value : Values)
|
||||
{
|
||||
if (Value != QueryValue)
|
||||
{
|
||||
Result.Add(Value);
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
TArray<int32> SliceReference(const TArray<int32>& Values, const int32 StartIndex, const int32 Count)
|
||||
{
|
||||
TArray<int32> Result;
|
||||
Result.Reserve(Count);
|
||||
for (int32 Index = 0; Index < Count; ++Index)
|
||||
{
|
||||
Result.Add(Values[StartIndex + Index]);
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
TArray<int32> MakeSampleReference(const int32 SourceCount, const int32 RequestedCount, const int32 Seed)
|
||||
{
|
||||
TArray<int32> AvailableIndices = MakeSequentialValues(SourceCount);
|
||||
TArray<int32> Result;
|
||||
const int32 SampleCount = FMath::Clamp(RequestedCount, 0, SourceCount);
|
||||
Result.Reserve(SampleCount);
|
||||
FRandomStream RandomStream(Seed);
|
||||
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;
|
||||
}
|
||||
|
||||
template <typename ValueType>
|
||||
TArray<ValueType> MakeRotationReference(const TArray<ValueType>& Values, const int32 Shift)
|
||||
{
|
||||
if (Values.IsEmpty())
|
||||
{
|
||||
return Values;
|
||||
}
|
||||
|
||||
int32 NormalizedShift = Shift % Values.Num();
|
||||
if (NormalizedShift < 0)
|
||||
{
|
||||
NormalizedShift += Values.Num();
|
||||
}
|
||||
|
||||
TArray<ValueType> Result;
|
||||
Result.Reserve(Values.Num());
|
||||
for (int32 Index = 0; Index < Values.Num(); ++Index)
|
||||
{
|
||||
Result.Add(Values[(Index - NormalizedShift + Values.Num()) % Values.Num()]);
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilArrayCardinalityTest,
|
||||
"DirectiveUtilities.ArrayScenarios.Cardinality",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilArrayCardinalityTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
FArrayProperty* ArrayProperty = GetIntegerArrayProperty();
|
||||
if (!TestNotNull("Integer array property should be available", ArrayProperty))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UDirectiveUtilTestObject* TestObject = NewObject<UDirectiveUtilTestObject>();
|
||||
struct FScenario
|
||||
{
|
||||
int32 ItemCount;
|
||||
int32 DistinctCount;
|
||||
};
|
||||
const TArray<FScenario> Scenarios = {
|
||||
{0, 1},
|
||||
{1, 1},
|
||||
{2, 1},
|
||||
{3, 2},
|
||||
{16, 16},
|
||||
{31, 7},
|
||||
{64, 4},
|
||||
{257, 257},
|
||||
{1024, 1},
|
||||
{4096, 257},
|
||||
{16384, 1024}
|
||||
};
|
||||
|
||||
for (const FScenario& Scenario : Scenarios)
|
||||
{
|
||||
const TArray<int32> Source = MakeRepeatingValues(Scenario.ItemCount, Scenario.DistinctCount);
|
||||
const TArray<int32> ExpectedDistinct = MakeDistinctReference(Source);
|
||||
const FString Label = FString::Printf(
|
||||
TEXT("items=%d distinct=%d"),
|
||||
Scenario.ItemCount,
|
||||
FMath::Min(Scenario.ItemCount, Scenario.DistinctCount));
|
||||
|
||||
TestObject->TestArray = Source;
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestArray, ArrayProperty);
|
||||
TestEqual(Label + TEXT(" RemoveDuplicates"), TestObject->TestArray, ExpectedDistinct);
|
||||
|
||||
TArray<int32> DistinctResult;
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct(
|
||||
&Source,
|
||||
ArrayProperty,
|
||||
&DistinctResult,
|
||||
ArrayProperty);
|
||||
TestEqual(Label + TEXT(" GetDistinct"), DistinctResult, ExpectedDistinct);
|
||||
|
||||
int32 MostCommonValue = INDEX_NONE;
|
||||
int32 MostCommonCount = INDEX_NONE;
|
||||
const bool bFoundMostCommon = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetMostCommon(
|
||||
&Source,
|
||||
ArrayProperty,
|
||||
&MostCommonValue,
|
||||
&MostCommonCount);
|
||||
if (Source.IsEmpty())
|
||||
{
|
||||
TestFalse(Label + TEXT(" GetMostCommon should fail"), bFoundMostCommon);
|
||||
TestEqual(Label + TEXT(" GetMostCommon count"), MostCommonCount, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
TestTrue(Label + TEXT(" GetMostCommon should succeed"), bFoundMostCommon);
|
||||
TestEqual(Label + TEXT(" GetMostCommon value"), MostCommonValue, 0);
|
||||
TestEqual(
|
||||
Label + TEXT(" GetMostCommon count"),
|
||||
MostCommonCount,
|
||||
FMath::DivideAndRoundUp(Scenario.ItemCount, Scenario.DistinctCount));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilArraySamplingScenarioTest,
|
||||
"DirectiveUtilities.ArrayScenarios.Sampling",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilArraySamplingScenarioTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
FArrayProperty* ArrayProperty = GetIntegerArrayProperty();
|
||||
if (!TestNotNull("Integer array property should be available", ArrayProperty))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr int32 Seed = 7351;
|
||||
for (const int32 SourceCount : {0, 1, 2, 3, 4, 17, 100, 1000})
|
||||
{
|
||||
const TArray<int32> Source = MakeSequentialValues(SourceCount);
|
||||
const TArray<int32> RequestedCounts = {-1, 0, 1, SourceCount / 4, SourceCount, SourceCount + 5};
|
||||
for (const int32 RequestedCount : RequestedCounts)
|
||||
{
|
||||
FRandomStream FirstStream(Seed);
|
||||
FRandomStream SecondStream(Seed);
|
||||
TArray<int32> FirstSample;
|
||||
TArray<int32> SecondSample;
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(
|
||||
&Source,
|
||||
ArrayProperty,
|
||||
RequestedCount,
|
||||
false,
|
||||
&FirstStream,
|
||||
&FirstSample,
|
||||
ArrayProperty);
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(
|
||||
&Source,
|
||||
ArrayProperty,
|
||||
RequestedCount,
|
||||
false,
|
||||
&SecondStream,
|
||||
&SecondSample,
|
||||
ArrayProperty);
|
||||
|
||||
const FString Label = FString::Printf(TEXT("source=%d requested=%d"), SourceCount, RequestedCount);
|
||||
TestEqual(Label + TEXT(" count"), FirstSample.Num(), FMath::Clamp(RequestedCount, 0, SourceCount));
|
||||
TestEqual(Label + TEXT(" deterministic"), FirstSample, SecondSample);
|
||||
TSet<int32> UniqueValues;
|
||||
for (const int32 Value : FirstSample)
|
||||
{
|
||||
TestTrue(Label + TEXT(" source membership"), Source.Contains(Value));
|
||||
UniqueValues.Add(Value);
|
||||
}
|
||||
TestEqual(Label + TEXT(" uniqueness"), UniqueValues.Num(), FirstSample.Num());
|
||||
}
|
||||
}
|
||||
|
||||
const TArray<int32> BoundarySource = MakeSequentialValues(100);
|
||||
for (const int32 RequestedCount : {24, 25, 26, 75, 100})
|
||||
{
|
||||
FRandomStream RandomStream(Seed);
|
||||
TArray<int32> Sample;
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(
|
||||
&BoundarySource,
|
||||
ArrayProperty,
|
||||
RequestedCount,
|
||||
false,
|
||||
&RandomStream,
|
||||
&Sample,
|
||||
ArrayProperty);
|
||||
TestEqual(
|
||||
FString::Printf(TEXT("threshold requested=%d"), RequestedCount),
|
||||
Sample,
|
||||
MakeSampleReference(BoundarySource.Num(), RequestedCount, Seed));
|
||||
}
|
||||
|
||||
const TArray<int32> ReplacementSource = MakeSequentialValues(7);
|
||||
for (const int32 RequestedCount : {0, 1, 7, 14, 100})
|
||||
{
|
||||
FRandomStream FirstStream(Seed);
|
||||
FRandomStream SecondStream(Seed);
|
||||
TArray<int32> FirstSample;
|
||||
TArray<int32> SecondSample;
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(
|
||||
&ReplacementSource,
|
||||
ArrayProperty,
|
||||
RequestedCount,
|
||||
true,
|
||||
&FirstStream,
|
||||
&FirstSample,
|
||||
ArrayProperty);
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(
|
||||
&ReplacementSource,
|
||||
ArrayProperty,
|
||||
RequestedCount,
|
||||
true,
|
||||
&SecondStream,
|
||||
&SecondSample,
|
||||
ArrayProperty);
|
||||
const FString Label = FString::Printf(TEXT("replacement requested=%d"), RequestedCount);
|
||||
TestEqual(Label + TEXT(" count"), FirstSample.Num(), RequestedCount);
|
||||
TestEqual(Label + TEXT(" deterministic"), FirstSample, SecondSample);
|
||||
for (const int32 Value : FirstSample)
|
||||
{
|
||||
TestTrue(Label + TEXT(" source membership"), ReplacementSource.Contains(Value));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilArrayWeightedSamplingScenarioTest,
|
||||
"DirectiveUtilities.ArrayScenarios.WeightedSampling",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilArrayWeightedSamplingScenarioTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
FArrayProperty* ArrayProperty = GetIntegerArrayProperty();
|
||||
if (!TestNotNull("Integer array property should be available", ArrayProperty))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr int32 Seed = 4815;
|
||||
for (const int32 SourceCount : {0, 1, 2, 3, 17, 100, 1000, 4096})
|
||||
{
|
||||
const TArray<int32> Source = MakeSequentialValues(SourceCount);
|
||||
TArray<float> Weights;
|
||||
Weights.Reserve(SourceCount);
|
||||
TSet<int32> SelectableValues;
|
||||
for (int32 Index = 0; Index < SourceCount; ++Index)
|
||||
{
|
||||
const float Weight = Index % 3 == 0 ? 0.0f : static_cast<float>((Index % 7) + 1);
|
||||
Weights.Add(Weight);
|
||||
if (Weight > 0.0f)
|
||||
{
|
||||
SelectableValues.Add(Index);
|
||||
}
|
||||
}
|
||||
|
||||
const TArray<int32> RequestedCounts = {0, 1, SourceCount / 4, SourceCount, SourceCount + 5};
|
||||
for (const int32 RequestedCount : RequestedCounts)
|
||||
{
|
||||
for (const bool bWithReplacement : {false, true})
|
||||
{
|
||||
FRandomStream FirstStream(Seed);
|
||||
FRandomStream SecondStream(Seed);
|
||||
TArray<int32> FirstSample;
|
||||
TArray<int32> SecondSample;
|
||||
const bool bFirstSucceeded = UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
||||
&Source,
|
||||
ArrayProperty,
|
||||
Weights,
|
||||
RequestedCount,
|
||||
bWithReplacement,
|
||||
&FirstStream,
|
||||
&FirstSample,
|
||||
ArrayProperty);
|
||||
const bool bSecondSucceeded = UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
||||
&Source,
|
||||
ArrayProperty,
|
||||
Weights,
|
||||
RequestedCount,
|
||||
bWithReplacement,
|
||||
&SecondStream,
|
||||
&SecondSample,
|
||||
ArrayProperty);
|
||||
const FString Label = FString::Printf(
|
||||
TEXT("source=%d requested=%d replacement=%d"),
|
||||
SourceCount,
|
||||
RequestedCount,
|
||||
bWithReplacement);
|
||||
|
||||
const bool bExpectedSuccess = RequestedCount == 0 || !SelectableValues.IsEmpty();
|
||||
TestEqual(Label + TEXT(" first validity"), bFirstSucceeded, bExpectedSuccess);
|
||||
TestEqual(Label + TEXT(" second validity"), bSecondSucceeded, bExpectedSuccess);
|
||||
TestEqual(Label + TEXT(" deterministic"), FirstSample, SecondSample);
|
||||
const int32 ExpectedCount = !bExpectedSuccess
|
||||
? 0
|
||||
: bWithReplacement
|
||||
? RequestedCount
|
||||
: FMath::Min(RequestedCount, SelectableValues.Num());
|
||||
TestEqual(Label + TEXT(" count"), FirstSample.Num(), ExpectedCount);
|
||||
|
||||
TSet<int32> UniqueValues;
|
||||
for (const int32 Value : FirstSample)
|
||||
{
|
||||
TestTrue(Label + TEXT(" selectable membership"), SelectableValues.Contains(Value));
|
||||
UniqueValues.Add(Value);
|
||||
}
|
||||
if (!bWithReplacement)
|
||||
{
|
||||
TestEqual(Label + TEXT(" unique source indices"), UniqueValues.Num(), FirstSample.Num());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilArrayRangeScenarioTest,
|
||||
"DirectiveUtilities.ArrayScenarios.RangesAndPages",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilArrayRangeScenarioTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
FArrayProperty* ArrayProperty = GetIntegerArrayProperty();
|
||||
if (!TestNotNull("Integer array property should be available", ArrayProperty))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
struct FPageScenario
|
||||
{
|
||||
int32 ItemCount;
|
||||
int32 PageIndex;
|
||||
int32 PageSize;
|
||||
bool bExpectedValid;
|
||||
int32 ExpectedPageCount;
|
||||
};
|
||||
const TArray<FPageScenario> PageScenarios = {
|
||||
{0, 0, 1, false, 0},
|
||||
{1, 0, 1, true, 1},
|
||||
{2, 1, 1, true, 2},
|
||||
{5, 0, 2, true, 3},
|
||||
{5, 1, 2, true, 3},
|
||||
{5, 2, 2, true, 3},
|
||||
{5, 3, 2, false, 3},
|
||||
{10, 3, 3, true, 4},
|
||||
{10, 0, 10, true, 1},
|
||||
{10, 0, 11, true, 1},
|
||||
{10, -1, 3, false, 0},
|
||||
{10, 0, 0, false, 0},
|
||||
{10, 0, -1, false, 0},
|
||||
{10, MAX_int32, 3, false, 4}
|
||||
};
|
||||
|
||||
for (const FPageScenario& Scenario : PageScenarios)
|
||||
{
|
||||
const TArray<int32> Source = MakeSequentialValues(Scenario.ItemCount);
|
||||
TArray<int32> Page;
|
||||
int32 PageCount = INDEX_NONE;
|
||||
const bool bValid = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetPage(
|
||||
&Source,
|
||||
ArrayProperty,
|
||||
Scenario.PageIndex,
|
||||
Scenario.PageSize,
|
||||
&Page,
|
||||
ArrayProperty,
|
||||
&PageCount);
|
||||
const FString Label = FString::Printf(
|
||||
TEXT("items=%d page=%d size=%d"),
|
||||
Scenario.ItemCount,
|
||||
Scenario.PageIndex,
|
||||
Scenario.PageSize);
|
||||
TestEqual(Label + TEXT(" validity"), bValid, Scenario.bExpectedValid);
|
||||
TestEqual(Label + TEXT(" page count"), PageCount, Scenario.ExpectedPageCount);
|
||||
|
||||
TArray<int32> ExpectedPage;
|
||||
if (Scenario.bExpectedValid)
|
||||
{
|
||||
const int32 StartIndex = Scenario.PageIndex * Scenario.PageSize;
|
||||
for (int32 Index = StartIndex; Index < FMath::Min(StartIndex + Scenario.PageSize, Source.Num()); ++Index)
|
||||
{
|
||||
ExpectedPage.Add(Source[Index]);
|
||||
}
|
||||
}
|
||||
TestEqual(Label + TEXT(" values"), Page, ExpectedPage);
|
||||
}
|
||||
|
||||
const TArray<int32> SliceSource = MakeSequentialValues(10);
|
||||
struct FSliceScenario
|
||||
{
|
||||
int32 StartIndex;
|
||||
int32 Count;
|
||||
};
|
||||
const TArray<FSliceScenario> SliceScenarios = {
|
||||
{-5, 3},
|
||||
{0, -1},
|
||||
{0, 0},
|
||||
{0, 10},
|
||||
{0, 20},
|
||||
{5, 3},
|
||||
{9, 5},
|
||||
{10, 1},
|
||||
{11, 1},
|
||||
{MAX_int32, MAX_int32}
|
||||
};
|
||||
for (const FSliceScenario& Scenario : SliceScenarios)
|
||||
{
|
||||
const int32 StartIndex = FMath::Clamp(Scenario.StartIndex, 0, SliceSource.Num());
|
||||
const int32 CopyCount = Scenario.Count > 0
|
||||
? FMath::Min(Scenario.Count, SliceSource.Num() - StartIndex)
|
||||
: 0;
|
||||
TArray<int32> ExpectedSlice;
|
||||
for (int32 Offset = 0; Offset < CopyCount; ++Offset)
|
||||
{
|
||||
ExpectedSlice.Add(SliceSource[StartIndex + Offset]);
|
||||
}
|
||||
|
||||
TArray<int32> Slice;
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(
|
||||
&SliceSource,
|
||||
ArrayProperty,
|
||||
Scenario.StartIndex,
|
||||
Scenario.Count,
|
||||
&Slice,
|
||||
ArrayProperty);
|
||||
const FString Label = FString::Printf(TEXT("start=%d count=%d"), Scenario.StartIndex, Scenario.Count);
|
||||
TestEqual(Label + TEXT(" separate output"), Slice, ExpectedSlice);
|
||||
|
||||
TArray<int32> AliasedSlice = SliceSource;
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(
|
||||
&AliasedSlice,
|
||||
ArrayProperty,
|
||||
Scenario.StartIndex,
|
||||
Scenario.Count,
|
||||
&AliasedSlice,
|
||||
ArrayProperty);
|
||||
TestEqual(Label + TEXT(" aliased output"), AliasedSlice, ExpectedSlice);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilArrayRotationScenarioTest,
|
||||
"DirectiveUtilities.ArrayScenarios.Rotation",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilArrayRotationScenarioTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
FArrayProperty* IntegerArrayProperty = GetIntegerArrayProperty();
|
||||
FArrayProperty* StringArrayProperty = FindFProperty<FArrayProperty>(
|
||||
UDirectiveUtilTestObject::StaticClass(),
|
||||
GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestStringArray));
|
||||
if (!TestNotNull("Integer array property should be available", IntegerArrayProperty)
|
||||
|| !TestNotNull("String array property should be available", StringArrayProperty))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const int32 ItemCount : {0, 1, 2, 3, 4, 5, 16, 257, 10000})
|
||||
{
|
||||
const TArray<int32> Source = MakeSequentialValues(ItemCount);
|
||||
const TArray<int32> Shifts = {
|
||||
0,
|
||||
1,
|
||||
-1,
|
||||
ItemCount,
|
||||
ItemCount + 1,
|
||||
-ItemCount - 1,
|
||||
MAX_int32,
|
||||
MIN_int32
|
||||
};
|
||||
for (const int32 Shift : Shifts)
|
||||
{
|
||||
TArray<int32> Rotated = Source;
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(&Rotated, IntegerArrayProperty, Shift);
|
||||
TestEqual(
|
||||
FString::Printf(TEXT("POD items=%d shift=%d"), ItemCount, Shift),
|
||||
Rotated,
|
||||
MakeRotationReference(Source, Shift));
|
||||
}
|
||||
}
|
||||
|
||||
for (const int32 ItemCount : {0, 1, 2, 5, 32})
|
||||
{
|
||||
TArray<FString> Source;
|
||||
Source.Reserve(ItemCount);
|
||||
for (int32 Index = 0; Index < ItemCount; ++Index)
|
||||
{
|
||||
Source.Add(FString::Printf(TEXT("Value%d"), Index));
|
||||
}
|
||||
for (const int32 Shift : {0, 1, -1, 7, -11, MAX_int32, MIN_int32})
|
||||
{
|
||||
TArray<FString> Rotated = Source;
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(&Rotated, StringArrayProperty, Shift);
|
||||
TestEqual(
|
||||
FString::Printf(TEXT("managed items=%d shift=%d"), ItemCount, Shift),
|
||||
Rotated,
|
||||
MakeRotationReference(Source, Shift));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilArrayOrderingScenarioTest,
|
||||
"DirectiveUtilities.ArrayScenarios.NaturalOrdering",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilArrayOrderingScenarioTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
for (const int32 ItemCount : {0, 1, 2, 10, 100, 1000})
|
||||
{
|
||||
TArray<FString> Strings;
|
||||
TArray<FString> ExpectedStrings;
|
||||
TArray<FName> Names;
|
||||
TArray<FName> ExpectedNames;
|
||||
Strings.Reserve(ItemCount);
|
||||
ExpectedStrings.Reserve(ItemCount);
|
||||
Names.Reserve(ItemCount);
|
||||
ExpectedNames.Reserve(ItemCount);
|
||||
for (int32 Index = 0; Index < ItemCount; ++Index)
|
||||
{
|
||||
ExpectedStrings.Add(FString::Printf(TEXT("Item%d"), Index));
|
||||
ExpectedNames.Add(FName(*FString::Printf(TEXT("Actor%d"), Index)));
|
||||
}
|
||||
for (int32 Index = ItemCount - 1; Index >= 0; --Index)
|
||||
{
|
||||
Strings.Add(ExpectedStrings[Index]);
|
||||
Names.Add(ExpectedNames[Index]);
|
||||
}
|
||||
|
||||
UDirectiveUtilArrayFunctionLibrary::NaturalSortStringArray(Strings);
|
||||
UDirectiveUtilArrayFunctionLibrary::NaturalSortNameArray(Names);
|
||||
const FString Label = FString::Printf(TEXT("items=%d"), ItemCount);
|
||||
TestEqual(Label + TEXT(" string ascending"), Strings, ExpectedStrings);
|
||||
TestEqual(Label + TEXT(" name ascending"), Names, ExpectedNames);
|
||||
|
||||
UDirectiveUtilArrayFunctionLibrary::NaturalSortStringArray(Strings, true);
|
||||
UDirectiveUtilArrayFunctionLibrary::NaturalSortNameArray(Names, true);
|
||||
Algo::Reverse(ExpectedStrings);
|
||||
Algo::Reverse(ExpectedNames);
|
||||
TestEqual(Label + TEXT(" string descending"), Strings, ExpectedStrings);
|
||||
TestEqual(Label + TEXT(" name descending"), Names, ExpectedNames);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilArrayDeterministicFuzzTest,
|
||||
"DirectiveUtilities.ArrayScenarios.DeterministicFuzz",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilArrayDeterministicFuzzTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
FArrayProperty* ArrayProperty = GetIntegerArrayProperty();
|
||||
if (!TestNotNull("Integer array property should be available", ArrayProperty))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UDirectiveUtilTestObject* TestObject = NewObject<UDirectiveUtilTestObject>();
|
||||
const TArray<int32> ItemCounts = {0, 1, 2, 3, 7, 16, 31, 32, 127, 128, 1024, 4096};
|
||||
for (const int32 Seed : {17, 271, 4099, 65537, 104729})
|
||||
{
|
||||
for (int32 CountIndex = 0; CountIndex < ItemCounts.Num(); ++CountIndex)
|
||||
{
|
||||
const int32 ItemCount = ItemCounts[CountIndex];
|
||||
FRandomStream Stream(Seed + ItemCount * 31);
|
||||
TArray<int32> Source;
|
||||
Source.Reserve(ItemCount);
|
||||
for (int32 Index = 0; Index < ItemCount; ++Index)
|
||||
{
|
||||
Source.Add(Stream.RandRange(-64, 64));
|
||||
}
|
||||
const FString Label = FString::Printf(TEXT("seed=%d items=%d"), Seed, ItemCount);
|
||||
|
||||
const TArray<int32> ExpectedDistinct = MakeDistinctReference(Source);
|
||||
TestObject->TestArray = Source;
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestArray, ArrayProperty);
|
||||
TestEqual(Label + TEXT(" RemoveDuplicates"), TestObject->TestArray, ExpectedDistinct);
|
||||
|
||||
TArray<int32> DistinctResult;
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct(
|
||||
&Source,
|
||||
ArrayProperty,
|
||||
&DistinctResult,
|
||||
ArrayProperty);
|
||||
TestEqual(Label + TEXT(" GetDistinct"), DistinctResult, ExpectedDistinct);
|
||||
|
||||
int32 ExpectedMostCommon = 0;
|
||||
int32 ExpectedMostCommonCount = 0;
|
||||
const bool bExpectedMostCommon = FindMostCommonReference(
|
||||
Source,
|
||||
ExpectedMostCommon,
|
||||
ExpectedMostCommonCount);
|
||||
int32 MostCommon = 0;
|
||||
int32 MostCommonCount = 0;
|
||||
const bool bFoundMostCommon = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetMostCommon(
|
||||
&Source,
|
||||
ArrayProperty,
|
||||
&MostCommon,
|
||||
&MostCommonCount);
|
||||
TestEqual(Label + TEXT(" GetMostCommon result"), bFoundMostCommon, bExpectedMostCommon);
|
||||
TestEqual(Label + TEXT(" GetMostCommon value"), MostCommon, ExpectedMostCommon);
|
||||
TestEqual(Label + TEXT(" GetMostCommon count"), MostCommonCount, ExpectedMostCommonCount);
|
||||
|
||||
const int32 QueryValue = Stream.RandRange(-70, 70);
|
||||
TestEqual(
|
||||
Label + TEXT(" CountOccurrences"),
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_CountOccurrences(&Source, ArrayProperty, &QueryValue),
|
||||
CountReference(Source, QueryValue));
|
||||
|
||||
const TArray<int32> ExpectedRemoved = RemoveAllReference(Source, QueryValue);
|
||||
TestObject->TestArray = Source;
|
||||
const bool bRemoved = UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences(
|
||||
&TestObject->TestArray,
|
||||
ArrayProperty,
|
||||
&QueryValue);
|
||||
TestEqual(Label + TEXT(" RemoveAllOccurrences result"), bRemoved, ExpectedRemoved.Num() != Source.Num());
|
||||
TestEqual(Label + TEXT(" RemoveAllOccurrences values"), TestObject->TestArray, ExpectedRemoved);
|
||||
|
||||
TArray<int32> ExpectedAppended = Source;
|
||||
ExpectedAppended.Append(Source);
|
||||
TestObject->TestArray = Source;
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized(
|
||||
&TestObject->TestArray,
|
||||
ArrayProperty,
|
||||
&TestObject->TestArray,
|
||||
ArrayProperty);
|
||||
TestEqual(Label + TEXT(" AppendOptimized self append"), TestObject->TestArray, ExpectedAppended);
|
||||
|
||||
for (const int32 Shift : {MIN_int32, -ItemCount - 1, -1, 0, 1, ItemCount + 1, MAX_int32})
|
||||
{
|
||||
TestObject->TestArray = Source;
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(&TestObject->TestArray, ArrayProperty, Shift);
|
||||
TestEqual(
|
||||
Label + FString::Printf(TEXT(" Rotate shift=%d"), Shift),
|
||||
TestObject->TestArray,
|
||||
MakeRotationReference(Source, Shift));
|
||||
}
|
||||
|
||||
const int32 StartIndex = Stream.RandRange(-ItemCount - 2, ItemCount + 2);
|
||||
const int32 SliceCount = Stream.RandRange(-2, ItemCount + 2);
|
||||
const int32 ExpectedStart = FMath::Clamp(StartIndex, 0, ItemCount);
|
||||
const int32 ExpectedSliceCount = FMath::Max(0, FMath::Min(SliceCount, ItemCount - ExpectedStart));
|
||||
const TArray<int32> ExpectedSlice = SliceReference(Source, ExpectedStart, ExpectedSliceCount);
|
||||
TArray<int32> SliceResult;
|
||||
UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(
|
||||
&Source,
|
||||
ArrayProperty,
|
||||
StartIndex,
|
||||
SliceCount,
|
||||
&SliceResult,
|
||||
ArrayProperty);
|
||||
TestEqual(Label + TEXT(" Slice"), SliceResult, ExpectedSlice);
|
||||
|
||||
const int32 PageSize = CountIndex % 4 == 0 ? 0 : 1 << (CountIndex % 7);
|
||||
const int32 ExpectedPageCount = PageSize > 0 ? FMath::DivideAndRoundUp(ItemCount, PageSize) : 0;
|
||||
for (const int32 PageIndex : {-1, 0, FMath::Max(0, ExpectedPageCount - 1), ExpectedPageCount})
|
||||
{
|
||||
TArray<int32> PageResult;
|
||||
int32 PageCount = -1;
|
||||
const bool bPageValid = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetPage(
|
||||
&Source,
|
||||
ArrayProperty,
|
||||
PageIndex,
|
||||
PageSize,
|
||||
&PageResult,
|
||||
ArrayProperty,
|
||||
&PageCount);
|
||||
const bool bExpectedValid = PageSize > 0 && PageIndex >= 0 && PageIndex < ExpectedPageCount;
|
||||
const TArray<int32> ExpectedPage = bExpectedValid
|
||||
? SliceReference(Source, PageIndex * PageSize, FMath::Min(PageSize, ItemCount - PageIndex * PageSize))
|
||||
: TArray<int32>();
|
||||
const FString PageLabel = Label + FString::Printf(TEXT(" Page index=%d size=%d"), PageIndex, PageSize);
|
||||
TestEqual(PageLabel + TEXT(" validity"), bPageValid, bExpectedValid);
|
||||
TestEqual(
|
||||
PageLabel + TEXT(" count"),
|
||||
PageCount,
|
||||
PageSize > 0 && PageIndex >= 0 ? ExpectedPageCount : 0);
|
||||
TestEqual(PageLabel + TEXT(" values"), PageResult, ExpectedPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,13 +12,11 @@
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "GameFramework/DefaultPawn.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "TimerManager.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
namespace DirectiveUtilAsyncTraceTestHelpers
|
||||
{
|
||||
/** Creates a transient game world with a physics scene, initialized for play so traces resolve. */
|
||||
UWorld* CreateTraceWorld()
|
||||
{
|
||||
UWorld* World = UWorld::CreateWorld(EWorldType::Game, false);
|
||||
@@ -33,7 +31,6 @@ namespace DirectiveUtilAsyncTraceTestHelpers
|
||||
return World;
|
||||
}
|
||||
|
||||
/** Spawns a blocking cube actor at the origin so traces have something to hit. */
|
||||
AStaticMeshActor* SpawnBlockingCube(UWorld* World, UStaticMesh* CubeMesh)
|
||||
{
|
||||
AStaticMeshActor* Cube = World->SpawnActor<AStaticMeshActor>(FVector::ZeroVector, FRotator::ZeroRotator);
|
||||
@@ -42,15 +39,36 @@ namespace DirectiveUtilAsyncTraceTestHelpers
|
||||
Component->SetStaticMesh(CubeMesh);
|
||||
Component->SetCollisionProfileName(TEXT("BlockAll"));
|
||||
Component->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
||||
Component->UpdateCollisionProfile();
|
||||
return Cube;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Latent command that ticks a trace world each frame until every listener has reported completion
|
||||
* (or the frame budget runs out), asserts the expected outcome, and tears the world down.
|
||||
*/
|
||||
bool UDirectiveUtilTestMoveToLocationTask::HasRegisteredTimers() const
|
||||
{
|
||||
const UWorld* World = TimerWorld.Get();
|
||||
return World && (World->GetTimerManager().TimerExists(TimerHandle) || World->GetTimerManager().TimerExists(StuckTimerHandle));
|
||||
}
|
||||
|
||||
void UDirectiveUtilTestMoveToLocationTask::RegisterTimersForTest(UWorld* World)
|
||||
{
|
||||
TimerWorld = World;
|
||||
World->GetTimerManager().SetTimer(TimerHandle, FTimerDelegate::CreateLambda([] {}), 60.0f, true);
|
||||
World->GetTimerManager().SetTimer(StuckTimerHandle, FTimerDelegate::CreateLambda([] {}), 60.0f, true);
|
||||
}
|
||||
|
||||
bool UDirectiveUtilTestMoveToActorTask::HasRegisteredTimers() const
|
||||
{
|
||||
const UWorld* World = TimerWorld.Get();
|
||||
return World && (World->GetTimerManager().TimerExists(TimerHandle) || World->GetTimerManager().TimerExists(StuckTimerHandle));
|
||||
}
|
||||
|
||||
void UDirectiveUtilTestMoveToActorTask::RegisterTimersForTest(UWorld* World)
|
||||
{
|
||||
TimerWorld = World;
|
||||
World->GetTimerManager().SetTimer(TimerHandle, FTimerDelegate::CreateLambda([] {}), 60.0f, true);
|
||||
World->GetTimerManager().SetTimer(StuckTimerHandle, FTimerDelegate::CreateLambda([] {}), 60.0f, true);
|
||||
}
|
||||
|
||||
class FDirectiveUtilTickTraceWorld : public IAutomationLatentCommand
|
||||
{
|
||||
public:
|
||||
@@ -111,18 +129,12 @@ private:
|
||||
int32 FramesRemaining;
|
||||
};
|
||||
|
||||
/**
|
||||
* DirectiveUtilTask_AsyncTrace: verifies the null-world guard broadcasts an empty result, and that each trace
|
||||
* shape (line, sphere, box, capsule) resolves against a blocking body and reports a hit.
|
||||
*/
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilAsyncTraceTest, "DirectiveUtilities.AsyncTaskTraceTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilAsyncTraceTest, "DirectiveUtilities.AsyncTaskTraceTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilAsyncTraceTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// The null-world activation intentionally logs a warning.
|
||||
AddExpectedMessagePlain(TEXT("Async Trace failed to activate. World is null."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
// Null world guard: activating with a null context broadcasts an empty result and does not crash.
|
||||
{
|
||||
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
||||
Listener->AddToRoot();
|
||||
@@ -142,8 +154,8 @@ bool FDirectiveUtilAsyncTraceTest::RunTest(const FString& Parameters)
|
||||
UStaticMesh* CubeMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
|
||||
if (!CubeMesh)
|
||||
{
|
||||
AddInfo(TEXT("Engine cube mesh unavailable; skipping async trace hit scenarios."));
|
||||
return true;
|
||||
AddError(TEXT("Engine cube mesh unavailable for async trace hit scenarios."));
|
||||
return false;
|
||||
}
|
||||
|
||||
UWorld* World = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
|
||||
@@ -154,10 +166,9 @@ bool FDirectiveUtilAsyncTraceTest::RunTest(const FString& Parameters)
|
||||
}
|
||||
DirectiveUtilAsyncTraceTestHelpers::SpawnBlockingCube(World, CubeMesh);
|
||||
|
||||
// Trace straight down through the cube at the origin so every shape intersects it.
|
||||
const FVector Start(0.0f, 0.0f, 500.0f);
|
||||
const FVector End(0.0f, 0.0f, -500.0f);
|
||||
const ETraceTypeQuery Channel = ETraceTypeQuery::TraceTypeQuery1; // Visibility, which BlockAll blocks.
|
||||
const ETraceTypeQuery VisibilityChannel = ETraceTypeQuery::TraceTypeQuery1;
|
||||
|
||||
auto MakeListener = [](UDirectiveUtilTask_AsyncTrace* Task) -> UDirectiveUtilDelegateListener*
|
||||
{
|
||||
@@ -170,82 +181,23 @@ bool FDirectiveUtilAsyncTraceTest::RunTest(const FString& Parameters)
|
||||
};
|
||||
|
||||
TArray<UDirectiveUtilDelegateListener*> Listeners;
|
||||
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncLineTraceByChannel(World, Start, End, Channel, false)));
|
||||
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncSphereTraceByChannel(World, Start, End, 25.0f, Channel, false)));
|
||||
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncBoxTraceByChannel(World, Start, End, FVector(25.0f), FRotator::ZeroRotator, Channel, false)));
|
||||
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncCapsuleTraceByChannel(World, Start, End, 25.0f, 50.0f, Channel, false)));
|
||||
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncLineTraceByChannel(World, Start, End, VisibilityChannel, false)));
|
||||
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncSphereTraceByChannel(World, Start, End, 25.0f, VisibilityChannel, false)));
|
||||
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncBoxTraceByChannel(World, Start, End, FVector(25.0f), FRotator::ZeroRotator, VisibilityChannel, false)));
|
||||
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncCapsuleTraceByChannel(World, Start, End, 25.0f, 50.0f, VisibilityChannel, false)));
|
||||
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickTraceWorld(this, World, Listeners, 120));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Latent command that ticks a move-to-location world each frame until the listener reports
|
||||
* completion (or the frame budget runs out), asserts a single failed completion, and tears the
|
||||
* world down.
|
||||
*/
|
||||
class FDirectiveUtilTickMoveToLocationWorld : public IAutomationLatentCommand
|
||||
{
|
||||
public:
|
||||
FDirectiveUtilTickMoveToLocationWorld(FAutomationTestBase* InTest, UWorld* InWorld, UDirectiveUtilDelegateListener* InListener, int32 InFrames)
|
||||
: Test(InTest)
|
||||
, World(InWorld)
|
||||
, Listener(InListener)
|
||||
, FramesRemaining(InFrames)
|
||||
{
|
||||
}
|
||||
|
||||
virtual bool Update() override
|
||||
{
|
||||
if (UWorld* TickWorld = World.Get())
|
||||
{
|
||||
TickWorld->Tick(LEVELTICK_All, 0.05f);
|
||||
}
|
||||
|
||||
if (Listener && !Listener->bCompleted && --FramesRemaining > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Listener)
|
||||
{
|
||||
Test->TestTrue(TEXT("Move without navigation broadcasts Completed"), Listener->bCompleted);
|
||||
Test->TestFalse(TEXT("Move without navigation reports failure"), Listener->bLastSuccess);
|
||||
Test->TestEqual(TEXT("Move without navigation completes exactly once"), Listener->CompletedCount, 1);
|
||||
Listener->Keepalive = nullptr;
|
||||
Listener->RemoveFromRoot();
|
||||
}
|
||||
|
||||
if (UWorld* TearDownWorld = World.Get())
|
||||
{
|
||||
GEngine->DestroyWorldContext(TearDownWorld);
|
||||
TearDownWorld->DestroyWorld(false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
FAutomationTestBase* Test;
|
||||
TWeakObjectPtr<UWorld> World;
|
||||
UDirectiveUtilDelegateListener* Listener;
|
||||
int32 FramesRemaining;
|
||||
};
|
||||
|
||||
/**
|
||||
* DirectiveUtilTask_MoveToLocation: verifies the guard paths (null controller, controller without a pawn) and
|
||||
* EndTask all broadcast Completed(false) without crashing, that a second EndTask does not broadcast
|
||||
* again, and that a move with no navigation data terminates with failure. The successful navigation
|
||||
* path requires a built navigation mesh and is exercised in a project-level test rather than here.
|
||||
*/
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMoveToLocationTest, "DirectiveUtilities.AsyncTaskMoveToLocationTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMoveToLocationTest, "DirectiveUtilities.AsyncTaskMoveToLocationTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// The guard paths intentionally log a warning.
|
||||
AddExpectedMessagePlain(TEXT("Controller, pawn, or world is unavailable while moving to location. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
AddExpectedMessagePlain(TEXT("Controller or pawn has been destroyed while moving to location. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
// Null controller guard: activating broadcasts Completed(false).
|
||||
{
|
||||
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
||||
Listener->AddToRoot();
|
||||
@@ -262,16 +214,15 @@ bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
|
||||
Listener->RemoveFromRoot();
|
||||
}
|
||||
|
||||
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
|
||||
UWorld* World = UWorld::CreateWorld(EWorldType::Game, false);
|
||||
if (!World)
|
||||
{
|
||||
AddError(TEXT("Failed to create a transient world for the move-to-location test."));
|
||||
return false;
|
||||
}
|
||||
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
|
||||
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Game);
|
||||
WorldContext.SetCurrentWorld(World);
|
||||
|
||||
// Controller-without-pawn guard: activating broadcasts Completed(false).
|
||||
{
|
||||
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
||||
|
||||
@@ -290,7 +241,6 @@ bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
|
||||
Listener->RemoveFromRoot();
|
||||
}
|
||||
|
||||
// EndTask broadcasts Completed(false) and clears timers without crashing.
|
||||
{
|
||||
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
||||
|
||||
@@ -309,7 +259,6 @@ bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
|
||||
Listener->RemoveFromRoot();
|
||||
}
|
||||
|
||||
// Double completion guard: a second EndTask does not broadcast Completed again.
|
||||
{
|
||||
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
||||
|
||||
@@ -328,31 +277,44 @@ bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
|
||||
Listener->RemoveFromRoot();
|
||||
}
|
||||
|
||||
{
|
||||
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
||||
ADefaultPawn* Pawn = World->SpawnActor<ADefaultPawn>();
|
||||
if (Controller && Pawn)
|
||||
{
|
||||
Controller->SetPawn(Pawn);
|
||||
UDirectiveUtilTestMoveToLocationTask* Task = NewObject<UDirectiveUtilTestMoveToLocationTask>();
|
||||
Task->Configure(Controller, FVector(1000.0f, 0.0f, 0.0f), true);
|
||||
Task->RegisterTimersForTest(World);
|
||||
TestTrue("Move to location registers both lifecycle timers", Task->HasRegisteredTimers());
|
||||
Task->ClearController();
|
||||
Task->Complete();
|
||||
TestFalse("Move to location clears timers without a controller", Task->HasRegisteredTimers());
|
||||
Task->Configure(Controller, FVector(1000.0f, 0.0f, 0.0f), true);
|
||||
Task->Activate();
|
||||
TestFalse("A completed move to location should not restart", Task->HasRegisteredTimers());
|
||||
}
|
||||
}
|
||||
|
||||
GEngine->DestroyWorldContext(World);
|
||||
World->DestroyWorld(false);
|
||||
|
||||
// No-navigation failure: without a navmesh the idle path-following check terminates the task
|
||||
// with failure instead of polling forever.
|
||||
{
|
||||
// SimpleMoveToLocation may warn when the world has no navigation system.
|
||||
AddExpectedMessagePlain(TEXT("SimpleMoveToActor called for NavSys:"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
AddExpectedMessagePlain(TEXT("SimpleMove failed for"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
UWorld* MoveWorld = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
|
||||
if (!MoveWorld)
|
||||
{
|
||||
AddInfo(TEXT("Failed to create a transient game world; skipping the no-navigation move scenario."));
|
||||
return true;
|
||||
AddError(TEXT("Failed to create a transient game world for the no-navigation move scenario."));
|
||||
return false;
|
||||
}
|
||||
|
||||
APlayerController* Controller = MoveWorld->SpawnActor<APlayerController>();
|
||||
ADefaultPawn* Pawn = MoveWorld->SpawnActor<ADefaultPawn>(FVector::ZeroVector, FRotator::ZeroRotator);
|
||||
if (!Controller || !Pawn)
|
||||
{
|
||||
AddInfo(TEXT("Failed to spawn a controller or pawn; skipping the no-navigation move scenario."));
|
||||
AddError(TEXT("Failed to spawn a controller or pawn for the no-navigation move scenario."));
|
||||
GEngine->DestroyWorldContext(MoveWorld);
|
||||
MoveWorld->DestroyWorld(false);
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
Controller->SetPawn(Pawn);
|
||||
|
||||
@@ -364,26 +326,26 @@ bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
|
||||
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
|
||||
Task->Activate();
|
||||
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickMoveToLocationWorld(this, MoveWorld, Listener, 120));
|
||||
TestTrue("A location move without navigation broadcasts Completed", Listener->bCompleted);
|
||||
TestFalse("A location move without navigation reports failure", Listener->bLastSuccess);
|
||||
TestEqual("A location move without navigation completes exactly once", Listener->CompletedCount, 1);
|
||||
Listener->Keepalive = nullptr;
|
||||
Listener->RemoveFromRoot();
|
||||
GEngine->DestroyWorldContext(MoveWorld);
|
||||
MoveWorld->DestroyWorld(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* DirectiveUtilTask_MoveToActor: verifies the guard paths (null controller, null goal) broadcast Completed(false)
|
||||
* without crashing, that a second EndTask does not broadcast again, and that a move with no
|
||||
* navigation data terminates with failure. The successful navigation path requires a built
|
||||
* navigation mesh and is exercised in a project-level test rather than here.
|
||||
*/
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMoveToActorTest, "DirectiveUtilities.AsyncTaskMoveToActorTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMoveToActorTest, "DirectiveUtilities.AsyncTaskMoveToActorTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// The guard paths intentionally log a warning.
|
||||
AddExpectedMessagePlain(TEXT("Controller, pawn, goal, or world is unavailable while moving to actor. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
AddExpectedMessagePlain(TEXT("Controller, pawn, or world is unavailable while moving to actor. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
AddExpectedMessagePlain(TEXT("Controller, pawn, or goal has been destroyed while moving to actor. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
// Null controller guard: activating broadcasts Completed(false).
|
||||
{
|
||||
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
||||
Listener->AddToRoot();
|
||||
@@ -400,16 +362,15 @@ bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters)
|
||||
Listener->RemoveFromRoot();
|
||||
}
|
||||
|
||||
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
|
||||
UWorld* World = UWorld::CreateWorld(EWorldType::Game, false);
|
||||
if (!World)
|
||||
{
|
||||
AddError(TEXT("Failed to create a transient world for the move-to-actor test."));
|
||||
return false;
|
||||
}
|
||||
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
|
||||
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Game);
|
||||
WorldContext.SetCurrentWorld(World);
|
||||
|
||||
// Null goal guard: a controller with a pawn but no goal broadcasts Completed(false).
|
||||
{
|
||||
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
||||
ADefaultPawn* Pawn = World->SpawnActor<ADefaultPawn>(FVector::ZeroVector, FRotator::ZeroRotator);
|
||||
@@ -433,11 +394,10 @@ bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters)
|
||||
}
|
||||
else
|
||||
{
|
||||
AddInfo(TEXT("Failed to spawn a controller or pawn; skipping the null-goal scenario."));
|
||||
AddError(TEXT("Failed to spawn a controller or pawn for the null-goal scenario."));
|
||||
}
|
||||
}
|
||||
|
||||
// Double completion guard: a second EndTask does not broadcast Completed again.
|
||||
{
|
||||
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
||||
|
||||
@@ -456,21 +416,35 @@ bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters)
|
||||
Listener->RemoveFromRoot();
|
||||
}
|
||||
|
||||
{
|
||||
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
||||
ADefaultPawn* Pawn = World->SpawnActor<ADefaultPawn>();
|
||||
AStaticMeshActor* Goal = World->SpawnActor<AStaticMeshActor>();
|
||||
if (Controller && Pawn && Goal)
|
||||
{
|
||||
Controller->SetPawn(Pawn);
|
||||
UDirectiveUtilTestMoveToActorTask* Task = NewObject<UDirectiveUtilTestMoveToActorTask>();
|
||||
Task->Configure(Controller, Goal, true);
|
||||
Task->RegisterTimersForTest(World);
|
||||
TestTrue("Move to actor registers both lifecycle timers", Task->HasRegisteredTimers());
|
||||
Task->ClearController();
|
||||
Task->Complete();
|
||||
TestFalse("Move to actor clears timers without a controller", Task->HasRegisteredTimers());
|
||||
Task->Configure(Controller, Goal, true);
|
||||
Task->Activate();
|
||||
TestFalse("A completed move to actor should not restart", Task->HasRegisteredTimers());
|
||||
}
|
||||
}
|
||||
|
||||
GEngine->DestroyWorldContext(World);
|
||||
World->DestroyWorld(false);
|
||||
|
||||
// No-navigation failure: without a navmesh the idle path-following check terminates the task
|
||||
// with failure instead of polling forever.
|
||||
{
|
||||
// SimpleMoveToActor may warn when the world has no navigation system.
|
||||
AddExpectedMessagePlain(TEXT("SimpleMoveToActor called for NavSys:"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
AddExpectedMessagePlain(TEXT("SimpleMove failed for"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
UWorld* MoveWorld = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
|
||||
if (!MoveWorld)
|
||||
{
|
||||
AddInfo(TEXT("Failed to create a transient game world; skipping the no-navigation move scenario."));
|
||||
return true;
|
||||
AddError(TEXT("Failed to create a transient game world for the no-navigation move scenario."));
|
||||
return false;
|
||||
}
|
||||
|
||||
APlayerController* Controller = MoveWorld->SpawnActor<APlayerController>();
|
||||
@@ -478,10 +452,10 @@ bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters)
|
||||
AStaticMeshActor* GoalActor = MoveWorld->SpawnActor<AStaticMeshActor>(FVector(10000.0f, 0.0f, 0.0f), FRotator::ZeroRotator);
|
||||
if (!Controller || !Pawn || !GoalActor)
|
||||
{
|
||||
AddInfo(TEXT("Failed to spawn a controller, pawn, or goal; skipping the no-navigation move scenario."));
|
||||
AddError(TEXT("Failed to spawn a controller, pawn, or goal for the no-navigation move scenario."));
|
||||
GEngine->DestroyWorldContext(MoveWorld);
|
||||
MoveWorld->DestroyWorld(false);
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
Controller->SetPawn(Pawn);
|
||||
|
||||
@@ -493,10 +467,14 @@ bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters)
|
||||
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
|
||||
Task->Activate();
|
||||
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickMoveToLocationWorld(this, MoveWorld, Listener, 120));
|
||||
TestTrue("An actor move without navigation broadcasts Completed", Listener->bCompleted);
|
||||
TestFalse("An actor move without navigation reports failure", Listener->bLastSuccess);
|
||||
TestEqual("An actor move without navigation completes exactly once", Listener->CompletedCount, 1);
|
||||
Listener->Keepalive = nullptr;
|
||||
Listener->RemoveFromRoot();
|
||||
GEngine->DestroyWorldContext(MoveWorld);
|
||||
MoveWorld->DestroyWorld(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // WITH_EDITOR
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "UObject/Class.h"
|
||||
#include "UObject/Package.h"
|
||||
#include "UObject/UnrealType.h"
|
||||
#include "UObject/UObjectIterator.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilBlueprintCategoryTest, "DirectiveUtilities.BlueprintCategoryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
@@ -8,8 +11,9 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilBlueprintCategoryTest, "Directive
|
||||
bool FDirectiveUtilBlueprintCategoryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
const FString ExpectedCategoryRoot = TEXT("Directive Utilities");
|
||||
const FString RuntimeScriptPackage = TEXT("/Script/DirectiveUtilitiesRuntime");
|
||||
const TSet<FString> PluginScriptPackages = {
|
||||
TEXT("/Script/DirectiveUtilitiesRuntime"),
|
||||
RuntimeScriptPackage,
|
||||
TEXT("/Script/DirectiveUtilitiesEditor")
|
||||
};
|
||||
|
||||
@@ -32,6 +36,15 @@ bool FDirectiveUtilBlueprintCategoryTest::RunTest(const FString& Parameters)
|
||||
}
|
||||
|
||||
++TestedFunctionCount;
|
||||
if (Class->GetOutermost()->GetName() == RuntimeScriptPackage)
|
||||
{
|
||||
TestFalse(
|
||||
FString::Printf(TEXT("%s.%s is available at runtime"), *Class->GetName(), *Function->GetName()),
|
||||
Class->GetOutermost()->HasAnyPackageFlags(PKG_EditorOnly | PKG_UncookedOnly | PKG_Developer)
|
||||
|| Function->HasAnyFunctionFlags(FUNC_EditorOnly)
|
||||
);
|
||||
}
|
||||
|
||||
const FString Category = Function->GetMetaData(TEXT("Category"));
|
||||
FString CategoryRoot = Category;
|
||||
int32 CategoryDelimiterIndex = INDEX_NONE;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
#include "AssetRegistry/DirectiveUtilDependencyCycleFinder.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilDependencyCycleFinderTest,
|
||||
"DirectiveUtilities.EditorDependencyCycleFinderTests",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilDependencyCycleFinderTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
TMap<FName, TArray<FName>> Graph;
|
||||
Graph.Add(TEXT("/Game/A"), {TEXT("/Game/B")});
|
||||
Graph.Add(TEXT("/Game/B"), {TEXT("/Game/A")});
|
||||
Graph.Add(TEXT("/Game/C"), {TEXT("/Game/C")});
|
||||
Graph.Add(TEXT("/Game/D"), {TEXT("/Game/E")});
|
||||
Graph.Add(TEXT("/Game/E"));
|
||||
|
||||
const TArray<FDirectiveUtilAssetDependencyCycle> Cycles =
|
||||
DirectiveUtilitiesEditor::FDependencyCycleFinder(Graph).Find();
|
||||
TestEqual(TEXT("The graph contains two cycles"), Cycles.Num(), 2);
|
||||
if (Cycles.Num() == 2)
|
||||
{
|
||||
TestEqual(TEXT("The first cycle contains two packages"), Cycles[0].Packages.Num(), 2);
|
||||
TestTrue(TEXT("The first cycle contains A"), Cycles[0].Packages.Contains(TEXT("/Game/A")));
|
||||
TestTrue(TEXT("The first cycle contains B"), Cycles[0].Packages.Contains(TEXT("/Game/B")));
|
||||
TestEqual(TEXT("The self-cycle contains one package"), Cycles[1].Packages.Num(), 1);
|
||||
TestTrue(TEXT("The self-cycle contains C"), Cycles[1].Packages.Contains(TEXT("/Game/C")));
|
||||
}
|
||||
|
||||
constexpr int32 NodeCount = 20000;
|
||||
constexpr int32 CycleStart = NodeCount / 2;
|
||||
TArray<FName> Nodes;
|
||||
Nodes.Reserve(NodeCount);
|
||||
for (int32 Index = 0; Index < NodeCount; ++Index)
|
||||
{
|
||||
Nodes.Add(*FString::Printf(TEXT("/Game/Deep/%05d"), Index));
|
||||
}
|
||||
|
||||
TMap<FName, TArray<FName>> DeepGraph;
|
||||
DeepGraph.Reserve(NodeCount);
|
||||
for (int32 Index = 0; Index < NodeCount - 1; ++Index)
|
||||
{
|
||||
DeepGraph.Add(Nodes[Index], {Nodes[Index + 1]});
|
||||
}
|
||||
DeepGraph.Add(Nodes.Last(), {Nodes[CycleStart]});
|
||||
|
||||
const TArray<FDirectiveUtilAssetDependencyCycle> DeepCycles =
|
||||
DirectiveUtilitiesEditor::FDependencyCycleFinder(DeepGraph).Find();
|
||||
TestEqual(TEXT("The deep graph contains one cycle"), DeepCycles.Num(), 1);
|
||||
if (DeepCycles.Num() == 1)
|
||||
{
|
||||
TestEqual(TEXT("The deep cycle contains every connected member"), DeepCycles[0].Packages.Num(), NodeCount - CycleStart);
|
||||
TestTrue(TEXT("The deep cycle starts at the expected package"), DeepCycles[0].Packages.Contains(Nodes[CycleStart]));
|
||||
TestTrue(TEXT("The deep cycle ends at the expected package"), DeepCycles[0].Packages.Contains(Nodes.Last()));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,167 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
#include "Subsystems/DirectiveUtilEditorActorSubsystem.h"
|
||||
#include "Components/BoxComponent.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilEditorActorLayoutTest,
|
||||
"DirectiveUtilities.EditorActorLayoutTests",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilEditorActorLayoutTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
|
||||
if (!World)
|
||||
{
|
||||
AddError(TEXT("Failed to create an editor world"));
|
||||
return false;
|
||||
}
|
||||
|
||||
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
|
||||
WorldContext.SetCurrentWorld(World);
|
||||
|
||||
auto SpawnBox = [World](const FVector Location, const FVector Extent, const ECollisionChannel ObjectType) {
|
||||
AActor* Actor = World->SpawnActor<AActor>();
|
||||
Actor->ClearFlags(RF_Transient);
|
||||
UBoxComponent* Box = NewObject<UBoxComponent>(Actor);
|
||||
Actor->SetRootComponent(Box);
|
||||
Box->SetBoxExtent(Extent);
|
||||
Box->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||
Box->SetCollisionObjectType(ObjectType);
|
||||
Box->SetCollisionResponseToAllChannels(ECR_Block);
|
||||
Box->RegisterComponent();
|
||||
Actor->SetActorLocation(Location);
|
||||
return Actor;
|
||||
};
|
||||
|
||||
AActor* First = SpawnBox(FVector(0.0f, 0.0f, 100.0f), FVector(10.0f), ECC_WorldDynamic);
|
||||
AActor* Middle = SpawnBox(FVector(30.0f, 20.0f, 100.0f), FVector(10.0f), ECC_WorldDynamic);
|
||||
AActor* Last = SpawnBox(FVector(100.0f, 40.0f, 100.0f), FVector(10.0f), ECC_WorldDynamic);
|
||||
|
||||
FDirectiveUtilActorOperationResult AlignResult = UDirectiveUtilEditorActorSubsystem::AlignActors(
|
||||
{First, Middle, Last},
|
||||
EDirectiveUtilActorLayoutAxis::Y,
|
||||
EDirectiveUtilActorAlignment::Minimum);
|
||||
TestEqual(TEXT("Alignment changes two actors"), AlignResult.ChangedActors.Num(), 2);
|
||||
TestTrue(TEXT("Alignment preserves X"), FMath::IsNearlyEqual(Middle->GetActorLocation().X, 30.0f));
|
||||
TestTrue(TEXT("Alignment matches minimum bounds"), FMath::IsNearlyEqual(Middle->GetActorLocation().Y, 0.0f));
|
||||
|
||||
FDirectiveUtilActorOperationResult DistributeResult = UDirectiveUtilEditorActorSubsystem::DistributeActors(
|
||||
{First, Middle, Last},
|
||||
EDirectiveUtilActorLayoutAxis::X,
|
||||
EDirectiveUtilActorDistribution::Centers);
|
||||
TestEqual(TEXT("Distribution changes the middle actor"), DistributeResult.ChangedActors.Num(), 1);
|
||||
TestTrue(TEXT("Center distribution uses equal spacing"), FMath::IsNearlyEqual(Middle->GetActorLocation().X, 50.0f));
|
||||
TestTrue(TEXT("Distribution preserves Y"), FMath::IsNearlyEqual(Middle->GetActorLocation().Y, 0.0f));
|
||||
|
||||
AActor* Floor = SpawnBox(FVector::ZeroVector, FVector(200.0f, 200.0f, 10.0f), ECC_WorldStatic);
|
||||
World->UpdateWorldComponents(true, false);
|
||||
const FVector BeforeInvalidSnap = Middle->GetActorLocation();
|
||||
const FDirectiveUtilActorOperationResult InvalidDistanceResult = UDirectiveUtilEditorActorSubsystem::SnapActorsToSurface(
|
||||
{Middle},
|
||||
FVector::DownVector,
|
||||
std::numeric_limits<float>::quiet_NaN(),
|
||||
ECC_Visibility,
|
||||
EDirectiveUtilSurfacePlacement::Pivot,
|
||||
false);
|
||||
TestTrue(TEXT("Surface snapping skips a non-finite distance"), InvalidDistanceResult.SkippedActors.Contains(Middle));
|
||||
TestTrue(TEXT("A non-finite distance preserves the actor transform"), Middle->GetActorLocation().Equals(BeforeInvalidSnap));
|
||||
|
||||
const FDirectiveUtilActorOperationResult InvalidDirectionResult = UDirectiveUtilEditorActorSubsystem::SnapActorsToSurface(
|
||||
{Middle},
|
||||
FVector(std::numeric_limits<float>::infinity(), 0.0f, -1.0f),
|
||||
500.0f,
|
||||
ECC_Visibility,
|
||||
EDirectiveUtilSurfacePlacement::Pivot,
|
||||
false);
|
||||
TestTrue(TEXT("Surface snapping skips a non-finite direction"), InvalidDirectionResult.SkippedActors.Contains(Middle));
|
||||
|
||||
const FDirectiveUtilActorOperationResult InvalidChannelResult = UDirectiveUtilEditorActorSubsystem::SnapActorsToSurface(
|
||||
{Middle},
|
||||
FVector::DownVector,
|
||||
500.0f,
|
||||
static_cast<ECollisionChannel>(ECC_MAX),
|
||||
EDirectiveUtilSurfacePlacement::Pivot,
|
||||
false);
|
||||
TestTrue(TEXT("Surface snapping skips an invalid collision channel"), InvalidChannelResult.SkippedActors.Contains(Middle));
|
||||
|
||||
FDirectiveUtilActorOperationResult SnapResult = UDirectiveUtilEditorActorSubsystem::SnapActorsToSurface(
|
||||
{Middle},
|
||||
FVector::DownVector,
|
||||
500.0f,
|
||||
ECC_Visibility,
|
||||
EDirectiveUtilSurfacePlacement::Pivot,
|
||||
false);
|
||||
TestEqual(TEXT("Surface snapping changes the actor"), SnapResult.ChangedActors.Num(), 1);
|
||||
TestTrue(TEXT("Pivot snapping reaches the floor surface"), FMath::IsNearlyEqual(Middle->GetActorLocation().Z, 10.0f, 0.1f));
|
||||
|
||||
AActor* BoundsActor = SpawnBox(FVector(150.0f, 150.0f, 100.0f), FVector(10.0f), ECC_WorldDynamic);
|
||||
FDirectiveUtilActorOperationResult BoundsSnapResult = UDirectiveUtilEditorActorSubsystem::SnapActorsToSurface(
|
||||
{BoundsActor},
|
||||
FVector::DownVector,
|
||||
500.0f,
|
||||
ECC_Visibility,
|
||||
EDirectiveUtilSurfacePlacement::Bounds,
|
||||
false);
|
||||
TestEqual(TEXT("Bounds snapping changes the actor"), BoundsSnapResult.ChangedActors.Num(), 1);
|
||||
TestTrue(TEXT("Bounds snapping places the lower bound on the surface"), FMath::IsNearlyEqual(BoundsActor->GetActorLocation().Z, 20.0f, 0.1f));
|
||||
|
||||
AActor* NormalActor = SpawnBox(FVector(-150.0f, -150.0f, 100.0f), FVector(10.0f), ECC_WorldDynamic);
|
||||
NormalActor->SetActorRotation(FRotator(45.0f, 0.0f, 0.0f));
|
||||
FDirectiveUtilActorOperationResult NormalSnapResult = UDirectiveUtilEditorActorSubsystem::SnapActorsToSurface(
|
||||
{NormalActor},
|
||||
FVector::DownVector,
|
||||
500.0f,
|
||||
ECC_Visibility,
|
||||
EDirectiveUtilSurfacePlacement::Pivot,
|
||||
true);
|
||||
TestEqual(TEXT("Normal-aligned snapping changes the actor"), NormalSnapResult.ChangedActors.Num(), 1);
|
||||
TestTrue(TEXT("Normal-aligned snapping points the actor up from the surface"), NormalActor->GetActorUpVector().Equals(FVector::UpVector, 0.01f));
|
||||
|
||||
AActor* MissActor = SpawnBox(FVector(500.0f, 500.0f, 100.0f), FVector(10.0f), ECC_WorldDynamic);
|
||||
FDirectiveUtilActorOperationResult MissResult = UDirectiveUtilEditorActorSubsystem::SnapActorsToSurface(
|
||||
{MissActor},
|
||||
FVector::DownVector,
|
||||
500.0f,
|
||||
ECC_Visibility,
|
||||
EDirectiveUtilSurfacePlacement::Pivot,
|
||||
false);
|
||||
TestTrue(TEXT("Surface snapping reports collision misses as skipped"), MissResult.SkippedActors.Contains(MissActor));
|
||||
|
||||
Floor->Destroy();
|
||||
AActor* Ceiling = SpawnBox(FVector(0.0f, 0.0f, 50.0f), FVector(20.0f, 20.0f, 10.0f), ECC_WorldStatic);
|
||||
World->UpdateWorldComponents(true, false);
|
||||
AActor* ActorWithoutRoot = World->SpawnActor<AActor>();
|
||||
ActorWithoutRoot->ClearFlags(RF_Transient);
|
||||
FDirectiveUtilActorOperationResult FailedMoveResult = UDirectiveUtilEditorActorSubsystem::SnapActorsToSurface(
|
||||
{ActorWithoutRoot},
|
||||
FVector::UpVector,
|
||||
500.0f,
|
||||
ECC_Visibility,
|
||||
EDirectiveUtilSurfacePlacement::Pivot,
|
||||
false);
|
||||
TestTrue(TEXT("Surface snapping reports a failed transform as skipped"), FailedMoveResult.SkippedActors.Contains(ActorWithoutRoot));
|
||||
TestFalse(TEXT("A failed transform is not reported as changed"), FailedMoveResult.ChangedActors.Contains(ActorWithoutRoot));
|
||||
|
||||
AActor* TransientActor = SpawnBox(FVector::ZeroVector, FVector(10.0f), ECC_WorldDynamic);
|
||||
TransientActor->SetFlags(RF_Transient);
|
||||
FDirectiveUtilActorOperationResult InvalidResult = UDirectiveUtilEditorActorSubsystem::AlignActors(
|
||||
{First, TransientActor, nullptr},
|
||||
EDirectiveUtilActorLayoutAxis::X,
|
||||
EDirectiveUtilActorAlignment::Center);
|
||||
TestTrue(TEXT("Transient actors are skipped"), InvalidResult.SkippedActors.Contains(TransientActor));
|
||||
|
||||
Ceiling->Destroy();
|
||||
GEngine->DestroyWorldContext(World);
|
||||
World->DestroyWorld(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,3 +1,5 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
#include "Subsystems/DirectiveUtilEditorActorSubsystem.h"
|
||||
@@ -5,17 +7,23 @@
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "Engine/StaticMesh.h"
|
||||
#include "Engine/StaticMeshActor.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "Components/BoxComponent.h"
|
||||
#include "Components/CapsuleComponent.h"
|
||||
#include "Materials/Material.h"
|
||||
#include "Materials/MaterialExpressionTextureSample.h"
|
||||
#include "Materials/MaterialInterface.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "UObject/Package.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemTest, "DirectiveUtilities.EditorActorSubsystemTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilEditorActorSubsystemTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UDirectiveUtilEditorActorSubsystem::FocusActorsInViewport({nullptr});
|
||||
|
||||
// IsActorWithinBoxBounds should not crash and should return false with null Actor
|
||||
TestFalse("IsActorWithinBoxBounds should return false with null Actor",
|
||||
UDirectiveUtilEditorActorSubsystem::IsActorWithinBoxBounds(nullptr, nullptr));
|
||||
@@ -60,8 +68,8 @@ bool FDirectiveUtilEditorActorSubsystemFilterTest::RunTest(const FString& Parame
|
||||
UStaticMesh* Sphere = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Sphere.Sphere"));
|
||||
if (!Cube || !Sphere)
|
||||
{
|
||||
AddInfo(TEXT("Engine basic shapes unavailable; skipping include/exclude behaviour test."));
|
||||
return true;
|
||||
AddError(TEXT("Engine basic shapes unavailable for the include/exclude behaviour test."));
|
||||
return false;
|
||||
}
|
||||
|
||||
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
|
||||
@@ -126,8 +134,8 @@ bool FDirectiveUtilEditorActorSubsystemFilterCoverageTest::RunTest(const FString
|
||||
UMaterialInterface* MatB = LoadObject<UMaterialInterface>(nullptr, TEXT("/Engine/EngineMaterials/WorldGridMaterial.WorldGridMaterial"));
|
||||
if (!Cube || !Sphere || !MatA || !MatB)
|
||||
{
|
||||
AddInfo(TEXT("Engine basic shapes/materials unavailable; skipping filter coverage test."));
|
||||
return true;
|
||||
AddError(TEXT("Engine basic shapes/materials unavailable for the filter coverage test."));
|
||||
return false;
|
||||
}
|
||||
|
||||
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
|
||||
@@ -257,6 +265,31 @@ bool FDirectiveUtilEditorActorSubsystemFilterCoverageTest::RunTest(const FString
|
||||
TestTrue("ByBounds around A's size => A", RunFilter([&](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByBounds(S, O, Size - FVector(1.0f), Size + FVector(1.0f), Include); }).Contains(ActorA));
|
||||
}
|
||||
|
||||
// Static-mesh bounds use the source mesh dimensions, independently of actor transform.
|
||||
{
|
||||
const FVector MeshSize = Cube->GetBounds().BoxExtent * 2.0f;
|
||||
const TArray<AActor*> Match = RunFilter([&](const TArray<AActor*>& S, TArray<AActor*>& O)
|
||||
{
|
||||
UDirectiveUtilEditorActorSubsystem::FilterActorsByStaticMeshBounds(
|
||||
S, O, MeshSize - FVector(1.0f), MeshSize + FVector(1.0f), Include);
|
||||
});
|
||||
TestTrue("ByStaticMeshBounds includes the cube actor", Match.Contains(ActorA));
|
||||
TestFalse("ByStaticMeshBounds excludes the actor without a mesh", Match.Contains(ActorC));
|
||||
}
|
||||
|
||||
// Override lightmap resolution is a distinct search lane from the mesh default.
|
||||
{
|
||||
CompA->bOverrideLightMapRes = true;
|
||||
CompA->OverriddenLightMapRes = 128;
|
||||
const TArray<AActor*> Match = RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O)
|
||||
{
|
||||
UDirectiveUtilEditorActorSubsystem::FilterActorsByLightmapResolution(
|
||||
S, O, 128, 128, OverrideOnly, Include);
|
||||
});
|
||||
TestTrue("ByLightmapResolution finds the exact override", Match.Contains(ActorA));
|
||||
TestFalse("ByLightmapResolution rejects a different override", Match.Contains(ActorB));
|
||||
}
|
||||
|
||||
// World location: A at origin, B far away.
|
||||
{
|
||||
const TArray<AActor*> Near = RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByWorldLocation(S, O, FVector::ZeroVector, 50.0f, Include); });
|
||||
@@ -269,6 +302,67 @@ bool FDirectiveUtilEditorActorSubsystemFilterCoverageTest::RunTest(const FString
|
||||
TestEqual("ByTextureName(absent) Exclude => all", RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByTextureName(S, O, TEXT("__udcore_absent_texture__"), BaseAndOverride, Exclude); }).Num(), 3);
|
||||
}
|
||||
|
||||
// Texture-reference filtering must follow the same include/exclude contract.
|
||||
{
|
||||
UTexture2D* AbsentTexture = NewObject<UTexture2D>(GetTransientPackage());
|
||||
const TArray<AActor*> Inc = RunFilter([&](const TArray<AActor*>& S, TArray<AActor*>& O)
|
||||
{
|
||||
UDirectiveUtilEditorActorSubsystem::FilterActorsByTexture(S, O, AbsentTexture, BaseAndOverride, Include);
|
||||
});
|
||||
const TArray<AActor*> Exc = RunFilter([&](const TArray<AActor*>& S, TArray<AActor*>& O)
|
||||
{
|
||||
UDirectiveUtilEditorActorSubsystem::FilterActorsByTexture(S, O, AbsentTexture, BaseAndOverride, Exclude);
|
||||
});
|
||||
TestEqual("ByTexture(absent) Include returns none", Inc.Num(), 0);
|
||||
TestEqual("ByTexture(absent) Exclude returns every valid source actor", Exc.Num(), Src.Num());
|
||||
}
|
||||
|
||||
// Missing-resource convenience filters are semantic aliases, not merely spawn-tested nodes.
|
||||
{
|
||||
UStaticMesh* MissingMaterialMesh = DuplicateObject<UStaticMesh>(Cube, GetTransientPackage());
|
||||
MissingMaterialMesh->SetMaterial(0, nullptr);
|
||||
UStaticMeshComponent* MissingMaterialComp = SpawnMesh(MissingMaterialMesh, EComponentMobility::Static);
|
||||
UStaticMeshComponent* MissingMeshComp = SpawnMesh(nullptr, EComponentMobility::Static);
|
||||
const TArray<AActor*> MissingSource = { MissingMaterialComp->GetOwner(), MissingMeshComp->GetOwner(), ActorA };
|
||||
TArray<AActor*> MissingMaterials;
|
||||
UDirectiveUtilEditorActorSubsystem::FilterActorsByMissingMaterials(
|
||||
MissingSource, MissingMaterials, BaseOnly, Include);
|
||||
TestTrue("MissingMaterials finds the null source material", MissingMaterials.Contains(MissingMaterialComp->GetOwner()));
|
||||
TestFalse("MissingMaterials rejects the valid cube material", MissingMaterials.Contains(ActorA));
|
||||
TArray<AActor*> MissingMeshes;
|
||||
UDirectiveUtilEditorActorSubsystem::FilterActorsByMissingStaticMeshes(MissingSource, MissingMeshes, Include);
|
||||
TestTrue("MissingStaticMeshes finds a mesh component with no mesh", MissingMeshes.Contains(MissingMeshComp->GetOwner()));
|
||||
TestFalse("MissingStaticMeshes rejects a valid mesh", MissingMeshes.Contains(ActorA));
|
||||
}
|
||||
|
||||
{
|
||||
UMaterial* MissingTextureMaterial = NewObject<UMaterial>(GetTransientPackage());
|
||||
UMaterialExpressionTextureSample* MissingTextureExpression = NewObject<UMaterialExpressionTextureSample>(MissingTextureMaterial);
|
||||
MissingTextureMaterial->GetExpressionCollection().AddExpression(MissingTextureExpression);
|
||||
CompA->SetMaterial(0, MissingTextureMaterial);
|
||||
const TArray<AActor*> MissingTextures = RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O)
|
||||
{
|
||||
UDirectiveUtilEditorActorSubsystem::FilterActorsByMissingTextures(S, O, OverrideOnly, Include);
|
||||
});
|
||||
TestTrue("Missing texture filter should include an actor with an unset texture expression", MissingTextures.Contains(ActorA));
|
||||
TestFalse("Missing texture filter should exclude actors without unset texture expressions", MissingTextures.Contains(ActorB));
|
||||
CompA->SetMaterial(0, MatA);
|
||||
}
|
||||
|
||||
{
|
||||
AActor* DestroyedActor = World->SpawnActor<AActor>();
|
||||
World->DestroyActor(DestroyedActor);
|
||||
TestFalse("Destroyed actor should be invalid", IsValid(DestroyedActor));
|
||||
TArray<AActor*> InvalidActors = {DestroyedActor};
|
||||
TArray<AActor*> FilteredInvalidActors;
|
||||
UDirectiveUtilEditorActorSubsystem::FilterActorsByTag(
|
||||
InvalidActors,
|
||||
FilteredInvalidActors,
|
||||
TEXT("Absent"),
|
||||
Exclude);
|
||||
TestEqual("Actor filters should skip invalid actors", FilteredInvalidActors.Num(), 0);
|
||||
}
|
||||
|
||||
GEngine->DestroyWorldContext(World);
|
||||
World->DestroyWorld(false);
|
||||
|
||||
@@ -327,10 +421,12 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemQueryAlignmen
|
||||
bool FDirectiveUtilEditorActorSubsystemQueryAlignmentTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UStaticMesh* Cube = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
|
||||
if (!Cube)
|
||||
UMaterialInterface* BasicMaterial = LoadObject<UMaterialInterface>(
|
||||
nullptr, TEXT("/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial"));
|
||||
if (!Cube || !BasicMaterial)
|
||||
{
|
||||
AddInfo(TEXT("Engine basic shapes unavailable; skipping query alignment test."));
|
||||
return true;
|
||||
AddError(TEXT("Engine basic shapes or materials unavailable for the query alignment test."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// The GetActorsBy* queries read from the editor world rather than a passed array.
|
||||
@@ -345,8 +441,8 @@ bool FDirectiveUtilEditorActorSubsystemQueryAlignmentTest::RunTest(const FString
|
||||
}
|
||||
if (!EditorWorld)
|
||||
{
|
||||
AddInfo(TEXT("No editor world available; skipping query alignment test."));
|
||||
return true;
|
||||
AddError(TEXT("No editor world available for the query alignment test."));
|
||||
return false;
|
||||
}
|
||||
|
||||
AActor* MeshActor = EditorWorld->SpawnActor<AActor>();
|
||||
@@ -358,6 +454,7 @@ bool FDirectiveUtilEditorActorSubsystemQueryAlignmentTest::RunTest(const FString
|
||||
MeshComponent->SetMobility(EComponentMobility::Movable);
|
||||
MeshComponent->SetupAttachment(Root);
|
||||
MeshComponent->SetStaticMesh(Cube);
|
||||
MeshComponent->SetMaterial(0, BasicMaterial);
|
||||
MeshComponent->RegisterComponent();
|
||||
MeshActor->AddInstanceComponent(MeshComponent);
|
||||
MeshActor->SetActorLocation(FVector::ZeroVector);
|
||||
@@ -365,6 +462,10 @@ bool FDirectiveUtilEditorActorSubsystemQueryAlignmentTest::RunTest(const FString
|
||||
// The query methods keep no instance state, so a transient instance is enough headless.
|
||||
UDirectiveUtilEditorActorSubsystem* Subsystem = NewObject<UDirectiveUtilEditorActorSubsystem>();
|
||||
|
||||
TArray<AActor*> ByClass;
|
||||
Subsystem->GetActorsByClass(ByClass, AActor::StaticClass(), World, Include);
|
||||
TestTrue("Class: AActor query finds the fixture actor", ByClass.Contains(MeshActor));
|
||||
|
||||
// Bounding box: an enclosing box finds the actor, a disjoint one does not.
|
||||
TArray<AActor*> InBox;
|
||||
Subsystem->GetActorsByBoundingBox(InBox, FVector(-100000.0f), FVector(100000.0f), World, Include);
|
||||
@@ -378,6 +479,81 @@ bool FDirectiveUtilEditorActorSubsystemQueryAlignmentTest::RunTest(const FString
|
||||
Subsystem->GetActorsByStaticMeshName(ByName, TEXT("cub"), World, Include);
|
||||
TestTrue("StaticMeshName: lowercase substring finds the actor", ByName.Contains(MeshActor));
|
||||
|
||||
TArray<AActor*> ByMaterial;
|
||||
Subsystem->GetActorsByMaterial(ByMaterial, BasicMaterial, OverrideOnly, World, Include);
|
||||
TestTrue("Material: generic actor with a static mesh component is included", ByMaterial.Contains(MeshActor));
|
||||
TArray<AActor*> ByMaterialSoftReference;
|
||||
Subsystem->GetActorsByMaterialSoftReference(
|
||||
ByMaterialSoftReference, BasicMaterial, OverrideOnly, World, Include);
|
||||
TestTrue("Material soft reference: generic actor with a static mesh component is included",
|
||||
ByMaterialSoftReference.Contains(MeshActor));
|
||||
TArray<AActor*> ByMaterialName;
|
||||
Subsystem->GetActorsByMaterialName(
|
||||
ByMaterialName, BasicMaterial->GetName(), OverrideOnly, World, Include);
|
||||
TestTrue("Material name: generic actor with a static mesh component is included",
|
||||
ByMaterialName.Contains(MeshActor));
|
||||
TArray<AActor*> WithoutMaterial;
|
||||
Subsystem->GetActorsByMaterial(WithoutMaterial, BasicMaterial, OverrideOnly, World, Exclude);
|
||||
TestFalse("Material exclude: matching generic actor is excluded", WithoutMaterial.Contains(MeshActor));
|
||||
|
||||
const int32 VertexCount = Cube->GetNumVertices(0);
|
||||
TArray<AActor*> ByVertexCount;
|
||||
Subsystem->GetActorsByVertexCount(ByVertexCount, VertexCount, VertexCount, World, Include);
|
||||
TestTrue("VertexCount: exact range finds the fixture actor", ByVertexCount.Contains(MeshActor));
|
||||
const int32 TriangleCount = Cube->GetNumTriangles(0);
|
||||
TArray<AActor*> ByTriangleCount;
|
||||
Subsystem->GetActorsByTriCount(ByTriangleCount, TriangleCount, TriangleCount, World, Include);
|
||||
TestTrue("TriangleCount: exact range finds the fixture actor", ByTriangleCount.Contains(MeshActor));
|
||||
|
||||
const float MeshSize = Cube->GetBoundingBox().GetSize().Size();
|
||||
TArray<AActor*> ByMeshSize;
|
||||
Subsystem->GetActorsByMeshSize(ByMeshSize, MeshSize - 1.0f, MeshSize + 1.0f, World, Include);
|
||||
TestTrue("MeshSize: enclosing range finds the fixture actor", ByMeshSize.Contains(MeshActor));
|
||||
TArray<AActor*> ByWorldLocation;
|
||||
Subsystem->GetActorsByWorldLocation(ByWorldLocation, FVector::ZeroVector, 100.0f, World, Include);
|
||||
TestTrue("WorldLocation: nearby query finds the fixture actor", ByWorldLocation.Contains(MeshActor));
|
||||
|
||||
const int32 LODCount = Cube->GetNumLODs();
|
||||
TArray<AActor*> ByLODCount;
|
||||
Subsystem->GetActorsByLODCount(ByLODCount, LODCount, LODCount, World, Include);
|
||||
TestTrue("LODCount: exact range finds the fixture actor", ByLODCount.Contains(MeshActor));
|
||||
TArray<AActor*> NaniteOff;
|
||||
TArray<AActor*> NaniteOn;
|
||||
Subsystem->GetActorsByNaniteEnabled(NaniteOff, false, World, Include);
|
||||
Subsystem->GetActorsByNaniteEnabled(NaniteOn, true, World, Include);
|
||||
TestTrue("Nanite: fixture belongs to exactly one state", NaniteOff.Contains(MeshActor) != NaniteOn.Contains(MeshActor));
|
||||
|
||||
const int32 SourceLightmapResolution = Cube->GetLightMapResolution();
|
||||
TArray<AActor*> ByLightmapResolution;
|
||||
Subsystem->GetActorsByLightmapResolution(
|
||||
ByLightmapResolution, SourceLightmapResolution, SourceLightmapResolution, World, Include);
|
||||
TestTrue("LightmapResolution: exact source resolution finds the fixture actor", ByLightmapResolution.Contains(MeshActor));
|
||||
TArray<AActor*> ByMobility;
|
||||
Subsystem->GetActorsByMobility(ByMobility, EComponentMobility::Movable, World, Include);
|
||||
TestTrue("Mobility query finds the movable fixture actor", ByMobility.Contains(MeshActor));
|
||||
|
||||
TArray<AActor*> ByStaticMesh;
|
||||
Subsystem->GetActorsByStaticMesh(ByStaticMesh, Cube, World, Include);
|
||||
TestTrue("StaticMesh reference query finds the fixture actor", ByStaticMesh.Contains(MeshActor));
|
||||
TArray<AActor*> ByStaticMeshSoft;
|
||||
Subsystem->GetActorsByStaticMeshSoftReference(ByStaticMeshSoft, Cube, World, Include);
|
||||
TestTrue("StaticMesh soft-reference query finds the fixture actor", ByStaticMeshSoft.Contains(MeshActor));
|
||||
|
||||
UTexture2D* AbsentTexture = NewObject<UTexture2D>(GetTransientPackage());
|
||||
TArray<AActor*> ByTexture;
|
||||
Subsystem->GetActorsByTexture(ByTexture, AbsentTexture, World, Include);
|
||||
TestFalse("Texture reference query rejects an absent texture", ByTexture.Contains(MeshActor));
|
||||
TArray<AActor*> ByTextureSoft;
|
||||
Subsystem->GetActorsByTextureSoftReference(ByTextureSoft, AbsentTexture, World, Include);
|
||||
TestFalse("Texture soft-reference query rejects an absent texture", ByTextureSoft.Contains(MeshActor));
|
||||
TArray<AActor*> ByTextureName;
|
||||
Subsystem->GetActorsByTextureName(ByTextureName, TEXT("__udcore_absent_texture__"), World, Include);
|
||||
TestFalse("Texture-name query rejects an absent texture", ByTextureName.Contains(MeshActor));
|
||||
|
||||
TArray<AActor*> InvalidActors = { MeshActor };
|
||||
Subsystem->GetInvalidActors(InvalidActors);
|
||||
TestEqual("GetInvalidActors resets its deprecated output", InvalidActors.Num(), 0);
|
||||
|
||||
// Mobility: the root component's mobility is what counts.
|
||||
const TArray<AActor*> Source = { MeshActor };
|
||||
TArray<AActor*> MovableActors;
|
||||
@@ -392,6 +568,47 @@ bool FDirectiveUtilEditorActorSubsystemQueryAlignmentTest::RunTest(const FString
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilPushOverrideMaterialsTest, "DirectiveUtilities.PushOverrideMaterialsTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilPushOverrideMaterialsTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UMaterialInterface* BaseMaterial = LoadObject<UMaterialInterface>(
|
||||
nullptr, TEXT("/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial"));
|
||||
UMaterialInterface* OverrideMaterial = LoadObject<UMaterialInterface>(
|
||||
nullptr, TEXT("/Engine/EngineMaterials/WorldGridMaterial.WorldGridMaterial"));
|
||||
UStaticMesh* Cube = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
|
||||
if (!BaseMaterial || !OverrideMaterial || !Cube)
|
||||
{
|
||||
AddError(TEXT("Engine materials unavailable for the override material test."));
|
||||
return false;
|
||||
}
|
||||
|
||||
UStaticMesh* StaticMesh = DuplicateObject<UStaticMesh>(Cube, GetTransientPackage());
|
||||
StaticMesh->GetStaticMaterials()[0].MaterialInterface = BaseMaterial;
|
||||
UStaticMeshComponent* StaticMeshComponent = NewObject<UStaticMeshComponent>(GetTransientPackage());
|
||||
StaticMeshComponent->SetStaticMesh(StaticMesh);
|
||||
StaticMeshComponent->SetMaterial(0, OverrideMaterial);
|
||||
|
||||
UDirectiveUtilEditorActorSubsystem::PushOverrideMaterialsToSource(StaticMeshComponent);
|
||||
TestEqual("Override material is copied to its source slot", StaticMesh->GetMaterial(0), OverrideMaterial);
|
||||
|
||||
UStaticMesh* MeshWithoutOverride = DuplicateObject<UStaticMesh>(Cube, GetTransientPackage());
|
||||
MeshWithoutOverride->GetStaticMaterials()[0].MaterialInterface = BaseMaterial;
|
||||
UStaticMeshComponent* ComponentWithoutOverride = NewObject<UStaticMeshComponent>(GetTransientPackage());
|
||||
ComponentWithoutOverride->SetStaticMesh(MeshWithoutOverride);
|
||||
UDirectiveUtilEditorActorSubsystem::PushOverrideMaterialsToSource(ComponentWithoutOverride);
|
||||
TestEqual("A source slot is unchanged when the component has no override",
|
||||
MeshWithoutOverride->GetMaterial(0), BaseMaterial);
|
||||
|
||||
AddExpectedError(TEXT("Static Mesh Component is invalid."), EAutomationExpectedErrorFlags::Exact, 1);
|
||||
UDirectiveUtilEditorActorSubsystem::PushOverrideMaterialsToSource(nullptr);
|
||||
UStaticMeshComponent* ComponentWithoutMesh = NewObject<UStaticMeshComponent>(GetTransientPackage());
|
||||
AddExpectedError(TEXT("Static Mesh Component has no valid static mesh."), EAutomationExpectedErrorFlags::Exact, 1);
|
||||
UDirectiveUtilEditorActorSubsystem::PushOverrideMaterialsToSource(ComponentWithoutMesh);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemBoundsTest, "DirectiveUtilities.EditorActorSubsystemBoundsTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilEditorActorSubsystemBoundsTest::RunTest(const FString& Parameters)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
#include "AssetRegistry/IAssetRegistry.h"
|
||||
#include "Libraries/DirectiveUtilEditorAssetAuditLibrary.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilEditorAssetAuditLibraryTest,
|
||||
"DirectiveUtilities.EditorAssetAuditLibraryTests",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilEditorAssetAuditLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
FDirectiveUtilAssetAuditOptions Options;
|
||||
Options.PackagePaths = {TEXT("/Engine/BasicShapes")};
|
||||
|
||||
const FDirectiveUtilAssetAuditReport Report = UDirectiveUtilEditorAssetAuditLibrary::BuildAssetAuditReport(Options);
|
||||
TestTrue(TEXT("The engine shape scan returns assets"), !Report.Assets.IsEmpty());
|
||||
if (const IAssetRegistry* AssetRegistry = IAssetRegistry::Get())
|
||||
{
|
||||
TestTrue(TEXT("An asset audit starts the initial registry scan"), AssetRegistry->IsSearchAllAssets());
|
||||
}
|
||||
|
||||
for (const FDirectiveUtilAssetAuditEntry& Entry : Report.Assets)
|
||||
{
|
||||
TestTrue(TEXT("Report assets have valid data"), Entry.Asset.IsValid());
|
||||
TestTrue(TEXT("Report assets have a package"), !Entry.PackageName.IsNone());
|
||||
TestTrue(TEXT("Report assets have a class"), !Entry.AssetClass.IsEmpty());
|
||||
TestTrue(TEXT("Report counts are non-negative"), Entry.DependencyCount >= 0 && Entry.ReferencerCount >= 0);
|
||||
}
|
||||
|
||||
const FString Csv = UDirectiveUtilEditorAssetAuditLibrary::AssetAuditReportToCsv(Report);
|
||||
TestTrue(TEXT("CSV includes its header"), Csv.StartsWith(TEXT("Asset,Package,Path,Class")));
|
||||
TestTrue(TEXT("CSV includes scanned assets"), Csv.Contains(TEXT("/Engine/BasicShapes")));
|
||||
|
||||
Options.ExcludedPackagePaths = {TEXT("/Engine/BasicShapes")};
|
||||
TestTrue(
|
||||
TEXT("Excluded paths return no candidates"),
|
||||
UDirectiveUtilEditorAssetAuditLibrary::FindUnreferencedAssetCandidates(Options).IsEmpty());
|
||||
TestTrue(
|
||||
TEXT("Excluded paths return no missing references"),
|
||||
UDirectiveUtilEditorAssetAuditLibrary::FindMissingAssetReferences(Options).IsEmpty());
|
||||
TestTrue(
|
||||
TEXT("Excluded paths return no cycles"),
|
||||
UDirectiveUtilEditorAssetAuditLibrary::FindAssetDependencyCycles(Options).IsEmpty());
|
||||
|
||||
FDirectiveUtilAssetAuditOptions EngineOptions;
|
||||
EngineOptions.PackagePaths = {TEXT("/Engine")};
|
||||
const FDirectiveUtilAssetAuditReport EngineReport = UDirectiveUtilEditorAssetAuditLibrary::BuildAssetAuditReport(EngineOptions);
|
||||
TestTrue(TEXT("A full engine audit returns more than the basic shapes scan"), EngineReport.Assets.Num() > Report.Assets.Num());
|
||||
for (const FDirectiveUtilAssetDependencyCycle& Cycle : EngineReport.DependencyCycles)
|
||||
{
|
||||
TestTrue(TEXT("Dependency cycles contain at least one package"), !Cycle.Packages.IsEmpty());
|
||||
for (int32 Index = 1; Index < Cycle.Packages.Num(); ++Index)
|
||||
{
|
||||
TestTrue(TEXT("Dependency cycle packages are sorted"), Cycle.Packages[Index - 1].LexicalLess(Cycle.Packages[Index]));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,8 +1,18 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
#include "AssetRegistry/AssetData.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "Editor.h"
|
||||
#include "Libraries/DirectiveUtilEditorAssetLibrary.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "Misc/Guid.h"
|
||||
#include "ObjectTools.h"
|
||||
#include "Subsystems/EditorAssetSubsystem.h"
|
||||
#include "Tests/DirectiveUtilTestObject.h"
|
||||
#include "UObject/ObjectRedirector.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorAssetLibraryTest, "DirectiveUtilities.EditorAssetLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
@@ -45,6 +55,78 @@ bool FDirectiveUtilEditorAssetLibraryTest::RunTest(const FString& Parameters)
|
||||
TestEqual("FindDuplicateAssets should return each asset path once", UniquePaths.Num(), Pair.Value.DuplicateAssetPaths.Num());
|
||||
}
|
||||
|
||||
int32 RedirectorsProcessed = INDEX_NONE;
|
||||
TestEqual("FixUpRedirectorsInPaths succeeds when no redirectors match",
|
||||
UDirectiveUtilEditorAssetLibrary::FixUpRedirectorsInPaths(
|
||||
{TEXT("/Game/DirectiveUtilitiesTests/NoRedirectors")}, RedirectorsProcessed),
|
||||
EDirectiveUtilSuccessStatus::Success);
|
||||
TestEqual("No redirectors are reported for an empty path", RedirectorsProcessed, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilFixUpRedirectorsTest, "DirectiveUtilities.FixUpRedirectorsTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilFixUpRedirectorsTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UEditorAssetSubsystem* EditorAssetSubsystem = GEditor
|
||||
? GEditor->GetEditorSubsystem<UEditorAssetSubsystem>()
|
||||
: nullptr;
|
||||
if (!EditorAssetSubsystem)
|
||||
{
|
||||
AddError(TEXT("Editor asset subsystem unavailable for the redirector test."));
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString RootPath = FString::Printf(
|
||||
TEXT("/Game/DirectiveUtilitiesTests/Redirectors_%s"),
|
||||
*FGuid::NewGuid().ToString(EGuidFormats::Digits));
|
||||
const FString OriginalName = TEXT("OriginalAsset");
|
||||
const FString OriginalPackageName = RootPath / OriginalName;
|
||||
UPackage* Package = CreatePackage(*OriginalPackageName);
|
||||
UDirectiveUtilTestObject* Asset = NewObject<UDirectiveUtilTestObject>(
|
||||
Package, *OriginalName, RF_Public | RF_Standalone);
|
||||
FAssetRegistryModule::AssetCreated(Asset);
|
||||
|
||||
const FString RenamedAssetName = TEXT("RenamedAsset");
|
||||
ObjectTools::FPackageGroupName PackageGroupName;
|
||||
PackageGroupName.PackageName = RootPath / RenamedAssetName;
|
||||
PackageGroupName.ObjectName = RenamedAssetName;
|
||||
TSet<UPackage*> RefusedPackages;
|
||||
FText RenameError;
|
||||
if (!ObjectTools::RenameSingleObject(
|
||||
Asset, PackageGroupName, RefusedPackages, RenameError, nullptr, true))
|
||||
{
|
||||
EditorAssetSubsystem->DeleteDirectory(RootPath);
|
||||
AddError(FString::Printf(TEXT("Failed to rename the redirector test asset: %s"), *RenameError.ToString()));
|
||||
return false;
|
||||
}
|
||||
|
||||
const FString OriginalObjectPath = OriginalPackageName + TEXT(".") + OriginalName;
|
||||
UObjectRedirector* Redirector = FindObject<UObjectRedirector>(nullptr, *OriginalObjectPath);
|
||||
if (!Redirector)
|
||||
{
|
||||
EditorAssetSubsystem->DeleteDirectory(RootPath);
|
||||
AddError(TEXT("Renaming the test asset did not create a redirector."));
|
||||
return false;
|
||||
}
|
||||
TestNotNull("Renaming an asset creates a redirector", Redirector);
|
||||
|
||||
AddExpectedMessagePlain(
|
||||
TEXT("Redirector fix-up requires an interactive editor session."),
|
||||
ELogVerbosity::Warning,
|
||||
EAutomationExpectedMessageFlags::Contains,
|
||||
1);
|
||||
int32 RedirectorsProcessed = INDEX_NONE;
|
||||
const EDirectiveUtilSuccessStatus Status = UDirectiveUtilEditorAssetLibrary::FixUpRedirectorsInPaths(
|
||||
{RootPath}, RedirectorsProcessed);
|
||||
TestEqual("Redirector fix-up fails closed in unattended runs", Status, EDirectiveUtilSuccessStatus::Failure);
|
||||
TestEqual("No redirector is reported as processed after a guarded run", RedirectorsProcessed, 0);
|
||||
TestTrue("Guarded fix-up leaves the redirector intact", EditorAssetSubsystem->DoesAssetExist(OriginalObjectPath));
|
||||
TestTrue("Renamed asset remains available", EditorAssetSubsystem->DoesAssetExist(
|
||||
RootPath / RenamedAssetName + TEXT(".") + RenamedAssetName));
|
||||
|
||||
TestTrue("Redirector test assets are removed", EditorAssetSubsystem->DeleteDirectory(RootPath));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "Libraries/DirectiveUtilEditorBlueprintLibrary.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "Tests/DirectiveUtilTestObject.h"
|
||||
#include "EdGraphSchema_K2.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "Engine/SCS_Node.h"
|
||||
#include "Engine/SimpleConstructionScript.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "Kismet2/KismetEditorUtilities.h"
|
||||
#include "Misc/Guid.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilEditorBlueprintLibraryTest,
|
||||
"DirectiveUtilities.EditorBlueprintLibraryTests",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilEditorBlueprintLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
auto ContainsBlueprint = [](const TArray<FAssetData>& Assets, const UBlueprint* Expected) {
|
||||
return Assets.ContainsByPredicate([Expected](const FAssetData& Asset) {
|
||||
return Asset.GetAsset() == Expected;
|
||||
});
|
||||
};
|
||||
|
||||
TestEqual(
|
||||
TEXT("A null Blueprint has unknown status"),
|
||||
UDirectiveUtilEditorBlueprintLibrary::GetBlueprintCompileStatus(nullptr),
|
||||
EDirectiveUtilBlueprintCompileStatus::Unknown);
|
||||
|
||||
UBlueprint* Blueprint = NewObject<UBlueprint>();
|
||||
const TArray<TPair<EBlueprintStatus, EDirectiveUtilBlueprintCompileStatus>> Statuses = {
|
||||
{BS_Unknown, EDirectiveUtilBlueprintCompileStatus::Unknown},
|
||||
{BS_Dirty, EDirectiveUtilBlueprintCompileStatus::Dirty},
|
||||
{BS_Error, EDirectiveUtilBlueprintCompileStatus::Error},
|
||||
{BS_UpToDate, EDirectiveUtilBlueprintCompileStatus::UpToDate},
|
||||
{BS_BeingCreated, EDirectiveUtilBlueprintCompileStatus::BeingCreated},
|
||||
{BS_UpToDateWithWarnings, EDirectiveUtilBlueprintCompileStatus::UpToDateWithWarnings},
|
||||
};
|
||||
for (const TPair<EBlueprintStatus, EDirectiveUtilBlueprintCompileStatus>& Status : Statuses)
|
||||
{
|
||||
Blueprint->Status = Status.Key;
|
||||
TestEqual(
|
||||
TEXT("Blueprint status maps to the public enum"),
|
||||
UDirectiveUtilEditorBlueprintLibrary::GetBlueprintCompileStatus(Blueprint),
|
||||
Status.Value);
|
||||
}
|
||||
|
||||
FDirectiveUtilBlueprintSearchOptions Options;
|
||||
Options.PackagePaths = {TEXT("/Engine/BasicShapes")};
|
||||
TestTrue(
|
||||
TEXT("A folder without Blueprints has no compile-status matches"),
|
||||
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsByCompileStatus(
|
||||
EDirectiveUtilBlueprintCompileStatus::UpToDate,
|
||||
Options).IsEmpty());
|
||||
TestTrue(
|
||||
TEXT("A null parent class returns no matches"),
|
||||
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsByParentClass(nullptr, Options).IsEmpty());
|
||||
TestTrue(
|
||||
TEXT("A folder without Blueprints has no parent-class matches"),
|
||||
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsByParentClass(AActor::StaticClass(), Options).IsEmpty());
|
||||
TestTrue(
|
||||
TEXT("A non-interface class returns no interface matches"),
|
||||
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsImplementingInterface(UObject::StaticClass(), Options).IsEmpty());
|
||||
TestTrue(
|
||||
TEXT("A folder without Blueprints has no interface matches"),
|
||||
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsImplementingInterface(UInterface::StaticClass(), Options).IsEmpty());
|
||||
TestTrue(
|
||||
TEXT("A non-component class returns no component matches"),
|
||||
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsContainingComponentClass(UObject::StaticClass(), Options).IsEmpty());
|
||||
TestTrue(
|
||||
TEXT("A folder without Blueprints has no component matches"),
|
||||
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsContainingComponentClass(UActorComponent::StaticClass(), Options).IsEmpty());
|
||||
TestTrue(
|
||||
TEXT("A Blueprint without generated properties has no unused variables"),
|
||||
UDirectiveUtilEditorBlueprintLibrary::GetUnusedBlueprintVariables(Blueprint).IsEmpty());
|
||||
|
||||
const FName FixtureName(*FString::Printf(
|
||||
TEXT("BP_InspectionFixture_%s"),
|
||||
*FGuid::NewGuid().ToString(EGuidFormats::Digits)));
|
||||
const FString FixturePackageName = FString::Printf(
|
||||
TEXT("/Game/DirectiveUtilitiesTests/%s"),
|
||||
*FixtureName.ToString());
|
||||
UPackage* TestPackage = CreatePackage(*FixturePackageName);
|
||||
UBlueprint* InspectionBlueprint = FKismetEditorUtilities::CreateBlueprint(
|
||||
AActor::StaticClass(),
|
||||
TestPackage,
|
||||
FixtureName,
|
||||
BPTYPE_Normal,
|
||||
TEXT("DirectiveUtilities.EditorBlueprintLibraryTests"));
|
||||
if (!TestNotNull(TEXT("The inspection fixture Blueprint is created"), InspectionBlueprint))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TestTrue(
|
||||
TEXT("The inspection fixture implements its test interface"),
|
||||
FBlueprintEditorUtils::ImplementNewInterface(
|
||||
InspectionBlueprint,
|
||||
UDirectiveUtilTestInterface::StaticClass()->GetClassPathName()));
|
||||
USCS_Node* ComponentNode = InspectionBlueprint->SimpleConstructionScript->CreateNode(
|
||||
UStaticMeshComponent::StaticClass(),
|
||||
TEXT("InspectionComponent"));
|
||||
InspectionBlueprint->SimpleConstructionScript->AddNode(ComponentNode);
|
||||
FEdGraphPinType VariableType;
|
||||
VariableType.PinCategory = UEdGraphSchema_K2::PC_Int;
|
||||
TestTrue(
|
||||
TEXT("The inspection fixture adds an unused variable"),
|
||||
FBlueprintEditorUtils::AddMemberVariable(InspectionBlueprint, TEXT("UnusedValue"), VariableType));
|
||||
FBlueprintEditorUtils::SetBlueprintVariableMetaData(
|
||||
InspectionBlueprint,
|
||||
TEXT("UnusedValue"),
|
||||
nullptr,
|
||||
FBlueprintMetadata::MD_Private,
|
||||
TEXT("true"));
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(InspectionBlueprint);
|
||||
FKismetEditorUtilities::CompileBlueprint(InspectionBlueprint);
|
||||
FAssetRegistryModule::AssetCreated(InspectionBlueprint);
|
||||
|
||||
FDirectiveUtilBlueprintSearchOptions FixtureOptions;
|
||||
FixtureOptions.PackagePaths = {TEXT("/Game/DirectiveUtilitiesTests")};
|
||||
TestTrue(
|
||||
TEXT("Compile-status search finds the fixture"),
|
||||
ContainsBlueprint(
|
||||
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsByCompileStatus(
|
||||
EDirectiveUtilBlueprintCompileStatus::UpToDate,
|
||||
FixtureOptions),
|
||||
InspectionBlueprint));
|
||||
TestTrue(
|
||||
TEXT("Direct-parent search finds the fixture"),
|
||||
ContainsBlueprint(
|
||||
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsByParentClass(
|
||||
AActor::StaticClass(),
|
||||
FixtureOptions,
|
||||
false),
|
||||
InspectionBlueprint));
|
||||
TestTrue(
|
||||
TEXT("Descendant-parent search finds the fixture"),
|
||||
ContainsBlueprint(
|
||||
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsByParentClass(
|
||||
AActor::StaticClass(),
|
||||
FixtureOptions,
|
||||
true),
|
||||
InspectionBlueprint));
|
||||
TestTrue(
|
||||
TEXT("Interface search finds the fixture"),
|
||||
ContainsBlueprint(
|
||||
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsImplementingInterface(
|
||||
UDirectiveUtilTestInterface::StaticClass(),
|
||||
FixtureOptions),
|
||||
InspectionBlueprint));
|
||||
TestTrue(
|
||||
TEXT("Exact component search finds the fixture"),
|
||||
ContainsBlueprint(
|
||||
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsContainingComponentClass(
|
||||
UStaticMeshComponent::StaticClass(),
|
||||
FixtureOptions,
|
||||
false),
|
||||
InspectionBlueprint));
|
||||
TestTrue(
|
||||
TEXT("Derived component search finds the fixture"),
|
||||
ContainsBlueprint(
|
||||
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsContainingComponentClass(
|
||||
UActorComponent::StaticClass(),
|
||||
FixtureOptions,
|
||||
true),
|
||||
InspectionBlueprint));
|
||||
TestTrue(
|
||||
TEXT("Unused-variable search finds the fixture variable"),
|
||||
UDirectiveUtilEditorBlueprintLibrary::GetUnusedBlueprintVariables(InspectionBlueprint).Contains(TEXT("UnusedValue")));
|
||||
|
||||
FixtureOptions.ExcludedPackagePaths = {TEXT("/Game/DirectiveUtilitiesTests")};
|
||||
TestTrue(
|
||||
TEXT("Excluded paths remove the fixture from search"),
|
||||
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsByParentClass(
|
||||
AActor::StaticClass(),
|
||||
FixtureOptions).IsEmpty());
|
||||
|
||||
FAssetRegistryModule::AssetDeleted(InspectionBlueprint);
|
||||
TestPackage->SetDirtyFlag(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilEditorTaskLibrary.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "Tasks/DirectiveUtilEditorSlowTask.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilEditorTaskLibraryTest,
|
||||
"DirectiveUtilities.EditorTaskLibraryTests",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilEditorTaskLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
TestNull("StartEditorSlowTask should reject zero work", UDirectiveUtilEditorTaskLibrary::StartEditorSlowTask(0.0f, FText::GetEmpty()));
|
||||
TestNull(
|
||||
"StartEditorSlowTask should reject NaN work",
|
||||
UDirectiveUtilEditorTaskLibrary::StartEditorSlowTask(
|
||||
std::numeric_limits<float>::quiet_NaN(),
|
||||
FText::GetEmpty()));
|
||||
TestNull(
|
||||
"StartEditorSlowTask should reject infinite work",
|
||||
UDirectiveUtilEditorTaskLibrary::StartEditorSlowTask(
|
||||
std::numeric_limits<float>::infinity(),
|
||||
FText::GetEmpty()));
|
||||
UDirectiveUtilEditorSlowTask* PublicTask = UDirectiveUtilEditorTaskLibrary::StartEditorSlowTask(
|
||||
1.0f,
|
||||
FText::FromString(TEXT("Public task")));
|
||||
TestNotNull("StartEditorSlowTask should create a task for positive work", PublicTask);
|
||||
if (PublicTask)
|
||||
{
|
||||
TestNull(
|
||||
"StartEditorSlowTask should reject a nested task",
|
||||
UDirectiveUtilEditorTaskLibrary::StartEditorSlowTask(
|
||||
1.0f,
|
||||
FText::FromString(TEXT("Nested task"))));
|
||||
PublicTask->Finish();
|
||||
}
|
||||
|
||||
UDirectiveUtilEditorSlowTask* Task = NewObject<UDirectiveUtilEditorSlowTask>();
|
||||
Task->Initialize(2.0f, FText::FromString(TEXT("Test task")), false, false);
|
||||
TestTrue("Slow task should be active after initialization", Task->IsActive());
|
||||
TestTrue("Advance should accept non-negative work", Task->Advance(1.0f, FText::FromString(TEXT("Step"))));
|
||||
TestFalse("Advance should reject negative work", Task->Advance(-1.0f, FText::GetEmpty()));
|
||||
TestFalse("Advance should reject NaN work", Task->Advance(std::numeric_limits<float>::quiet_NaN(), FText::GetEmpty()));
|
||||
TestFalse("Advance should reject infinite work", Task->Advance(std::numeric_limits<float>::infinity(), FText::GetEmpty()));
|
||||
TestFalse("A non-cancellable task should not report cancellation", Task->IsCancelRequested());
|
||||
Task->Finish();
|
||||
TestFalse("Slow task should be inactive after Finish", Task->IsActive());
|
||||
TestFalse("Advance should fail after Finish", Task->Advance(1.0f, FText::GetEmpty()));
|
||||
Task->Initialize(std::numeric_limits<float>::quiet_NaN(), FText::GetEmpty(), false, false);
|
||||
TestFalse("Initialize should leave a task inactive for NaN work", Task->IsActive());
|
||||
Task->Initialize(std::numeric_limits<float>::infinity(), FText::GetEmpty(), false, false);
|
||||
TestFalse("Initialize should leave a task inactive for infinite work", Task->IsActive());
|
||||
|
||||
UDirectiveUtilEditorSlowTask* FirstTask = NewObject<UDirectiveUtilEditorSlowTask>();
|
||||
UDirectiveUtilEditorSlowTask* SecondTask = NewObject<UDirectiveUtilEditorSlowTask>();
|
||||
FirstTask->Initialize(1.0f, FText::FromString(TEXT("First task")), false, false);
|
||||
SecondTask->Initialize(1.0f, FText::FromString(TEXT("Second task")), false, false);
|
||||
TestTrue("The first slow task should remain active", FirstTask->IsActive());
|
||||
TestFalse("A concurrent slow task should remain inactive", SecondTask->IsActive());
|
||||
FirstTask->Finish();
|
||||
SecondTask->Initialize(1.0f, FText::FromString(TEXT("Second task")), false, false);
|
||||
TestTrue("A slow task should start after the active task finishes", SecondTask->IsActive());
|
||||
SecondTask->Finish();
|
||||
|
||||
TWeakObjectPtr<UDirectiveUtilEditorSlowTask> AbandonedTask = NewObject<UDirectiveUtilEditorSlowTask>();
|
||||
AbandonedTask->Initialize(1.0f, FText::FromString(TEXT("Abandoned task")), false, false);
|
||||
TestTrue("An unfinished slow task should be active before collection", AbandonedTask->IsActive());
|
||||
CollectGarbage(RF_NoFlags);
|
||||
TestTrue("An unfinished slow task should stay alive until Finish", AbandonedTask.IsValid());
|
||||
if (AbandonedTask.IsValid())
|
||||
{
|
||||
AbandonedTask->Finish();
|
||||
}
|
||||
CollectGarbage(RF_NoFlags);
|
||||
TestFalse("A finished slow task should be collectable", AbandonedTask.IsValid());
|
||||
UDirectiveUtilEditorSlowTask* TaskAfterCollection = NewObject<UDirectiveUtilEditorSlowTask>();
|
||||
TaskAfterCollection->Initialize(1.0f, FText::FromString(TEXT("Task after collection")), false, false);
|
||||
TestTrue("A slow task should start after the previous task is collected", TaskAfterCollection->IsActive());
|
||||
TaskAfterCollection->Finish();
|
||||
|
||||
TestTrue(
|
||||
"ShowEditorNotification should create a notification while Slate is active",
|
||||
UDirectiveUtilEditorTaskLibrary::ShowEditorNotification(
|
||||
FText::FromString(TEXT("Directive Utilities test")),
|
||||
EDirectiveUtilEditorNotificationState::Success,
|
||||
0.01f));
|
||||
TestTrue(
|
||||
"ShowEditorNotification should normalize a non-finite duration",
|
||||
UDirectiveUtilEditorTaskLibrary::ShowEditorNotification(
|
||||
FText::FromString(TEXT("Directive Utilities duration test")),
|
||||
EDirectiveUtilEditorNotificationState::Neutral,
|
||||
std::numeric_limits<float>::quiet_NaN()));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,41 +1,244 @@
|
||||
#include "Libraries/DirectiveUtilFunctionLibrary.h"
|
||||
#include "Tests/AutomationCommon.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilFunctionLibraryTest, "DirectiveUtilities.FunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
#include "Libraries/DirectiveUtilFunctionLibrary.h"
|
||||
#include "Async/Async.h"
|
||||
#include "Engine/World.h"
|
||||
#include "HAL/PlatformProcess.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "Misc/App.h"
|
||||
#include "UObject/Package.h"
|
||||
|
||||
namespace DirectiveUtilFunctionLibraryTest
|
||||
{
|
||||
EDirectiveUtilBuildConfiguration GetExpectedBuildConfiguration(const EBuildConfiguration BuildConfiguration)
|
||||
{
|
||||
switch (BuildConfiguration)
|
||||
{
|
||||
case EBuildConfiguration::Unknown:
|
||||
return EDirectiveUtilBuildConfiguration::Unknown;
|
||||
case EBuildConfiguration::Debug:
|
||||
return EDirectiveUtilBuildConfiguration::Debug;
|
||||
case EBuildConfiguration::DebugGame:
|
||||
return EDirectiveUtilBuildConfiguration::DebugGame;
|
||||
case EBuildConfiguration::Development:
|
||||
return EDirectiveUtilBuildConfiguration::Development;
|
||||
case EBuildConfiguration::Shipping:
|
||||
return EDirectiveUtilBuildConfiguration::Shipping;
|
||||
case EBuildConfiguration::Test:
|
||||
return EDirectiveUtilBuildConfiguration::Test;
|
||||
}
|
||||
|
||||
return EDirectiveUtilBuildConfiguration::Unknown;
|
||||
}
|
||||
|
||||
EDirectiveUtilBuildTargetType GetExpectedBuildTargetType(const EBuildTargetType BuildTargetType)
|
||||
{
|
||||
switch (BuildTargetType)
|
||||
{
|
||||
case EBuildTargetType::Unknown:
|
||||
return EDirectiveUtilBuildTargetType::Unknown;
|
||||
case EBuildTargetType::Game:
|
||||
return EDirectiveUtilBuildTargetType::Game;
|
||||
case EBuildTargetType::Server:
|
||||
return EDirectiveUtilBuildTargetType::Server;
|
||||
case EBuildTargetType::Client:
|
||||
return EDirectiveUtilBuildTargetType::Client;
|
||||
case EBuildTargetType::Editor:
|
||||
return EDirectiveUtilBuildTargetType::Editor;
|
||||
case EBuildTargetType::Program:
|
||||
return EDirectiveUtilBuildTargetType::Program;
|
||||
}
|
||||
|
||||
return EDirectiveUtilBuildTargetType::Unknown;
|
||||
}
|
||||
|
||||
enum class EClipboardStage : uint8
|
||||
{
|
||||
CopyText,
|
||||
CheckText,
|
||||
CheckString,
|
||||
CheckClear,
|
||||
Complete
|
||||
};
|
||||
|
||||
class FClipboardRoundTripCommand : public IAutomationLatentCommand
|
||||
{
|
||||
public:
|
||||
FClipboardRoundTripCommand(FAutomationTestBase* InTest, FString InOriginalClipboard)
|
||||
: Test(InTest)
|
||||
, OriginalClipboard(MoveTemp(InOriginalClipboard))
|
||||
{
|
||||
}
|
||||
|
||||
virtual bool Update() override
|
||||
{
|
||||
switch (Stage)
|
||||
{
|
||||
case EClipboardStage::CopyText:
|
||||
UDirectiveUtilFunctionLibrary::CopyTextToClipboard(FText::FromString(TextPayload));
|
||||
BeginRetryWindow(EClipboardStage::CheckText);
|
||||
return false;
|
||||
|
||||
case EClipboardStage::CheckText:
|
||||
if (!HasExpectedValue(UDirectiveUtilFunctionLibrary::GetTextFromClipboard().ToString(), TextPayload, TEXT("GetTextFromClipboard should return the copied text")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
UDirectiveUtilFunctionLibrary::CopyStringToClipboard(StringPayload);
|
||||
BeginRetryWindow(EClipboardStage::CheckString);
|
||||
return false;
|
||||
|
||||
case EClipboardStage::CheckString:
|
||||
if (!HasExpectedValue(UDirectiveUtilFunctionLibrary::GetStringFromClipboard(), StringPayload, TEXT("GetStringFromClipboard should return the copied string")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
UDirectiveUtilFunctionLibrary::ClearClipboard();
|
||||
BeginRetryWindow(EClipboardStage::CheckClear);
|
||||
return false;
|
||||
|
||||
case EClipboardStage::CheckClear:
|
||||
if (!HasExpectedValue(UDirectiveUtilFunctionLibrary::GetStringFromClipboard(), FString(), TEXT("GetStringFromClipboard should return an empty string after clearing the clipboard")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
UDirectiveUtilFunctionLibrary::CopyStringToClipboard(OriginalClipboard);
|
||||
Stage = EClipboardStage::Complete;
|
||||
return true;
|
||||
|
||||
case EClipboardStage::Complete:
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
void BeginRetryWindow(EClipboardStage NextStage)
|
||||
{
|
||||
Stage = NextStage;
|
||||
FramesRemaining = 120;
|
||||
}
|
||||
|
||||
bool HasExpectedValue(const FString& Actual, const FString& Expected, const TCHAR* FailureMessage)
|
||||
{
|
||||
if (Actual == Expected)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (--FramesRemaining > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Test->TestEqual(FailureMessage, Actual, Expected);
|
||||
return true;
|
||||
}
|
||||
|
||||
FAutomationTestBase* Test;
|
||||
FString OriginalClipboard;
|
||||
EClipboardStage Stage = EClipboardStage::CopyText;
|
||||
int32 FramesRemaining = 0;
|
||||
const FString TextPayload = TEXT("Directive Utilities Text Clipboard");
|
||||
const FString StringPayload = TEXT("Directive Utilities String Clipboard");
|
||||
};
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilFunctionLibraryTest, "DirectiveUtilities.FunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// Preserve the user/CI clipboard so this test doesn't destroy it, and restore it before returning.
|
||||
const FString OriginalClipboard = UDirectiveUtilFunctionLibrary::GetStringFromClipboard();
|
||||
|
||||
const FText TestText = FText::FromString(TEXT("Hello, Clipboard!"));
|
||||
UDirectiveUtilFunctionLibrary::CopyTextToClipboard(TestText);
|
||||
const FText ClipboardText = UDirectiveUtilFunctionLibrary::GetTextFromClipboard();
|
||||
TestEqual("GetTextFromClipboard should return the copied text", ClipboardText.ToString(), TestText.ToString());
|
||||
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FEngineWaitLatentCommand(1.0f));
|
||||
|
||||
const FString TestString = TEXT("Hello, Clipboard!");
|
||||
UDirectiveUtilFunctionLibrary::CopyStringToClipboard(TestString);
|
||||
const FString ClipboardString = UDirectiveUtilFunctionLibrary::GetStringFromClipboard();
|
||||
TestEqual("GetStringFromClipboard should return the copied string", ClipboardString, TestString);
|
||||
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FEngineWaitLatentCommand(1.0f));
|
||||
|
||||
UDirectiveUtilFunctionLibrary::ClearClipboard();
|
||||
const FString ClearedClipboardString = UDirectiveUtilFunctionLibrary::GetStringFromClipboard();
|
||||
TestEqual("GetStringFromClipboard should return an empty string after clearing the clipboard", ClearedClipboardString, TEXT(""));
|
||||
ADD_LATENT_AUTOMATION_COMMAND(DirectiveUtilFunctionLibraryTest::FClipboardRoundTripCommand(this, OriginalClipboard));
|
||||
|
||||
const FString ProjectVersion = UDirectiveUtilFunctionLibrary::GetProjectVersion();
|
||||
TestNotEqual("GetProjectVersion should return a non-empty string", ProjectVersion, FString(""));
|
||||
|
||||
TestTrue("IsRunningInEditor should return true in editor context",
|
||||
UDirectiveUtilFunctionLibrary::IsRunningInEditor());
|
||||
#if WITH_EDITOR
|
||||
constexpr bool bExpectedEditorContext = true;
|
||||
#else
|
||||
constexpr bool bExpectedEditorContext = false;
|
||||
#endif
|
||||
TestEqual(
|
||||
"IsRunningInEditor should match the target context",
|
||||
UDirectiveUtilFunctionLibrary::IsRunningInEditor(),
|
||||
bExpectedEditorContext);
|
||||
|
||||
// Pure pass-through to the engine's GetDerivedClasses (which appends to the output array).
|
||||
struct FWorldTypeCase
|
||||
{
|
||||
EWorldType::Type WorldType;
|
||||
EDirectiveUtilWorldType Expected;
|
||||
};
|
||||
|
||||
const FWorldTypeCase WorldTypeCases[] = {
|
||||
{ EWorldType::None, EDirectiveUtilWorldType::None },
|
||||
{ EWorldType::Game, EDirectiveUtilWorldType::Game },
|
||||
{ EWorldType::Editor, EDirectiveUtilWorldType::Editor },
|
||||
{ EWorldType::PIE, EDirectiveUtilWorldType::PlayInEditor },
|
||||
{ EWorldType::EditorPreview, EDirectiveUtilWorldType::EditorPreview },
|
||||
{ EWorldType::GamePreview, EDirectiveUtilWorldType::GamePreview },
|
||||
{ EWorldType::GameRPC, EDirectiveUtilWorldType::GameRPC },
|
||||
{ EWorldType::Inactive, EDirectiveUtilWorldType::Inactive }
|
||||
};
|
||||
|
||||
UWorld* TestWorld = NewObject<UWorld>();
|
||||
TestNotNull("A transient world should be created for world type tests", TestWorld);
|
||||
if (TestWorld)
|
||||
{
|
||||
for (const FWorldTypeCase& TestCase : WorldTypeCases)
|
||||
{
|
||||
TestWorld->WorldType = TestCase.WorldType;
|
||||
TestEqual(
|
||||
FString::Printf(TEXT("GetWorldType should map %s"), LexToString(TestCase.WorldType)),
|
||||
UDirectiveUtilFunctionLibrary::GetWorldType(TestWorld),
|
||||
TestCase.Expected);
|
||||
}
|
||||
|
||||
TestWorld->WorldType = static_cast<EWorldType::Type>(MAX_uint8);
|
||||
TestEqual(
|
||||
"GetWorldType should reject unsupported world types",
|
||||
UDirectiveUtilFunctionLibrary::GetWorldType(TestWorld),
|
||||
EDirectiveUtilWorldType::Unknown);
|
||||
}
|
||||
|
||||
TestEqual(
|
||||
"GetWorldType should return Unknown for a null context",
|
||||
UDirectiveUtilFunctionLibrary::GetWorldType(nullptr),
|
||||
EDirectiveUtilWorldType::Unknown);
|
||||
TestEqual(
|
||||
"GetWorldType should return Unknown when the context has no world",
|
||||
UDirectiveUtilFunctionLibrary::GetWorldType(GetTransientPackage()),
|
||||
EDirectiveUtilWorldType::Unknown);
|
||||
|
||||
TestEqual(
|
||||
"GetBuildConfigurationType should match the application build configuration",
|
||||
UDirectiveUtilFunctionLibrary::GetBuildConfigurationType(),
|
||||
DirectiveUtilFunctionLibraryTest::GetExpectedBuildConfiguration(FApp::GetBuildConfiguration()));
|
||||
TestEqual(
|
||||
"GetBuildTargetType should match the application build target",
|
||||
UDirectiveUtilFunctionLibrary::GetBuildTargetType(),
|
||||
DirectiveUtilFunctionLibraryTest::GetExpectedBuildTargetType(FApp::GetBuildTargetType()));
|
||||
|
||||
TestNotNull("The world type enum should be reflected", StaticEnum<EDirectiveUtilWorldType>());
|
||||
TestNotNull("The build configuration enum should be reflected", StaticEnum<EDirectiveUtilBuildConfiguration>());
|
||||
TestNotNull("The build target type enum should be reflected", StaticEnum<EDirectiveUtilBuildTargetType>());
|
||||
|
||||
const UFunction* GetWorldTypeFunction = UDirectiveUtilFunctionLibrary::StaticClass()->FindFunctionByName(
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilFunctionLibrary, GetWorldType));
|
||||
TestNotNull("GetWorldType should be reflected", GetWorldTypeFunction);
|
||||
if (GetWorldTypeFunction)
|
||||
{
|
||||
TestTrue("GetWorldType should be Blueprint pure", GetWorldTypeFunction->HasAnyFunctionFlags(FUNC_BlueprintPure));
|
||||
TestFalse("GetWorldType should be available at runtime", GetWorldTypeFunction->HasAnyFunctionFlags(FUNC_EditorOnly));
|
||||
#if WITH_EDITOR
|
||||
TestEqual(
|
||||
"GetWorldType should use its input as the Blueprint world context",
|
||||
GetWorldTypeFunction->GetMetaData(TEXT("WorldContext")),
|
||||
FString(TEXT("WorldContextObject")));
|
||||
#endif
|
||||
}
|
||||
|
||||
// Happy path (recursive): the running module guarantees a stable hierarchy to query.
|
||||
TArray<UClass*> RecursiveDerived;
|
||||
UDirectiveUtilFunctionLibrary::GetChildClasses(UBlueprintFunctionLibrary::StaticClass(), true, RecursiveDerived);
|
||||
TestTrue("GetChildClasses should return a non-empty list for a base class with subclasses",
|
||||
@@ -45,19 +248,121 @@ bool FDirectiveUtilFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
TestFalse("GetChildClasses should not include the base class itself",
|
||||
RecursiveDerived.Contains(UBlueprintFunctionLibrary::StaticClass()));
|
||||
|
||||
// Non-recursive results must not exceed the recursive results for the same base.
|
||||
double ElapsedMilliseconds = 123.0;
|
||||
TestFalse(
|
||||
"StartStopwatch should reject NAME_None",
|
||||
UDirectiveUtilFunctionLibrary::StartStopwatch(NAME_None));
|
||||
TestFalse(
|
||||
"StopStopwatch should reject NAME_None",
|
||||
UDirectiveUtilFunctionLibrary::StopStopwatch(NAME_None, ElapsedMilliseconds));
|
||||
TestEqual("StopStopwatch should reset elapsed time for NAME_None", ElapsedMilliseconds, 0.0);
|
||||
|
||||
const FName MissingStopwatchKey(TEXT("DirectiveUtilMissingStopwatch"));
|
||||
ElapsedMilliseconds = 123.0;
|
||||
TestFalse(
|
||||
"StopStopwatch should reject a missing key",
|
||||
UDirectiveUtilFunctionLibrary::StopStopwatch(MissingStopwatchKey, ElapsedMilliseconds));
|
||||
TestEqual("StopStopwatch should reset elapsed time for a missing key", ElapsedMilliseconds, 0.0);
|
||||
|
||||
const FName BasicStopwatchKey(TEXT("DirectiveUtilBasicStopwatch"));
|
||||
TestTrue(
|
||||
"StartStopwatch should start an unused key",
|
||||
UDirectiveUtilFunctionLibrary::StartStopwatch(BasicStopwatchKey));
|
||||
TestFalse(
|
||||
"StartStopwatch should preserve an active key by default",
|
||||
UDirectiveUtilFunctionLibrary::StartStopwatch(BasicStopwatchKey));
|
||||
TestTrue(
|
||||
"StopStopwatch should stop an active key",
|
||||
UDirectiveUtilFunctionLibrary::StopStopwatch(BasicStopwatchKey, ElapsedMilliseconds));
|
||||
TestTrue("StopStopwatch should return a non-negative duration", ElapsedMilliseconds >= 0.0);
|
||||
TestFalse(
|
||||
"StopStopwatch should consume the active key",
|
||||
UDirectiveUtilFunctionLibrary::StopStopwatch(BasicStopwatchKey, ElapsedMilliseconds));
|
||||
|
||||
const FName RestartStopwatchKey(TEXT("DirectiveUtilRestartStopwatch"));
|
||||
TestTrue(
|
||||
"StartStopwatch should start the restart test key",
|
||||
UDirectiveUtilFunctionLibrary::StartStopwatch(RestartStopwatchKey));
|
||||
TestTrue(
|
||||
"StartStopwatch should replace an active key when requested",
|
||||
UDirectiveUtilFunctionLibrary::StartStopwatch(RestartStopwatchKey, true));
|
||||
TestTrue(
|
||||
"StopStopwatch should stop a restarted key",
|
||||
UDirectiveUtilFunctionLibrary::StopStopwatch(RestartStopwatchKey, ElapsedMilliseconds));
|
||||
|
||||
const FName FirstOverlapKey(TEXT("DirectiveUtilFirstOverlapStopwatch"));
|
||||
const FName SecondOverlapKey(TEXT("DirectiveUtilSecondOverlapStopwatch"));
|
||||
TestTrue(
|
||||
"StartStopwatch should start the first overlapping key",
|
||||
UDirectiveUtilFunctionLibrary::StartStopwatch(FirstOverlapKey));
|
||||
TestTrue(
|
||||
"StartStopwatch should start the second overlapping key",
|
||||
UDirectiveUtilFunctionLibrary::StartStopwatch(SecondOverlapKey));
|
||||
TestTrue(
|
||||
"StopStopwatch should stop the first overlapping key out of order",
|
||||
UDirectiveUtilFunctionLibrary::StopStopwatch(FirstOverlapKey, ElapsedMilliseconds));
|
||||
TestTrue(
|
||||
"StopStopwatch should stop the second overlapping key",
|
||||
UDirectiveUtilFunctionLibrary::StopStopwatch(SecondOverlapKey, ElapsedMilliseconds));
|
||||
|
||||
const FName TimedStopwatchKey(TEXT("DirectiveUtilTimedStopwatch"));
|
||||
TestTrue(
|
||||
"StartStopwatch should start the elapsed-time test key",
|
||||
UDirectiveUtilFunctionLibrary::StartStopwatch(TimedStopwatchKey));
|
||||
FPlatformProcess::SleepNoStats(0.01f);
|
||||
TestTrue(
|
||||
"StopStopwatch should stop the elapsed-time test key",
|
||||
UDirectiveUtilFunctionLibrary::StopStopwatch(TimedStopwatchKey, ElapsedMilliseconds));
|
||||
TestTrue("StopStopwatch should measure elapsed real time in milliseconds", ElapsedMilliseconds >= 5.0);
|
||||
|
||||
const FName CrossThreadStopwatchKey(TEXT("DirectiveUtilCrossThreadStopwatch"));
|
||||
TestTrue(
|
||||
"StartStopwatch should start a key that will stop on another thread",
|
||||
UDirectiveUtilFunctionLibrary::StartStopwatch(CrossThreadStopwatchKey));
|
||||
double CrossThreadElapsedMilliseconds = 0.0;
|
||||
TFuture<bool> CrossThreadStop = Async(EAsyncExecution::ThreadPool, [&CrossThreadElapsedMilliseconds, CrossThreadStopwatchKey]()
|
||||
{
|
||||
return UDirectiveUtilFunctionLibrary::StopStopwatch(
|
||||
CrossThreadStopwatchKey,
|
||||
CrossThreadElapsedMilliseconds);
|
||||
});
|
||||
TestTrue("StopStopwatch should find a key started on another thread", CrossThreadStop.Get());
|
||||
TestTrue("Cross-thread stopwatch duration should be non-negative", CrossThreadElapsedMilliseconds >= 0.0);
|
||||
|
||||
const UFunction* StartStopwatchFunction = UDirectiveUtilFunctionLibrary::StaticClass()->FindFunctionByName(
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilFunctionLibrary, StartStopwatch));
|
||||
const UFunction* StopStopwatchFunction = UDirectiveUtilFunctionLibrary::StaticClass()->FindFunctionByName(
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilFunctionLibrary, StopStopwatch));
|
||||
TestNotNull("StartStopwatch should be reflected", StartStopwatchFunction);
|
||||
TestNotNull("StopStopwatch should be reflected", StopStopwatchFunction);
|
||||
for (const UFunction* StopwatchFunction : { StartStopwatchFunction, StopStopwatchFunction })
|
||||
{
|
||||
if (!StopwatchFunction)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TestTrue("Stopwatch functions should be Blueprint callable", StopwatchFunction->HasAnyFunctionFlags(FUNC_BlueprintCallable));
|
||||
TestFalse("Stopwatch functions should not be Blueprint pure", StopwatchFunction->HasAnyFunctionFlags(FUNC_BlueprintPure));
|
||||
TestFalse("Stopwatch functions should be available at runtime", StopwatchFunction->HasAnyFunctionFlags(FUNC_EditorOnly));
|
||||
#if WITH_EDITOR
|
||||
TestEqual(
|
||||
"Stopwatch functions should use the profiling category",
|
||||
StopwatchFunction->GetMetaData(TEXT("Category")),
|
||||
FString(TEXT("Directive Utilities|Utility|Profiling")));
|
||||
#endif
|
||||
}
|
||||
|
||||
TArray<UClass*> NonRecursiveDerived;
|
||||
UDirectiveUtilFunctionLibrary::GetChildClasses(UBlueprintFunctionLibrary::StaticClass(), false, NonRecursiveDerived);
|
||||
TestTrue("GetChildClasses non-recursive count should not exceed recursive count",
|
||||
NonRecursiveDerived.Num() <= RecursiveDerived.Num());
|
||||
|
||||
// Leaf class with no subclasses: an empty input array should remain empty.
|
||||
TArray<UClass*> LeafDerived;
|
||||
UDirectiveUtilFunctionLibrary::GetChildClasses(UDirectiveUtilFunctionLibrary::StaticClass(), true, LeafDerived);
|
||||
TestEqual("GetChildClasses should return an empty list for a class with no subclasses",
|
||||
LeafDerived.Num(), 0);
|
||||
|
||||
// Null base class: should not crash and should add nothing.
|
||||
TArray<UClass*> NullBaseDerived;
|
||||
UDirectiveUtilFunctionLibrary::GetChildClasses(nullptr, true, NullBaseDerived);
|
||||
TestEqual("GetChildClasses should return an empty list for a null base class",
|
||||
@@ -87,11 +392,9 @@ bool FDirectiveUtilFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
TestFalse("GetCommandLineOption should return false for an empty key",
|
||||
UDirectiveUtilFunctionLibrary::GetCommandLineOption(CommandLine, TEXT(""), Value));
|
||||
|
||||
// Smoke test through the process-command-line path.
|
||||
TestFalse("HasCommandLineSwitch should not find a switch that was never passed",
|
||||
UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(TEXT("DirectiveUtilitiesDefinitelyNotPassed")));
|
||||
}
|
||||
|
||||
UDirectiveUtilFunctionLibrary::CopyStringToClipboard(OriginalClipboard);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilGameplayTagFunctionLibrary.h"
|
||||
#include "GameplayTagsManager.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilGameplayTagFunctionLibraryTest, "DirectiveUtilities.GameplayTagFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilGameplayTagFunctionLibraryTest, "DirectiveUtilities.GameplayTagFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilGameplayTagFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
@@ -109,5 +111,23 @@ bool FDirectiveUtilGameplayTagFunctionLibraryTest::RunTest(const FString& Parame
|
||||
TestEqual("FindRegisteredTags with no match should be empty", UDirectiveUtilGameplayTagFunctionLibrary::FindRegisteredTags(TEXT("DirectiveUtilitiesNoSuchTagXYZ")).Num(), 0);
|
||||
TestEqual("FindRegisteredTags with an empty substring should be empty", UDirectiveUtilGameplayTagFunctionLibrary::FindRegisteredTags(TEXT("")).Num(), 0);
|
||||
|
||||
#if WITH_EDITOR
|
||||
const TArray<FName> RegistryQueryFunctions = {
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilGameplayTagFunctionLibrary, GetTagParents),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilGameplayTagFunctionLibrary, GetTagChildren),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilGameplayTagFunctionLibrary, GetTagDirectChildren),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilGameplayTagFunctionLibrary, IsLeafTag)
|
||||
};
|
||||
for (const FName FunctionName : RegistryQueryFunctions)
|
||||
{
|
||||
const UFunction* Function = UDirectiveUtilGameplayTagFunctionLibrary::StaticClass()->FindFunctionByName(FunctionName);
|
||||
TestNotNull(*FString::Printf(TEXT("%s should be reflected"), *FunctionName.ToString()), Function);
|
||||
if (Function)
|
||||
{
|
||||
TestFalse(*FString::Printf(TEXT("%s should not advertise worker-thread safety"), *FunctionName.ToString()), Function->HasMetaData(TEXT("BlueprintThreadSafe")));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "Types/DirectiveUtilInputTypes.h"
|
||||
#include "Types/DirectiveUtilTypes.h"
|
||||
#include "InputMappingContext.h"
|
||||
#include "EnhancedPlayerInput.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Engine/LocalPlayer.h"
|
||||
@@ -12,34 +13,24 @@
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
/**
|
||||
* Exercises the Enhanced Input library's active paths against a real EnhancedInput subsystem,
|
||||
* which requires a live local player. A standalone game instance + local player is built for this;
|
||||
* if that cannot be created in the headless harness the test skips its assertions rather than failing.
|
||||
*/
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilInputActivePathTest, "DirectiveUtilities.InputActivePathTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilInputActivePathTest, "DirectiveUtilities.InputActivePathTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilInputActivePathTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// These warnings only fire on the graceful-skip paths; allow (but never require) them.
|
||||
AddExpectedMessagePlain(TEXT("LocalPlayer not found. Cannot set input mapping contexts."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
AddExpectedMessagePlain(TEXT("EnhancedInput subsystem not found."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
UGameInstance* GameInstance = NewObject<UGameInstance>(GEngine);
|
||||
if (!GameInstance)
|
||||
{
|
||||
AddInfo(TEXT("Could not create a game instance; skipping Enhanced Input active-path assertions."));
|
||||
return true;
|
||||
AddError(TEXT("Could not create a game instance for Enhanced Input active-path assertions."));
|
||||
return false;
|
||||
}
|
||||
GameInstance->AddToRoot();
|
||||
GameInstance->InitializeStandalone();
|
||||
|
||||
UWorld* World = GameInstance->GetWorld();
|
||||
|
||||
// CreateLocalPlayer ensures when there is no game viewport client (headless), so build the local
|
||||
// player directly via AddLocalPlayer, which initializes the local-player subsystems without one.
|
||||
UClass* LocalPlayerClass = GEngine->LocalPlayerClass ? GEngine->LocalPlayerClass.Get() : ULocalPlayer::StaticClass();
|
||||
ULocalPlayer* LocalPlayer = NewObject<ULocalPlayer>(GEngine, LocalPlayerClass);
|
||||
if (World && LocalPlayer)
|
||||
@@ -49,33 +40,40 @@ bool FDirectiveUtilInputActivePathTest::RunTest(const FString& Parameters)
|
||||
|
||||
if (!World || !LocalPlayer)
|
||||
{
|
||||
AddInfo(TEXT("Local player unavailable in the headless harness; skipping Enhanced Input active-path assertions."));
|
||||
AddError(TEXT("Local player unavailable in the Enhanced Input test harness."));
|
||||
GameInstance->Shutdown();
|
||||
GameInstance->RemoveFromRoot();
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
APlayerController* PlayerController = World->SpawnActor<APlayerController>();
|
||||
if (!PlayerController)
|
||||
{
|
||||
AddInfo(TEXT("Could not spawn a player controller; skipping Enhanced Input active-path assertions."));
|
||||
AddError(TEXT("Could not spawn a player controller for Enhanced Input active-path assertions."));
|
||||
GameInstance->Shutdown();
|
||||
GameInstance->RemoveFromRoot();
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
PlayerController->SetPlayer(LocalPlayer);
|
||||
PlayerController->InitInputSystem();
|
||||
PlayerController->PlayerInput = NewObject<UEnhancedPlayerInput>(PlayerController);
|
||||
|
||||
UEnhancedInputLocalPlayerSubsystem* Subsystem = UDirectiveUtilInputFunctionLibrary::GetEnhancedInputSubsystem(PlayerController);
|
||||
if (!Subsystem)
|
||||
{
|
||||
AddInfo(TEXT("Enhanced Input subsystem unavailable for the synthetic local player; skipping active-path assertions."));
|
||||
AddError(TEXT("Enhanced Input subsystem unavailable for the synthetic local player."));
|
||||
GameInstance->Shutdown();
|
||||
GameInstance->RemoveFromRoot();
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// We have a live subsystem: exercise the active add/active/swap/remove/clear paths for real.
|
||||
TestNotNull("GetEnhancedInputSubsystem returns the subsystem for a live local player", Subsystem);
|
||||
auto ApplyPendingMappings = [Subsystem]
|
||||
{
|
||||
FModifyContextOptions Options;
|
||||
Options.bForceImmediately = true;
|
||||
Subsystem->RequestRebuildControlMappings(Options);
|
||||
};
|
||||
|
||||
UInputMappingContext* ContextA = NewObject<UInputMappingContext>(GetTransientPackage());
|
||||
UInputMappingContext* ContextB = NewObject<UInputMappingContext>(GetTransientPackage());
|
||||
@@ -84,7 +82,6 @@ bool FDirectiveUtilInputActivePathTest::RunTest(const FString& Parameters)
|
||||
const TSoftObjectPtr<UInputMappingContext> SoftA(ContextA);
|
||||
const TSoftObjectPtr<UInputMappingContext> SoftB(ContextB);
|
||||
|
||||
// Add context A.
|
||||
FDirectiveUtilEnhancedInputContextData DataA;
|
||||
DataA.InputContext = SoftA;
|
||||
DataA.Priority = 0;
|
||||
@@ -93,38 +90,47 @@ bool FDirectiveUtilInputActivePathTest::RunTest(const FString& Parameters)
|
||||
TestEqual("AddInputMappingContexts succeeds for a live controller",
|
||||
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(PlayerController, ToAdd, false),
|
||||
EDirectiveUtilSuccessStatus::Success);
|
||||
ApplyPendingMappings();
|
||||
TestTrue("Added context is reported active",
|
||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftA));
|
||||
|
||||
// Swap A -> B.
|
||||
TestEqual("SwapInputMappingContexts succeeds",
|
||||
UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(PlayerController, SoftA, SoftB, 0, false),
|
||||
EDirectiveUtilSuccessStatus::Success);
|
||||
ApplyPendingMappings();
|
||||
TestFalse("Swapped-out context is inactive",
|
||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftA));
|
||||
TestTrue("Swapped-in context is active",
|
||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftB));
|
||||
|
||||
// Remove B.
|
||||
TArray<TSoftObjectPtr<UInputMappingContext>> ToRemove;
|
||||
ToRemove.Add(SoftB);
|
||||
TestEqual("RemoveInputMappingContexts succeeds",
|
||||
UDirectiveUtilInputFunctionLibrary::RemoveInputMappingContexts(PlayerController, ToRemove),
|
||||
EDirectiveUtilSuccessStatus::Success);
|
||||
ApplyPendingMappings();
|
||||
TestFalse("Removed context is inactive",
|
||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftB));
|
||||
TestEqual("Swap adds the new context when the previous context is inactive",
|
||||
UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(PlayerController, SoftA, SoftB, 7, true),
|
||||
EDirectiveUtilSuccessStatus::Success);
|
||||
ApplyPendingMappings();
|
||||
TestTrue("Fallback swap context is active",
|
||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftB));
|
||||
UDirectiveUtilInputFunctionLibrary::RemoveInputMappingContexts(PlayerController, {SoftB});
|
||||
ApplyPendingMappings();
|
||||
|
||||
// Clear all (after re-adding A).
|
||||
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(PlayerController, ToAdd, false);
|
||||
TestEqual("ClearAllInputMappingContexts succeeds",
|
||||
UDirectiveUtilInputFunctionLibrary::ClearAllInputMappingContexts(PlayerController),
|
||||
EDirectiveUtilSuccessStatus::Success);
|
||||
ApplyPendingMappings();
|
||||
TestFalse("Context is inactive after clear-all",
|
||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftA));
|
||||
|
||||
// A clearing add whose every context fails to load must leave the existing mappings untouched.
|
||||
AddExpectedMessagePlain(TEXT("Input Mapping Contexts failed to load and were not added!"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
AddExpectedMessagePlain(TEXT("input mapping contexts could not be loaded"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(PlayerController, ToAdd, false);
|
||||
ApplyPendingMappings();
|
||||
FDirectiveUtilEnhancedInputContextData UnresolvableData;
|
||||
UnresolvableData.Priority = 0;
|
||||
TArray<FDirectiveUtilEnhancedInputContextData> UnresolvableToAdd;
|
||||
@@ -142,5 +148,3 @@ bool FDirectiveUtilInputActivePathTest::RunTest(const FString& Parameters)
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // WITH_EDITOR
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilInputFunctionLibrary.h"
|
||||
#include "Types/DirectiveUtilTypes.h"
|
||||
#include "InputMappingContext.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilInputFunctionLibraryTest, "DirectiveUtilities.InputFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilInputFunctionLibraryTest, "DirectiveUtilities.InputFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilInputFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// The null-controller and invalid-context paths intentionally log warnings. Register them as
|
||||
// expected (plain match, negative count = consume if present, never required) so the run is clean.
|
||||
AddExpectedMessagePlain(TEXT("PlayerController is null. Cannot set input mapping contexts."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
AddExpectedMessagePlain(TEXT("Both the previous and new input mapping contexts must be valid."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
// AddInputMappingContexts should return Failure with null controller
|
||||
TArray<FDirectiveUtilEnhancedInputContextData> Contexts;
|
||||
@@ -39,8 +40,6 @@ bool FDirectiveUtilInputFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
UDirectiveUtilInputFunctionLibrary::RemoveInputMappingContexts(nullptr, EmptyRemoveContexts),
|
||||
EDirectiveUtilSuccessStatus::Failure);
|
||||
|
||||
// SwapInputMappingContexts loads both contexts before using the controller, so invalid
|
||||
// (unset) soft pointers must return Failure regardless of the (null) controller.
|
||||
const TSoftObjectPtr<UInputMappingContext> NullContext;
|
||||
TestEqual("SwapInputMappingContexts should fail when both contexts are invalid",
|
||||
UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(nullptr, NullContext, NullContext, 0, false),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilMapFunctionLibrary.h"
|
||||
#include "Tests/DirectiveUtilTestObject.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMapFunctionLibraryTest, "DirectiveUtilities.MapFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMapFunctionLibraryTest, "DirectiveUtilities.MapFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilMapFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
@@ -144,5 +146,11 @@ bool FDirectiveUtilMapFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
TestEqual("Append with null or mismatched properties should preserve the value", *UntouchedValue, 10);
|
||||
}
|
||||
|
||||
TestObject->TestMap = {{1, 10}, {2, 20}};
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_Append(&TestObject->TestMap, MapProperty, &TestObject->TestMap, MapProperty, true);
|
||||
TestEqual("Append with the same map should preserve its size", TestObject->TestMap.Num(), 2);
|
||||
TestEqual("Append with the same map should preserve the first value", TestObject->TestMap.FindRef(1), 10);
|
||||
TestEqual("Append with the same map should preserve the second value", TestObject->TestMap.FindRef(2), 20);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphNode.h"
|
||||
#include "EdGraph/EdGraphPin.h"
|
||||
#include "EdGraphSchema_K2.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "K2Node_CallFunction.h"
|
||||
#include "K2Node_TemporaryVariable.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "Kismet2/CompilerResultsLog.h"
|
||||
#include "Libraries/DirectiveUtilMapFunctionLibrary.h"
|
||||
#include "Nodes/DirectiveUtilMapNodeMigration.h"
|
||||
#include "Nodes/K2Node_DirectiveUtilMapAppend.h"
|
||||
#include "Tests/DirectiveUtilTestObject.h"
|
||||
|
||||
namespace DirectiveUtilMapNodeTest
|
||||
{
|
||||
struct FDependentPin
|
||||
{
|
||||
FName Name;
|
||||
FName Category;
|
||||
EPinContainerType ContainerType;
|
||||
};
|
||||
|
||||
struct FMapNodeCase
|
||||
{
|
||||
FName FunctionName;
|
||||
TArray<FDependentPin> DependentPins;
|
||||
};
|
||||
|
||||
FEdGraphPinType MakeMapType(
|
||||
const FName KeyCategory,
|
||||
const FName ValueCategory,
|
||||
UObject* KeySubCategoryObject = nullptr,
|
||||
UObject* ValueSubCategoryObject = nullptr)
|
||||
{
|
||||
FEdGraphPinType PinType;
|
||||
PinType.PinCategory = KeyCategory;
|
||||
PinType.PinSubCategoryObject = KeySubCategoryObject;
|
||||
PinType.PinValueType.TerminalCategory = ValueCategory;
|
||||
PinType.PinValueType.TerminalSubCategoryObject = ValueSubCategoryObject;
|
||||
PinType.ContainerType = EPinContainerType::Map;
|
||||
return PinType;
|
||||
}
|
||||
|
||||
UEdGraph* MakeGraph()
|
||||
{
|
||||
UBlueprint* Blueprint = NewObject<UBlueprint>();
|
||||
Blueprint->GeneratedClass = UObject::StaticClass();
|
||||
Blueprint->SkeletonGeneratedClass = UObject::StaticClass();
|
||||
UEdGraph* Graph = NewObject<UEdGraph>(Blueprint);
|
||||
Graph->Schema = UEdGraphSchema_K2::StaticClass();
|
||||
Blueprint->UbergraphPages.Add(Graph);
|
||||
return Graph;
|
||||
}
|
||||
|
||||
UEdGraphPin* AddMapOutput(UEdGraph& Graph, const FName Name, const FEdGraphPinType& PinType)
|
||||
{
|
||||
UK2Node_TemporaryVariable* SourceNode = NewObject<UK2Node_TemporaryVariable>(&Graph);
|
||||
SourceNode->VariableType = PinType;
|
||||
Graph.AddNode(SourceNode);
|
||||
SourceNode->AllocateDefaultPins();
|
||||
UEdGraphPin* OutputPin = SourceNode->GetVariablePin();
|
||||
OutputPin->PinName = Name;
|
||||
return OutputPin;
|
||||
}
|
||||
|
||||
UK2Node_CallFunction* AddFunctionNode(UEdGraph& Graph, const FName FunctionName)
|
||||
{
|
||||
UK2Node_CallFunction* Node = NewObject<UK2Node_CallFunction>(&Graph);
|
||||
Graph.AddNode(Node);
|
||||
const UFunction* Function = UDirectiveUtilMapFunctionLibrary::StaticClass()->FindFunctionByName(FunctionName);
|
||||
Node->FunctionReference.SetExternalMember(FunctionName, UDirectiveUtilMapFunctionLibrary::StaticClass());
|
||||
Node->bDefaultsToPureFunc = Function && Function->HasAnyFunctionFlags(FUNC_BlueprintPure);
|
||||
Node->AllocateDefaultPins();
|
||||
return Node;
|
||||
}
|
||||
|
||||
void Connect(UEdGraphPin& OutputPin, UK2Node& Node, UEdGraphPin& InputPin)
|
||||
{
|
||||
OutputPin.MakeLinkTo(&InputPin);
|
||||
Node.PinConnectionListChanged(&InputPin);
|
||||
}
|
||||
|
||||
void Disconnect(UEdGraphPin& OutputPin, UK2Node& Node, UEdGraphPin& InputPin)
|
||||
{
|
||||
OutputPin.BreakLinkTo(&InputPin);
|
||||
Node.PinConnectionListChanged(&InputPin);
|
||||
}
|
||||
|
||||
bool HasMapType(const UEdGraphPin& Pin, const FName KeyCategory, const FName ValueCategory)
|
||||
{
|
||||
return Pin.PinType.IsMap()
|
||||
&& Pin.PinType.PinCategory == KeyCategory
|
||||
&& Pin.PinType.PinValueType.TerminalCategory == ValueCategory;
|
||||
}
|
||||
|
||||
bool HasMapType(const UEdGraphPin& Pin, const FEdGraphPinType& ExpectedType)
|
||||
{
|
||||
return Pin.PinType.IsMap()
|
||||
&& Pin.PinType.PinCategory == ExpectedType.PinCategory
|
||||
&& Pin.PinType.PinSubCategory == ExpectedType.PinSubCategory
|
||||
&& Pin.PinType.PinSubCategoryObject == ExpectedType.PinSubCategoryObject
|
||||
&& Pin.PinType.PinValueType == ExpectedType.PinValueType;
|
||||
}
|
||||
|
||||
bool HasElementType(
|
||||
const UEdGraphPin& Pin,
|
||||
const FName Category,
|
||||
UObject* SubCategoryObject,
|
||||
const EPinContainerType ContainerType)
|
||||
{
|
||||
return Pin.PinType.PinCategory == Category
|
||||
&& Pin.PinType.PinSubCategoryObject == SubCategoryObject
|
||||
&& Pin.PinType.ContainerType == ContainerType;
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilMapNodeWildcardTest,
|
||||
"DirectiveUtilities.MapNodeWildcardTests",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilMapNodeWildcardTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
using namespace DirectiveUtilMapNodeTest;
|
||||
|
||||
const TArray<FMapNodeCase> NodeCases = {
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMapFunctionLibrary, Map_FindOrAdd), {{TEXT("Key"), UEdGraphSchema_K2::PC_String, EPinContainerType::None}, {TEXT("Value"), UEdGraphSchema_K2::PC_Int, EPinContainerType::None}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMapFunctionLibrary, Map_ClearValues), {}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMapFunctionLibrary, Map_GetKeysByValue), {{TEXT("Value"), UEdGraphSchema_K2::PC_Int, EPinContainerType::None}, {TEXT("Keys"), UEdGraphSchema_K2::PC_String, EPinContainerType::Array}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMapFunctionLibrary, Map_HasValue), {{TEXT("Value"), UEdGraphSchema_K2::PC_Int, EPinContainerType::None}}},
|
||||
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMapFunctionLibrary, Map_RemoveKeys), {{TEXT("Keys"), UEdGraphSchema_K2::PC_String, EPinContainerType::Array}}},
|
||||
};
|
||||
|
||||
const FEdGraphPinType StringIntMap = MakeMapType(UEdGraphSchema_K2::PC_String, UEdGraphSchema_K2::PC_Int);
|
||||
for (const FMapNodeCase& NodeCase : NodeCases)
|
||||
{
|
||||
UEdGraph* Graph = MakeGraph();
|
||||
UK2Node_CallFunction* Node = AddFunctionNode(*Graph, NodeCase.FunctionName);
|
||||
UEdGraphPin* TargetMap = Node->FindPin(TEXT("TargetMap"));
|
||||
TestNotNull(*FString::Printf(TEXT("%s should have a TargetMap pin"), *NodeCase.FunctionName.ToString()), TargetMap);
|
||||
if (!TargetMap)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TestTrue(*FString::Printf(TEXT("%s should begin as a wildcard map"), *NodeCase.FunctionName.ToString()), HasMapType(*TargetMap, UEdGraphSchema_K2::PC_Wildcard, UEdGraphSchema_K2::PC_Wildcard));
|
||||
UEdGraphPin* MapOutput = AddMapOutput(*Graph, TEXT("MapOutput"), StringIntMap);
|
||||
Connect(*MapOutput, *Node, *TargetMap);
|
||||
TestTrue(*FString::Printf(TEXT("%s should resolve its map type"), *NodeCase.FunctionName.ToString()), HasMapType(*TargetMap, UEdGraphSchema_K2::PC_String, UEdGraphSchema_K2::PC_Int));
|
||||
|
||||
for (const FDependentPin& ExpectedPin : NodeCase.DependentPins)
|
||||
{
|
||||
const UEdGraphPin* DependentPin = Node->FindPin(ExpectedPin.Name);
|
||||
TestNotNull(*FString::Printf(TEXT("%s should have a %s pin"), *NodeCase.FunctionName.ToString(), *ExpectedPin.Name.ToString()), DependentPin);
|
||||
if (DependentPin)
|
||||
{
|
||||
TestEqual(*FString::Printf(TEXT("%s should resolve %s"), *NodeCase.FunctionName.ToString(), *ExpectedPin.Name.ToString()), DependentPin->PinType.PinCategory, ExpectedPin.Category);
|
||||
TestEqual(*FString::Printf(TEXT("%s should preserve the %s container"), *NodeCase.FunctionName.ToString(), *ExpectedPin.Name.ToString()), DependentPin->PinType.ContainerType, ExpectedPin.ContainerType);
|
||||
}
|
||||
}
|
||||
|
||||
Disconnect(*MapOutput, *Node, *TargetMap);
|
||||
TestTrue(*FString::Printf(TEXT("%s should reset after disconnect"), *NodeCase.FunctionName.ToString()), HasMapType(*TargetMap, UEdGraphSchema_K2::PC_Wildcard, UEdGraphSchema_K2::PC_Wildcard));
|
||||
for (const FDependentPin& ExpectedPin : NodeCase.DependentPins)
|
||||
{
|
||||
const UEdGraphPin* DependentPin = Node->FindPin(ExpectedPin.Name);
|
||||
if (DependentPin)
|
||||
{
|
||||
TestEqual(*FString::Printf(TEXT("%s should reset %s after disconnect"), *NodeCase.FunctionName.ToString(), *ExpectedPin.Name.ToString()), DependentPin->PinType.PinCategory, UEdGraphSchema_K2::PC_Wildcard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const FEdGraphPinType ObjectStructMap = MakeMapType(
|
||||
UEdGraphSchema_K2::PC_Object,
|
||||
UEdGraphSchema_K2::PC_Struct,
|
||||
UDirectiveUtilTestObject::StaticClass(),
|
||||
FDirectiveUtilCollisionValue::StaticStruct());
|
||||
for (const FMapNodeCase& NodeCase : NodeCases)
|
||||
{
|
||||
UEdGraph* Graph = MakeGraph();
|
||||
UK2Node_CallFunction* Node = AddFunctionNode(*Graph, NodeCase.FunctionName);
|
||||
UEdGraphPin* TargetMap = Node->FindPin(TEXT("TargetMap"));
|
||||
if (!TestNotNull(*FString::Printf(TEXT("%s should have a typed TargetMap pin"), *NodeCase.FunctionName.ToString()), TargetMap))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
UEdGraphPin* MapOutput = AddMapOutput(*Graph, TEXT("ObjectStructMapOutput"), ObjectStructMap);
|
||||
Connect(*MapOutput, *Node, *TargetMap);
|
||||
TestTrue(
|
||||
*FString::Printf(TEXT("%s should preserve map subtype objects"), *NodeCase.FunctionName.ToString()),
|
||||
HasMapType(*TargetMap, ObjectStructMap));
|
||||
for (const FDependentPin& ExpectedPin : NodeCase.DependentPins)
|
||||
{
|
||||
const UEdGraphPin* DependentPin = Node->FindPin(ExpectedPin.Name);
|
||||
if (!DependentPin)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const bool bUsesKeyType = ExpectedPin.Name == TEXT("Key") || ExpectedPin.Name == TEXT("Keys");
|
||||
TestTrue(
|
||||
*FString::Printf(TEXT("%s should preserve %s subtype"), *NodeCase.FunctionName.ToString(), *ExpectedPin.Name.ToString()),
|
||||
HasElementType(
|
||||
*DependentPin,
|
||||
bUsesKeyType ? UEdGraphSchema_K2::PC_Object : UEdGraphSchema_K2::PC_Struct,
|
||||
bUsesKeyType
|
||||
? static_cast<UObject*>(UDirectiveUtilTestObject::StaticClass())
|
||||
: static_cast<UObject*>(FDirectiveUtilCollisionValue::StaticStruct()),
|
||||
ExpectedPin.ContainerType));
|
||||
}
|
||||
|
||||
Disconnect(*MapOutput, *Node, *TargetMap);
|
||||
TestTrue(
|
||||
*FString::Printf(TEXT("%s should reset after a typed disconnect"), *NodeCase.FunctionName.ToString()),
|
||||
HasMapType(*TargetMap, UEdGraphSchema_K2::PC_Wildcard, UEdGraphSchema_K2::PC_Wildcard));
|
||||
}
|
||||
|
||||
UEdGraph* AppendGraph = MakeGraph();
|
||||
UK2Node_DirectiveUtilMapAppend* AppendNode = NewObject<UK2Node_DirectiveUtilMapAppend>(AppendGraph);
|
||||
AppendGraph->AddNode(AppendNode);
|
||||
AppendNode->AllocateDefaultPins();
|
||||
TestTrue(
|
||||
"Append node should be defined in an uncooked-only module",
|
||||
AppendNode->GetClass()->GetOutermost()->HasAnyPackageFlags(PKG_UncookedOnly));
|
||||
FCompilerResultsLog ModuleValidationLog;
|
||||
FBlueprintEditorUtils::ValidateEditorOnlyNodes(AppendNode, ModuleValidationLog);
|
||||
TestEqual("Append should be valid in a runtime Blueprint", ModuleValidationLog.NumWarnings, 0);
|
||||
TestEqual(
|
||||
"Append should call the runtime append function",
|
||||
AppendNode->GetTargetFunction(),
|
||||
UDirectiveUtilMapFunctionLibrary::StaticClass()->FindFunctionByName(GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMapFunctionLibrary, Map_Append)));
|
||||
|
||||
UEdGraphPin* TargetMap = AppendNode->FindPin(TEXT("TargetMap"));
|
||||
UEdGraphPin* SourceMap = AppendNode->FindPin(TEXT("SourceMap"));
|
||||
TestNotNull("Append should have a TargetMap pin", TargetMap);
|
||||
TestNotNull("Append should have a SourceMap pin", SourceMap);
|
||||
if (!TargetMap || !SourceMap)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TestTrue("Append TargetMap should begin as a wildcard map", HasMapType(*TargetMap, UEdGraphSchema_K2::PC_Wildcard, UEdGraphSchema_K2::PC_Wildcard));
|
||||
TestTrue("Append SourceMap should begin as a wildcard map", HasMapType(*SourceMap, UEdGraphSchema_K2::PC_Wildcard, UEdGraphSchema_K2::PC_Wildcard));
|
||||
|
||||
UEdGraphPin* SourceOutput = AddMapOutput(*AppendGraph, TEXT("SourceOutput"), StringIntMap);
|
||||
Connect(*SourceOutput, *AppendNode, *SourceMap);
|
||||
TestTrue("Append TargetMap should resolve from SourceMap", HasMapType(*TargetMap, UEdGraphSchema_K2::PC_String, UEdGraphSchema_K2::PC_Int));
|
||||
TestTrue("Append SourceMap should resolve from SourceMap", HasMapType(*SourceMap, UEdGraphSchema_K2::PC_String, UEdGraphSchema_K2::PC_Int));
|
||||
|
||||
FString DisallowReason;
|
||||
const FEdGraphPinType NameIntMap = MakeMapType(UEdGraphSchema_K2::PC_Name, UEdGraphSchema_K2::PC_Int);
|
||||
UEdGraphPin* MismatchedKeyOutput = AddMapOutput(*AppendGraph, TEXT("MismatchedKeyOutput"), NameIntMap);
|
||||
TestTrue("Append should reject a different key type", AppendNode->IsConnectionDisallowed(TargetMap, MismatchedKeyOutput, DisallowReason));
|
||||
|
||||
const FEdGraphPinType StringStringMap = MakeMapType(UEdGraphSchema_K2::PC_String, UEdGraphSchema_K2::PC_String);
|
||||
UEdGraphPin* MismatchedValueOutput = AddMapOutput(*AppendGraph, TEXT("MismatchedValueOutput"), StringStringMap);
|
||||
TestTrue("Append should reject a different value type", AppendNode->IsConnectionDisallowed(TargetMap, MismatchedValueOutput, DisallowReason));
|
||||
TestFalse("Append should accept matching map types", AppendNode->IsConnectionDisallowed(TargetMap, SourceOutput, DisallowReason));
|
||||
|
||||
Disconnect(*SourceOutput, *AppendNode, *SourceMap);
|
||||
TestTrue("Append TargetMap should reset after disconnect", HasMapType(*TargetMap, UEdGraphSchema_K2::PC_Wildcard, UEdGraphSchema_K2::PC_Wildcard));
|
||||
TestTrue("Append SourceMap should reset after disconnect", HasMapType(*SourceMap, UEdGraphSchema_K2::PC_Wildcard, UEdGraphSchema_K2::PC_Wildcard));
|
||||
|
||||
UEdGraphPin* TypedSourceOutput = AddMapOutput(*AppendGraph, TEXT("TypedSourceOutput"), ObjectStructMap);
|
||||
Connect(*TypedSourceOutput, *AppendNode, *SourceMap);
|
||||
TestTrue("Append TargetMap should preserve subtype objects", HasMapType(*TargetMap, ObjectStructMap));
|
||||
TestTrue("Append SourceMap should preserve subtype objects", HasMapType(*SourceMap, ObjectStructMap));
|
||||
Disconnect(*TypedSourceOutput, *AppendNode, *SourceMap);
|
||||
|
||||
UEdGraph* MigrationGraph = MakeGraph();
|
||||
UBlueprint* MigrationBlueprint = CastChecked<UBlueprint>(MigrationGraph->GetOuter());
|
||||
UK2Node_CallFunction* LegacyAppendNode = AddFunctionNode(
|
||||
*MigrationGraph,
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMapFunctionLibrary, Map_Append));
|
||||
LegacyAppendNode->NodePosX = 320;
|
||||
LegacyAppendNode->NodePosY = 180;
|
||||
UEdGraphPin* LegacyTargetMap = LegacyAppendNode->FindPin(TEXT("TargetMap"));
|
||||
UEdGraphPin* LegacyOverwrite = LegacyAppendNode->FindPin(TEXT("bOverwriteExisting"));
|
||||
TestNotNull("Legacy Append should have a TargetMap pin", LegacyTargetMap);
|
||||
TestNotNull("Legacy Append should have an overwrite pin", LegacyOverwrite);
|
||||
if (!LegacyTargetMap || !LegacyOverwrite)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
LegacyOverwrite->DefaultValue = TEXT("false");
|
||||
UEdGraphPin* LegacyMapOutput = AddMapOutput(*MigrationGraph, TEXT("LegacyMapOutput"), StringIntMap);
|
||||
Connect(*LegacyMapOutput, *LegacyAppendNode, *LegacyTargetMap);
|
||||
TestTrue("Legacy Append should be upgraded", DirectiveUtilMapNodeMigration::UpgradeLegacyAppendNodes(*MigrationBlueprint));
|
||||
|
||||
TArray<UK2Node_DirectiveUtilMapAppend*> MigratedAppendNodes;
|
||||
MigrationGraph->GetNodesOfClass(MigratedAppendNodes);
|
||||
TestEqual("Migration should create one Append node", MigratedAppendNodes.Num(), 1);
|
||||
if (MigratedAppendNodes.Num() != 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UK2Node_DirectiveUtilMapAppend* MigratedAppendNode = MigratedAppendNodes[0];
|
||||
TestEqual("Migration should preserve the X position", MigratedAppendNode->NodePosX, 320);
|
||||
TestEqual("Migration should preserve the Y position", MigratedAppendNode->NodePosY, 180);
|
||||
TestEqual("Migration should preserve the overwrite default", MigratedAppendNode->FindPinChecked(TEXT("bOverwriteExisting"))->DefaultValue, FString(TEXT("false")));
|
||||
TestTrue("Migrated TargetMap should preserve its connection", HasMapType(*MigratedAppendNode->FindPinChecked(TEXT("TargetMap")), UEdGraphSchema_K2::PC_String, UEdGraphSchema_K2::PC_Int));
|
||||
TestTrue("Migrated SourceMap should resolve from TargetMap", HasMapType(*MigratedAppendNode->FindPinChecked(TEXT("SourceMap")), UEdGraphSchema_K2::PC_String, UEdGraphSchema_K2::PC_Int));
|
||||
|
||||
UEdGraph* FailedMigrationGraph = MakeGraph();
|
||||
UBlueprint* FailedMigrationBlueprint = CastChecked<UBlueprint>(FailedMigrationGraph->GetOuter());
|
||||
UK2Node_CallFunction* IncompatibleLegacyNode = AddFunctionNode(
|
||||
*FailedMigrationGraph,
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMapFunctionLibrary, Map_Append));
|
||||
UEdGraphPin* IncompatibleTargetMap = IncompatibleLegacyNode->FindPinChecked(TEXT("TargetMap"));
|
||||
UEdGraphPin* LegacyOnlyPin = IncompatibleLegacyNode->CreatePin(
|
||||
EGPD_Input,
|
||||
UEdGraphSchema_K2::PC_Int,
|
||||
TEXT("LegacyOnly"));
|
||||
LegacyOnlyPin->ParentPin = IncompatibleTargetMap;
|
||||
IncompatibleTargetMap->SubPins.Add(LegacyOnlyPin);
|
||||
UEdGraphPin* IncompatibleMapOutput = AddMapOutput(*FailedMigrationGraph, TEXT("IncompatibleMapOutput"), StringIntMap);
|
||||
Connect(*IncompatibleMapOutput, *IncompatibleLegacyNode, *IncompatibleTargetMap);
|
||||
AddExpectedMessagePlain(
|
||||
TEXT("BackwardCompatibilityNodeConversion Error 'cannot find pin LegacyOnly"),
|
||||
ELogVerbosity::Warning,
|
||||
EAutomationExpectedMessageFlags::Contains,
|
||||
1);
|
||||
TestFalse(
|
||||
"An incompatible legacy Append node should not be replaced",
|
||||
DirectiveUtilMapNodeMigration::UpgradeLegacyAppendNodes(*FailedMigrationBlueprint));
|
||||
TestTrue(
|
||||
"A failed migration should preserve the legacy connection",
|
||||
IncompatibleMapOutput->LinkedTo.Contains(IncompatibleTargetMap));
|
||||
TestTrue(
|
||||
"A failed migration should preserve the legacy node",
|
||||
FailedMigrationGraph->Nodes.Contains(IncompatibleLegacyNode));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilMapFunctionLibrary.h"
|
||||
#include "Tests/DirectiveUtilTestObject.h"
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
template <typename PropertyType>
|
||||
PropertyType* GetTestProperty(const FName PropertyName)
|
||||
{
|
||||
return FindFProperty<PropertyType>(UDirectiveUtilTestObject::StaticClass(), PropertyName);
|
||||
}
|
||||
|
||||
TSet<int32> MakeIntegerSet(const TArray<int32>& Values)
|
||||
{
|
||||
TSet<int32> Result;
|
||||
for (const int32 Value : Values)
|
||||
{
|
||||
Result.Add(Value);
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
TSet<FString> MakeStringSet(const TArray<FString>& Values)
|
||||
{
|
||||
TSet<FString> Result;
|
||||
for (const FString& Value : Values)
|
||||
{
|
||||
Result.Add(Value);
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
TSet<FName> MakeNameSet(const TArray<FName>& Values)
|
||||
{
|
||||
TSet<FName> Result;
|
||||
for (const FName Value : Values)
|
||||
{
|
||||
Result.Add(Value);
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
template <typename ValueType>
|
||||
bool HaveSameValues(const TSet<ValueType>& Left, const TSet<ValueType>& Right)
|
||||
{
|
||||
if (Left.Num() != Right.Num())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (const ValueType& Value : Left)
|
||||
{
|
||||
if (!Right.Contains(Value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilMapScenarioTest,
|
||||
"DirectiveUtilities.MapScenarios.CardinalityAndTypes",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilMapScenarioTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
FMapProperty* IntegerMapProperty = GetTestProperty<FMapProperty>(GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestMap));
|
||||
FArrayProperty* IntegerArrayProperty = GetTestProperty<FArrayProperty>(GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestArray));
|
||||
FMapProperty* StringMapProperty = GetTestProperty<FMapProperty>(GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestStringMap));
|
||||
FMapProperty* StringMapProperty2 = GetTestProperty<FMapProperty>(GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestStringMap2));
|
||||
FArrayProperty* StringArrayProperty = GetTestProperty<FArrayProperty>(GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestStringArray));
|
||||
FMapProperty* StructMapProperty = GetTestProperty<FMapProperty>(GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestStructValueMap));
|
||||
FArrayProperty* NameArrayProperty = GetTestProperty<FArrayProperty>(GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestNameArray));
|
||||
if (!TestNotNull("Integer map property should be available", IntegerMapProperty)
|
||||
|| !TestNotNull("Integer array property should be available", IntegerArrayProperty)
|
||||
|| !TestNotNull("String map property should be available", StringMapProperty)
|
||||
|| !TestNotNull("Second string map property should be available", StringMapProperty2)
|
||||
|| !TestNotNull("String array property should be available", StringArrayProperty)
|
||||
|| !TestNotNull("Struct map property should be available", StructMapProperty)
|
||||
|| !TestNotNull("Name array property should be available", NameArrayProperty))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UDirectiveUtilTestObject* TestObject = NewObject<UDirectiveUtilTestObject>();
|
||||
for (const int32 ItemCount : {0, 1, 2, 16, 257, 4096})
|
||||
{
|
||||
TestObject->TestMap.Reset();
|
||||
for (int32 Key = 0; Key < ItemCount; ++Key)
|
||||
{
|
||||
TestObject->TestMap.Add(Key, Key % 7);
|
||||
}
|
||||
const FString Label = FString::Printf(TEXT("integer items=%d"), ItemCount);
|
||||
const int32 QueryValue = 3;
|
||||
TArray<int32> ExpectedKeys;
|
||||
for (int32 Key = 0; Key < ItemCount; ++Key)
|
||||
{
|
||||
if (Key % 7 == QueryValue)
|
||||
{
|
||||
ExpectedKeys.Add(Key);
|
||||
}
|
||||
}
|
||||
|
||||
TestObject->TestArray.Reset();
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_GetKeysByValue(
|
||||
&TestObject->TestMap,
|
||||
IntegerMapProperty,
|
||||
&QueryValue,
|
||||
&TestObject->TestArray,
|
||||
IntegerArrayProperty);
|
||||
TestEqual(Label + TEXT(" GetKeysByValue count"), TestObject->TestArray.Num(), ExpectedKeys.Num());
|
||||
TestTrue(Label + TEXT(" GetKeysByValue values"), HaveSameValues(MakeIntegerSet(TestObject->TestArray), MakeIntegerSet(ExpectedKeys)));
|
||||
TestEqual(
|
||||
Label + TEXT(" HasValue"),
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_HasValue(&TestObject->TestMap, IntegerMapProperty, &QueryValue),
|
||||
!ExpectedKeys.IsEmpty());
|
||||
|
||||
const int32 ExistingKey = ItemCount > 0 ? ItemCount / 2 : 0;
|
||||
int32 FoundValue = -1;
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_FindOrAdd(
|
||||
&TestObject->TestMap,
|
||||
IntegerMapProperty,
|
||||
&ExistingKey,
|
||||
&FoundValue);
|
||||
TestEqual(Label + TEXT(" FindOrAdd value"), FoundValue, ItemCount > 0 ? ExistingKey % 7 : 0);
|
||||
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_ClearValues(&TestObject->TestMap, IntegerMapProperty);
|
||||
TestEqual(Label + TEXT(" ClearValues count"), TestObject->TestMap.Num(), FMath::Max(ItemCount, 1));
|
||||
for (const TPair<int32, int32>& Pair : TestObject->TestMap)
|
||||
{
|
||||
TestEqual(Label + TEXT(" ClearValues value"), Pair.Value, 0);
|
||||
}
|
||||
|
||||
TestObject->TestArray.Reset();
|
||||
for (int32 Key = 0; Key < ItemCount; Key += 3)
|
||||
{
|
||||
TestObject->TestArray.Add(Key);
|
||||
}
|
||||
const int32 ExpectedRemoved = TestObject->TestArray.Num();
|
||||
const int32 Removed = UDirectiveUtilMapFunctionLibrary::GenericMap_RemoveKeys(
|
||||
&TestObject->TestMap,
|
||||
IntegerMapProperty,
|
||||
&TestObject->TestArray,
|
||||
IntegerArrayProperty);
|
||||
TestEqual(Label + TEXT(" RemoveKeys count"), Removed, ExpectedRemoved);
|
||||
TestEqual(Label + TEXT(" RemoveKeys remaining"), TestObject->TestMap.Num(), FMath::Max(ItemCount, 1) - ExpectedRemoved);
|
||||
}
|
||||
|
||||
for (const int32 ItemCount : {0, 1, 16, 257})
|
||||
{
|
||||
TestObject->TestStringMap.Reset();
|
||||
TestObject->TestStringMap2.Reset();
|
||||
for (int32 Index = 0; Index < ItemCount; ++Index)
|
||||
{
|
||||
const FString Key = FString::Printf(TEXT("Key%d"), Index);
|
||||
TestObject->TestStringMap.Add(Key, FString::Printf(TEXT("Original%d"), Index));
|
||||
TestObject->TestStringMap2.Add(Key, FString::Printf(TEXT("Replacement%d"), Index));
|
||||
}
|
||||
TestObject->TestStringMap2.Add(TEXT("Added"), TEXT("AddedValue"));
|
||||
const FString Label = FString::Printf(TEXT("string items=%d"), ItemCount);
|
||||
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_Append(
|
||||
&TestObject->TestStringMap,
|
||||
StringMapProperty,
|
||||
&TestObject->TestStringMap2,
|
||||
StringMapProperty2,
|
||||
false);
|
||||
TestEqual(Label + TEXT(" Append count"), TestObject->TestStringMap.Num(), ItemCount + 1);
|
||||
TestEqual(Label + TEXT(" Append new value"), TestObject->TestStringMap.FindRef(TEXT("Added")), FString(TEXT("AddedValue")));
|
||||
if (ItemCount > 0)
|
||||
{
|
||||
TestEqual(Label + TEXT(" Append preserve"), TestObject->TestStringMap.FindRef(TEXT("Key0")), FString(TEXT("Original0")));
|
||||
}
|
||||
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_Append(
|
||||
&TestObject->TestStringMap,
|
||||
StringMapProperty,
|
||||
&TestObject->TestStringMap2,
|
||||
StringMapProperty2,
|
||||
true);
|
||||
if (ItemCount > 0)
|
||||
{
|
||||
TestEqual(Label + TEXT(" Append overwrite"), TestObject->TestStringMap.FindRef(TEXT("Key0")), FString(TEXT("Replacement0")));
|
||||
}
|
||||
|
||||
const int32 CountBeforeSelfAppend = TestObject->TestStringMap.Num();
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_Append(
|
||||
&TestObject->TestStringMap,
|
||||
StringMapProperty,
|
||||
&TestObject->TestStringMap,
|
||||
StringMapProperty,
|
||||
true);
|
||||
TestEqual(Label + TEXT(" self append"), TestObject->TestStringMap.Num(), CountBeforeSelfAppend);
|
||||
|
||||
const FString QueryValue = ItemCount > 0 ? TEXT("Replacement0") : TEXT("missing");
|
||||
TestObject->TestStringArray.Reset();
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_GetKeysByValue(
|
||||
&TestObject->TestStringMap,
|
||||
StringMapProperty,
|
||||
&QueryValue,
|
||||
&TestObject->TestStringArray,
|
||||
StringArrayProperty);
|
||||
const TArray<FString> ExpectedKeys = ItemCount > 0 ? TArray<FString>({TEXT("Key0")}) : TArray<FString>();
|
||||
TestTrue(Label + TEXT(" managed GetKeysByValue"), HaveSameValues(MakeStringSet(TestObject->TestStringArray), MakeStringSet(ExpectedKeys)));
|
||||
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_ClearValues(&TestObject->TestStringMap, StringMapProperty);
|
||||
for (const TPair<FString, FString>& Pair : TestObject->TestStringMap)
|
||||
{
|
||||
TestTrue(Label + TEXT(" managed ClearValues"), Pair.Value.IsEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
FDirectiveUtilCollisionValue FirstStructValue;
|
||||
FirstStructValue.Value = 1;
|
||||
FDirectiveUtilCollisionValue SecondStructValue;
|
||||
SecondStructValue.Value = 2;
|
||||
TestObject->TestStructValueMap = {
|
||||
{TEXT("Alpha"), FirstStructValue},
|
||||
{TEXT("Beta"), SecondStructValue},
|
||||
{TEXT("Gamma"), FirstStructValue}
|
||||
};
|
||||
const FDirectiveUtilCollisionValue StructQuery = FirstStructValue;
|
||||
TestObject->TestNameArray.Reset();
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_GetKeysByValue(
|
||||
&TestObject->TestStructValueMap,
|
||||
StructMapProperty,
|
||||
&StructQuery,
|
||||
&TestObject->TestNameArray,
|
||||
NameArrayProperty);
|
||||
TestTrue(
|
||||
"Struct GetKeysByValue",
|
||||
HaveSameValues(
|
||||
MakeNameSet(TestObject->TestNameArray),
|
||||
MakeNameSet(TArray<FName>({TEXT("Alpha"), TEXT("Gamma")}))));
|
||||
TestTrue(
|
||||
"Struct HasValue",
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_HasValue(
|
||||
&TestObject->TestStructValueMap,
|
||||
StructMapProperty,
|
||||
&StructQuery));
|
||||
UDirectiveUtilMapFunctionLibrary::GenericMap_ClearValues(&TestObject->TestStructValueMap, StructMapProperty);
|
||||
for (const TPair<FName, FDirectiveUtilCollisionValue>& Pair : TestObject->TestStructValueMap)
|
||||
{
|
||||
TestEqual("Struct ClearValues", Pair.Value.Value, 0);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
|
||||
#include "Async/ParallelFor.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilMathExtendedFunctionLibraryTest,
|
||||
"DirectiveUtilities.Math.ExtendedFunctionLibrary",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilMathExtendedFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
float FloatResult = 0.0f;
|
||||
float Strength = 0.0f;
|
||||
TestTrue("Angle array average accepts values across the degree seam",
|
||||
UDirectiveUtilMathFunctionLibrary::GetAngleArrayAverage({350.0f, 10.0f}, FloatResult, Strength));
|
||||
TestTrue("Angle array average crosses the degree seam",
|
||||
FMath::IsNearlyZero(FloatResult, 1.e-4f));
|
||||
TestTrue("Angle array average reports concentration",
|
||||
FMath::IsNearlyEqual(Strength, FMath::Cos(FMath::DegreesToRadians(10.0f)), 1.e-4f));
|
||||
TestFalse("Angle array average rejects an undefined antipodal mean",
|
||||
UDirectiveUtilMathFunctionLibrary::GetAngleArrayAverage({0.0f, 180.0f}, FloatResult, Strength));
|
||||
TestFalse("Angle array average rejects non-finite values",
|
||||
UDirectiveUtilMathFunctionLibrary::GetAngleArrayAverage(
|
||||
{0.0f, std::numeric_limits<float>::quiet_NaN()}, FloatResult, Strength));
|
||||
|
||||
TestTrue("Weighted float average accepts aligned arrays",
|
||||
UDirectiveUtilMathFunctionLibrary::GetWeightedFloatArrayAverage(
|
||||
{10.0f, 20.0f}, {1.0f, 3.0f}, FloatResult));
|
||||
TestTrue("Weighted float average applies weights",
|
||||
FMath::IsNearlyEqual(FloatResult, 17.5f, 1.e-4f));
|
||||
TestTrue("Weighted float average ignores negative weights",
|
||||
UDirectiveUtilMathFunctionLibrary::GetWeightedFloatArrayAverage(
|
||||
{10.0f, 20.0f}, {-1.0f, 2.0f}, FloatResult)
|
||||
&& FMath::IsNearlyEqual(FloatResult, 20.0f, 1.e-4f));
|
||||
TestFalse("Weighted float average rejects mismatched arrays",
|
||||
UDirectiveUtilMathFunctionLibrary::GetWeightedFloatArrayAverage(
|
||||
{10.0f}, {1.0f, 2.0f}, FloatResult));
|
||||
|
||||
FVector VectorResult = FVector::ZeroVector;
|
||||
TestTrue("Weighted vector average accepts aligned arrays",
|
||||
UDirectiveUtilMathFunctionLibrary::GetWeightedVectorArrayAverage(
|
||||
{FVector::ForwardVector, FVector::RightVector}, {1.0f, 1.0f}, VectorResult));
|
||||
TestTrue("Weighted vector average applies weights",
|
||||
VectorResult.Equals(FVector(0.5, 0.5, 0.0), 1.e-6));
|
||||
TestTrue("Weighted vector average preserves large finite values",
|
||||
UDirectiveUtilMathFunctionLibrary::GetWeightedVectorArrayAverage(
|
||||
{FVector(1.e300, 0.0, 0.0)}, {1.e20f}, VectorResult)
|
||||
&& FMath::IsNearlyEqual(VectorResult.X / 1.e300, 1.0, 1.e-12));
|
||||
TestFalse("Weighted vector average rejects non-finite values",
|
||||
UDirectiveUtilMathFunctionLibrary::GetWeightedVectorArrayAverage(
|
||||
{FVector::ForwardVector, FVector(std::numeric_limits<double>::infinity(), 0.0, 0.0)},
|
||||
{1.0f, 1.0f}, VectorResult));
|
||||
|
||||
TArray<float> FloatArrayResult;
|
||||
TestTrue("Float array normalization accepts finite values",
|
||||
UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange(
|
||||
{2.0f, 4.0f, 6.0f}, -1.0f, 1.0f, FloatArrayResult));
|
||||
TestTrue("Float array normalization maps the full range",
|
||||
FloatArrayResult.Num() == 3
|
||||
&& FMath::IsNearlyEqual(FloatArrayResult[0], -1.0f)
|
||||
&& FMath::IsNearlyZero(FloatArrayResult[1])
|
||||
&& FMath::IsNearlyEqual(FloatArrayResult[2], 1.0f));
|
||||
TestTrue("Float array normalization accepts reversed output bounds",
|
||||
UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange(
|
||||
{2.0f, 4.0f, 6.0f}, 1.0f, -1.0f, FloatArrayResult)
|
||||
&& FMath::IsNearlyEqual(FloatArrayResult[0], 1.0f)
|
||||
&& FMath::IsNearlyEqual(FloatArrayResult[2], -1.0f));
|
||||
TestTrue("Float array normalization maps a constant array to the output minimum",
|
||||
UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange(
|
||||
{4.0f, 4.0f}, 5.0f, 10.0f, FloatArrayResult)
|
||||
&& FloatArrayResult == TArray<float>({5.0f, 5.0f}));
|
||||
TestFalse("Float array normalization rejects an empty array",
|
||||
UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange(
|
||||
{}, 0.0f, 1.0f, FloatArrayResult));
|
||||
TArray<float> InPlaceValues = {2.0f, 4.0f, 6.0f};
|
||||
TestTrue("Float array normalization supports the same input and output array",
|
||||
UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange(
|
||||
InPlaceValues, -1.0f, 1.0f, InPlaceValues)
|
||||
&& InPlaceValues == TArray<float>({-1.0f, 0.0f, 1.0f}));
|
||||
|
||||
TestTrue("Weight normalization accepts positive weights",
|
||||
UDirectiveUtilMathFunctionLibrary::NormalizeWeights({1.0f, 3.0f}, FloatArrayResult));
|
||||
TestTrue("Weight normalization sums to one",
|
||||
FloatArrayResult.Num() == 2
|
||||
&& FMath::IsNearlyEqual(FloatArrayResult[0], 0.25f)
|
||||
&& FMath::IsNearlyEqual(FloatArrayResult[1], 0.75f));
|
||||
TestTrue("Weight normalization clears negative weights",
|
||||
UDirectiveUtilMathFunctionLibrary::NormalizeWeights({-2.0f, 2.0f}, FloatArrayResult)
|
||||
&& FMath::IsNearlyZero(FloatArrayResult[0])
|
||||
&& FMath::IsNearlyEqual(FloatArrayResult[1], 1.0f));
|
||||
TestFalse("Weight normalization rejects all-zero weights",
|
||||
UDirectiveUtilMathFunctionLibrary::NormalizeWeights({0.0f, -1.0f}, FloatArrayResult));
|
||||
TArray<float> InPlaceWeights = {1.0f, 3.0f};
|
||||
TestTrue("Weight normalization supports the same input and output array",
|
||||
UDirectiveUtilMathFunctionLibrary::NormalizeWeights(InPlaceWeights, InPlaceWeights)
|
||||
&& InPlaceWeights == TArray<float>({0.25f, 0.75f}));
|
||||
|
||||
TestTrue("Float array percentile accepts finite values",
|
||||
UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile(
|
||||
{4.0f, 1.0f, 3.0f, 2.0f}, 25.0f, FloatResult));
|
||||
TestTrue("Float array percentile interpolates adjacent values",
|
||||
FMath::IsNearlyEqual(FloatResult, 1.75f, 1.e-4f));
|
||||
TestTrue("Float array percentile clamps above one hundred",
|
||||
UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile(
|
||||
{1.0f, 4.0f}, 125.0f, FloatResult)
|
||||
&& FMath::IsNearlyEqual(FloatResult, 4.0f));
|
||||
TestTrue("Float array percentile uses the Type 7 sample position",
|
||||
UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile(
|
||||
{10.0f, 1.0f, 8.0f, 2.0f, 7.0f, 3.0f, 6.0f, 4.0f, 9.0f, 5.0f}, 40.0f, FloatResult)
|
||||
&& FMath::IsNearlyEqual(FloatResult, 4.6f, 1.e-4f));
|
||||
TestFalse("Float array percentile rejects non-finite values",
|
||||
UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile(
|
||||
{1.0f, std::numeric_limits<float>::infinity()}, 50.0f, FloatResult));
|
||||
|
||||
TestTrue("Root mean square accepts finite values",
|
||||
UDirectiveUtilMathFunctionLibrary::GetFloatArrayRootMeanSquare({3.0f, 4.0f}, FloatResult));
|
||||
TestTrue("Root mean square uses the arithmetic mean of squares",
|
||||
FMath::IsNearlyEqual(FloatResult, FMath::Sqrt(12.5f), 1.e-4f));
|
||||
TestFalse("Root mean square rejects an empty array",
|
||||
UDirectiveUtilMathFunctionLibrary::GetFloatArrayRootMeanSquare({}, FloatResult));
|
||||
|
||||
TestTrue("Smooth Step clamps below its range",
|
||||
FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::SmoothStep(-1.0f, 0.0f, 1.0f)));
|
||||
TestTrue("Smooth Step reaches its midpoint",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmoothStep(0.5f, 0.0f, 1.0f), 0.5f));
|
||||
TestTrue("Smooth Step accepts reversed bounds",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmoothStep(0.25f, 1.0f, 0.0f), 0.15625f));
|
||||
TestTrue("Smooth Step treats equal bounds as a step",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmoothStep(2.0f, 2.0f, 2.0f), 1.0f));
|
||||
TestTrue("Smoother Step reaches its midpoint",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmootherStep(0.5f, 0.0f, 1.0f), 0.5f));
|
||||
TestTrue("Smoother Step has quintic shaping",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmootherStep(0.25f, 0.0f, 1.0f), 0.103515625f));
|
||||
|
||||
TestTrue("Range Falloff applies linear attenuation",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(5.0f, 0.0f, 10.0f), 0.5f));
|
||||
TestTrue("Range Falloff applies its exponent",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(5.0f, 0.0f, 10.0f, 2.0f), 0.25f));
|
||||
TestTrue("Range Falloff remains one inside the inner radius",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(2.0f, 3.0f, 10.0f), 1.0f));
|
||||
TestTrue("Range Falloff reaches zero at the outer radius",
|
||||
FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::RangeFalloff(10.0f, 3.0f, 10.0f)));
|
||||
|
||||
double Distance = 0.0;
|
||||
TestTrue("Direction and distance accepts distinct points",
|
||||
UDirectiveUtilMathFunctionLibrary::GetDirectionAndDistance(
|
||||
FVector::ZeroVector, FVector(3.0, 4.0, 0.0), VectorResult, Distance));
|
||||
TestTrue("Direction and distance returns a unit direction",
|
||||
VectorResult.Equals(FVector(0.6, 0.8, 0.0), 1.e-6));
|
||||
TestTrue("Direction and distance returns the length",
|
||||
FMath::IsNearlyEqual(Distance, 5.0));
|
||||
TestFalse("Direction and distance rejects equal points",
|
||||
UDirectiveUtilMathFunctionLibrary::GetDirectionAndDistance(
|
||||
FVector::ZeroVector, FVector::ZeroVector, VectorResult, Distance));
|
||||
TestTrue("Direction and distance handles large finite coordinates",
|
||||
UDirectiveUtilMathFunctionLibrary::GetDirectionAndDistance(
|
||||
FVector::ZeroVector, FVector(1.e200, 0.0, 0.0), VectorResult, Distance)
|
||||
&& VectorResult.Equals(FVector::ForwardVector, 1.e-12)
|
||||
&& FMath::IsNearlyEqual(Distance / 1.e200, 1.0, 1.e-12));
|
||||
|
||||
TestTrue("Signed angle handles large finite vectors",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(
|
||||
FVector(1.e200, 0.0, 0.0), FVector(0.0, 1.e200, 0.0), FVector::UpVector), 90.0f, 1.e-4f));
|
||||
TestTrue("Direction Within Cone handles large finite vectors",
|
||||
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(
|
||||
FVector(1.e200, 0.0, 0.0), FVector(1.e200, 0.0, 0.0), 0.0f));
|
||||
TestTrue("Direction Within Cone includes an identical non-axis direction at zero width",
|
||||
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(
|
||||
FVector(3.e200, 2.e200, 1.e200), FVector(3.e200, 2.e200, 1.e200), 0.0f));
|
||||
TestFalse("Direction Within Cone excludes a measurable angle from a zero-width cone",
|
||||
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(
|
||||
FVector(FMath::Cos(FMath::DegreesToRadians(0.005)), FMath::Sin(FMath::DegreesToRadians(0.005)), 0.0),
|
||||
FVector::ForwardVector, 0.0f));
|
||||
|
||||
const FVector2D RotatedPoint = UDirectiveUtilMathFunctionLibrary::RotatePointAroundPivot2D(
|
||||
FVector2D(2.0, 1.0), FVector2D(1.0, 1.0), 90.0f);
|
||||
TestTrue("Rotate Point Around Pivot 2D preserves the pivot offset",
|
||||
RotatedPoint.Equals(FVector2D(1.0, 2.0), 1.e-6));
|
||||
TestTrue("Rotate Point Around Pivot 2D rejects non-finite input",
|
||||
UDirectiveUtilMathFunctionLibrary::RotatePointAroundPivot2D(
|
||||
FVector2D(std::numeric_limits<double>::infinity(), 0.0), FVector2D::ZeroVector, 90.0f).IsZero());
|
||||
|
||||
TestTrue("Signed Distance To Plane is positive in front of the plane",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedDistanceToPlane(
|
||||
FVector(0.0, 0.0, 5.0), FVector(0.0, 0.0, 2.0), FVector::UpVector), 3.0));
|
||||
TestTrue("Signed Distance To Plane follows the normal direction",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedDistanceToPlane(
|
||||
FVector(0.0, 0.0, 5.0), FVector(0.0, 0.0, 2.0), -FVector::UpVector), -3.0));
|
||||
TestTrue("Signed Distance To Plane rejects a zero normal",
|
||||
FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::SignedDistanceToPlane(
|
||||
FVector::UpVector, FVector::ZeroVector, FVector::ZeroVector)));
|
||||
TestTrue("Signed Distance To Plane handles a large finite normal",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedDistanceToPlane(
|
||||
FVector::ForwardVector, FVector::ZeroVector, FVector(1.e200, 0.0, 0.0)), 1.0, 1.e-12));
|
||||
|
||||
TestTrue("Point Within Cone includes a point inside the cone",
|
||||
UDirectiveUtilMathFunctionLibrary::IsPointWithinCone(
|
||||
FVector(10.0, 0.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 10.0f, 20.0));
|
||||
TestFalse("Point Within Cone excludes a point outside the cone",
|
||||
UDirectiveUtilMathFunctionLibrary::IsPointWithinCone(
|
||||
FVector(0.0, 10.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 10.0f, 20.0));
|
||||
TestFalse("Point Within Cone applies the maximum distance",
|
||||
UDirectiveUtilMathFunctionLibrary::IsPointWithinCone(
|
||||
FVector(10.0, 0.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 10.0f, 5.0));
|
||||
TestTrue("Point Within Cone treats zero maximum distance as unlimited",
|
||||
UDirectiveUtilMathFunctionLibrary::IsPointWithinCone(
|
||||
FVector(10.0, 0.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 10.0f));
|
||||
TestFalse("Point Within Cone applies maximum distance to large finite coordinates",
|
||||
UDirectiveUtilMathFunctionLibrary::IsPointWithinCone(
|
||||
FVector(1.5e200, 0.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 180.0f, 1.e200));
|
||||
|
||||
bool bCircleSamplesValid = true;
|
||||
bool bAnnulusSamplesValid = true;
|
||||
bool bSphereSamplesValid = true;
|
||||
for (int32 Index = 0; Index < 100; ++Index)
|
||||
{
|
||||
const FVector2D CirclePoint = UDirectiveUtilMathFunctionLibrary::RandomPointInCircle(5.0f);
|
||||
const FVector2D AnnulusPoint = UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulus(2.0f, 5.0f);
|
||||
const FVector SpherePoint = UDirectiveUtilMathFunctionLibrary::RandomPointInSphere(5.0f);
|
||||
bCircleSamplesValid &= CirclePoint.Size() <= 5.0 + 1.e-6;
|
||||
bAnnulusSamplesValid &= AnnulusPoint.Size() >= 2.0 - 1.e-6 && AnnulusPoint.Size() <= 5.0 + 1.e-6;
|
||||
bSphereSamplesValid &= SpherePoint.Size() <= 5.0 + 1.e-6;
|
||||
}
|
||||
TestTrue("Random Point In Circle stays within its radius", bCircleSamplesValid);
|
||||
TestTrue("Random Point In Annulus stays between its radii", bAnnulusSamplesValid);
|
||||
TestTrue("Random Point In Sphere stays within its radius", bSphereSamplesValid);
|
||||
|
||||
FRandomStream FirstStream(12345);
|
||||
FRandomStream SecondStream(12345);
|
||||
TestTrue("Random Point In Circle stream variant is deterministic",
|
||||
UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(FirstStream, 5.0f).Equals(
|
||||
UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(SecondStream, 5.0f), 1.e-9));
|
||||
TestTrue("Random Point In Annulus stream variant is deterministic",
|
||||
UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(FirstStream, 2.0f, 5.0f).Equals(
|
||||
UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(SecondStream, 2.0f, 5.0f), 1.e-9));
|
||||
TestTrue("Random Point In Sphere stream variant is deterministic",
|
||||
UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(FirstStream, 5.0f).Equals(
|
||||
UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(SecondStream, 5.0f), 1.e-9));
|
||||
|
||||
#if WITH_EDITOR
|
||||
const TArray<FName> StreamRandomFunctions = {
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GetRandomIndexFromWeightsFromStream),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, RandomPointInCircleFromStream),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, RandomPointInAnnulusFromStream),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, RandomPointInSphereFromStream)
|
||||
};
|
||||
for (const FName FunctionName : StreamRandomFunctions)
|
||||
{
|
||||
const UFunction* Function = UDirectiveUtilMathFunctionLibrary::StaticClass()->FindFunctionByName(FunctionName);
|
||||
TestTrue(*FString::Printf(TEXT("%s should be exposed to Blueprint"), *FunctionName.ToString()), Function != nullptr);
|
||||
if (Function)
|
||||
{
|
||||
TestFalse(
|
||||
*FString::Printf(TEXT("%s should not advertise inert Blueprint thread safety"), *FunctionName.ToString()),
|
||||
Function->HasMetaData(TEXT("BlueprintThreadSafe")));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
TArray<FVector> ParallelRandomResults;
|
||||
ParallelRandomResults.SetNumUninitialized(64);
|
||||
ParallelFor(ParallelRandomResults.Num(), [&ParallelRandomResults](const int32 TaskIndex)
|
||||
{
|
||||
FRandomStream Stream(99173);
|
||||
const FVector2D Circle = UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(Stream, 5.0f);
|
||||
const FVector2D Annulus = UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(Stream, 2.0f, 5.0f);
|
||||
ParallelRandomResults[TaskIndex] = UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(Stream, 5.0f)
|
||||
+ FVector(Circle.X, Circle.Y, Annulus.X + Annulus.Y);
|
||||
});
|
||||
bool bParallelRandomResultsMatch = true;
|
||||
for (int32 Index = 1; Index < ParallelRandomResults.Num(); ++Index)
|
||||
{
|
||||
bParallelRandomResultsMatch &= ParallelRandomResults[Index].Equals(ParallelRandomResults[0], 1.e-12);
|
||||
}
|
||||
TestTrue("Seeded random nodes should remain deterministic across worker tasks", bParallelRandomResultsMatch);
|
||||
|
||||
FRandomStream ExpectedCircleStream(24680);
|
||||
const double ExpectedCircleAngle = static_cast<double>(ExpectedCircleStream.FRand()) * UE_TWO_PI;
|
||||
const double ExpectedCircleRadius = FMath::Sqrt(static_cast<double>(ExpectedCircleStream.FRand())) * 5.0;
|
||||
const FVector2D ExpectedCirclePoint(
|
||||
FMath::Cos(ExpectedCircleAngle) * ExpectedCircleRadius,
|
||||
FMath::Sin(ExpectedCircleAngle) * ExpectedCircleRadius);
|
||||
FRandomStream ActualCircleStream(24680);
|
||||
const FVector2D ActualCirclePoint =
|
||||
UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(ActualCircleStream, 5.0f);
|
||||
TestTrue("Random Point In Circle consumes angle before radius",
|
||||
ActualCirclePoint.Equals(ExpectedCirclePoint, 1.e-6));
|
||||
TestEqual("Random Point In Circle consumes two stream samples",
|
||||
ActualCircleStream.GetCurrentSeed(), ExpectedCircleStream.GetCurrentSeed());
|
||||
|
||||
FRandomStream ExpectedAnnulusStream(13579);
|
||||
const double ExpectedAnnulusAngle = static_cast<double>(ExpectedAnnulusStream.FRand()) * UE_TWO_PI;
|
||||
const double ExpectedAnnulusRadius = FMath::Sqrt(FMath::Lerp(
|
||||
4.0, 25.0, static_cast<double>(ExpectedAnnulusStream.FRand())));
|
||||
const FVector2D ExpectedAnnulusPoint(
|
||||
FMath::Cos(ExpectedAnnulusAngle) * ExpectedAnnulusRadius,
|
||||
FMath::Sin(ExpectedAnnulusAngle) * ExpectedAnnulusRadius);
|
||||
FRandomStream ActualAnnulusStream(13579);
|
||||
const FVector2D ActualAnnulusPoint =
|
||||
UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(ActualAnnulusStream, 2.0f, 5.0f);
|
||||
TestTrue("Random Point In Annulus consumes angle before radius",
|
||||
ActualAnnulusPoint.Equals(ExpectedAnnulusPoint, 1.e-6));
|
||||
TestEqual("Random Point In Annulus consumes two stream samples",
|
||||
ActualAnnulusStream.GetCurrentSeed(), ExpectedAnnulusStream.GetCurrentSeed());
|
||||
|
||||
FRandomStream ExpectedSphereStream(97531);
|
||||
FVector ExpectedSpherePoint;
|
||||
double ExpectedSphereSizeSquared;
|
||||
do
|
||||
{
|
||||
const double X = static_cast<double>(ExpectedSphereStream.FRand()) * 2.0 - 1.0;
|
||||
const double Y = static_cast<double>(ExpectedSphereStream.FRand()) * 2.0 - 1.0;
|
||||
const double Z = static_cast<double>(ExpectedSphereStream.FRand()) * 2.0 - 1.0;
|
||||
ExpectedSpherePoint = FVector(X, Y, Z);
|
||||
ExpectedSphereSizeSquared = ExpectedSpherePoint.SizeSquared();
|
||||
}
|
||||
while (ExpectedSphereSizeSquared > 1.0);
|
||||
ExpectedSpherePoint *= 5.0;
|
||||
FRandomStream ActualSphereStream(97531);
|
||||
TestTrue("Random Point In Sphere consumes coordinates in XYZ order",
|
||||
UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(ActualSphereStream, 5.0f).Equals(
|
||||
ExpectedSpherePoint, 1.e-12)
|
||||
&& ActualSphereStream.GetCurrentSeed() == ExpectedSphereStream.GetCurrentSeed());
|
||||
|
||||
FRandomStream UnchangedStream(86420);
|
||||
const int32 UnchangedSeed = UnchangedStream.GetCurrentSeed();
|
||||
UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(UnchangedStream, 0.0f);
|
||||
UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(UnchangedStream, 0.0f, 0.0f);
|
||||
UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(
|
||||
UnchangedStream, std::numeric_limits<float>::infinity());
|
||||
TestEqual("Invalid and zero radii do not advance random streams",
|
||||
UnchangedStream.GetCurrentSeed(), UnchangedSeed);
|
||||
|
||||
FRandomStream DistributionStream(112358);
|
||||
double CircleDistributionMean = 0.0;
|
||||
double AnnulusDistributionMean = 0.0;
|
||||
double SphereDistributionMean = 0.0;
|
||||
constexpr int32 DistributionSampleCount = 10000;
|
||||
for (int32 Index = 0; Index < DistributionSampleCount; ++Index)
|
||||
{
|
||||
const FVector2D CirclePoint = UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(
|
||||
DistributionStream, 5.0f);
|
||||
const FVector2D AnnulusPoint = UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(
|
||||
DistributionStream, 2.0f, 5.0f);
|
||||
const FVector SpherePoint = UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(
|
||||
DistributionStream, 5.0f);
|
||||
CircleDistributionMean += CirclePoint.SizeSquared() / 25.0;
|
||||
AnnulusDistributionMean += (AnnulusPoint.SizeSquared() - 4.0) / 21.0;
|
||||
SphereDistributionMean += FMath::Pow(SpherePoint.Size() / 5.0, 3.0);
|
||||
}
|
||||
CircleDistributionMean /= DistributionSampleCount;
|
||||
AnnulusDistributionMean /= DistributionSampleCount;
|
||||
SphereDistributionMean /= DistributionSampleCount;
|
||||
TestTrue("Random Point In Circle is uniform by area",
|
||||
FMath::IsNearlyEqual(CircleDistributionMean, 0.5, 0.02));
|
||||
TestTrue("Random Point In Annulus is uniform by area",
|
||||
FMath::IsNearlyEqual(AnnulusDistributionMean, 0.5, 0.02));
|
||||
TestTrue("Random Point In Sphere is uniform by volume",
|
||||
FMath::IsNearlyEqual(SphereDistributionMean, 0.5, 0.02));
|
||||
TestTrue("Random point functions reject non-finite radii",
|
||||
UDirectiveUtilMathFunctionLibrary::RandomPointInCircle(
|
||||
std::numeric_limits<float>::quiet_NaN()).IsZero()
|
||||
&& UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulus(
|
||||
0.0f, std::numeric_limits<float>::infinity()).IsZero()
|
||||
&& UDirectiveUtilMathFunctionLibrary::RandomPointInSphere(
|
||||
std::numeric_limits<float>::infinity()).IsZero());
|
||||
|
||||
TestTrue("Angle array average accepts equivalent angles across multiple turns",
|
||||
UDirectiveUtilMathFunctionLibrary::GetAngleArrayAverage({730.0f, -710.0f}, FloatResult, Strength)
|
||||
&& FMath::IsNearlyEqual(FloatResult, 10.0f, 1.e-4f)
|
||||
&& FMath::IsNearlyEqual(Strength, 1.0f, 1.e-4f));
|
||||
FloatResult = 123.0f;
|
||||
Strength = 123.0f;
|
||||
TestFalse("Angle array average rejects an empty array",
|
||||
UDirectiveUtilMathFunctionLibrary::GetAngleArrayAverage({}, FloatResult, Strength));
|
||||
TestTrue("A rejected angle average resets both outputs",
|
||||
FMath::IsNearlyZero(FloatResult) && FMath::IsNearlyZero(Strength));
|
||||
|
||||
FloatResult = 123.0f;
|
||||
TestTrue("Weighted float average ignores unusable weights",
|
||||
UDirectiveUtilMathFunctionLibrary::GetWeightedFloatArrayAverage(
|
||||
{10.0f, 20.0f, 30.0f, 40.0f},
|
||||
{std::numeric_limits<float>::quiet_NaN(), -1.0f, std::numeric_limits<float>::infinity(), 2.0f},
|
||||
FloatResult)
|
||||
&& FMath::IsNearlyEqual(FloatResult, 40.0f));
|
||||
FloatResult = 123.0f;
|
||||
TestFalse("Weighted float average rejects arrays without a usable weight",
|
||||
UDirectiveUtilMathFunctionLibrary::GetWeightedFloatArrayAverage(
|
||||
{10.0f, 20.0f}, {-1.0f, std::numeric_limits<float>::quiet_NaN()}, FloatResult));
|
||||
TestTrue("A rejected weighted float average resets its output", FMath::IsNearlyZero(FloatResult));
|
||||
|
||||
VectorResult = FVector(123.0);
|
||||
TestTrue("Weighted vector average ignores unusable weights",
|
||||
UDirectiveUtilMathFunctionLibrary::GetWeightedVectorArrayAverage(
|
||||
{FVector(10.0, 20.0, 30.0), FVector(-4.0, 5.0, -6.0)},
|
||||
{std::numeric_limits<float>::infinity(), 3.0f}, VectorResult)
|
||||
&& VectorResult.Equals(FVector(-4.0, 5.0, -6.0), 1.e-9));
|
||||
VectorResult = FVector(123.0);
|
||||
TestFalse("Weighted vector average rejects arrays without a usable weight",
|
||||
UDirectiveUtilMathFunctionLibrary::GetWeightedVectorArrayAverage(
|
||||
{FVector::ForwardVector}, {-1.0f}, VectorResult));
|
||||
TestTrue("A rejected weighted vector average resets its output", VectorResult.IsZero());
|
||||
|
||||
FloatArrayResult = {123.0f};
|
||||
TestFalse("Float array normalization rejects a non-finite source value",
|
||||
UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange(
|
||||
{1.0f, std::numeric_limits<float>::quiet_NaN()}, 0.0f, 1.0f, FloatArrayResult));
|
||||
TestTrue("Rejected float array normalization clears its output", FloatArrayResult.IsEmpty());
|
||||
TestFalse("Float array normalization rejects a non-finite output bound",
|
||||
UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange(
|
||||
{1.0f, 2.0f}, 0.0f, std::numeric_limits<float>::infinity(), FloatArrayResult));
|
||||
|
||||
TestTrue("Weight normalization ignores non-finite and negative weights",
|
||||
UDirectiveUtilMathFunctionLibrary::NormalizeWeights(
|
||||
{std::numeric_limits<float>::quiet_NaN(), std::numeric_limits<float>::infinity(), -2.0f, 4.0f},
|
||||
FloatArrayResult)
|
||||
&& FloatArrayResult == TArray<float>({0.0f, 0.0f, 0.0f, 1.0f}));
|
||||
FloatArrayResult = {123.0f};
|
||||
TestFalse("Weight normalization rejects an empty array",
|
||||
UDirectiveUtilMathFunctionLibrary::NormalizeWeights({}, FloatArrayResult));
|
||||
TestTrue("Rejected weight normalization clears its output", FloatArrayResult.IsEmpty());
|
||||
|
||||
TestTrue("Percentiles clamp below zero",
|
||||
UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile({7.0f, 3.0f, 11.0f}, -50.0f, FloatResult)
|
||||
&& FMath::IsNearlyEqual(FloatResult, 3.0f));
|
||||
TestTrue("Percentiles return the maximum at one hundred",
|
||||
UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile({7.0f, 3.0f, 11.0f}, 100.0f, FloatResult)
|
||||
&& FMath::IsNearlyEqual(FloatResult, 11.0f));
|
||||
TestTrue("A single-value percentile is stable at every finite percentile",
|
||||
UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile({-7.5f}, 37.25f, FloatResult)
|
||||
&& FMath::IsNearlyEqual(FloatResult, -7.5f));
|
||||
FloatResult = 123.0f;
|
||||
TestFalse("Percentiles reject a non-finite percentile",
|
||||
UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile(
|
||||
{1.0f, 2.0f}, std::numeric_limits<float>::quiet_NaN(), FloatResult));
|
||||
TestTrue("A rejected percentile resets its output", FMath::IsNearlyZero(FloatResult));
|
||||
|
||||
const float LargeFiniteValue = std::numeric_limits<float>::max() * 0.25f;
|
||||
TestTrue("Root mean square remains finite near the float limit",
|
||||
UDirectiveUtilMathFunctionLibrary::GetFloatArrayRootMeanSquare(
|
||||
{LargeFiniteValue, -LargeFiniteValue}, FloatResult)
|
||||
&& FMath::IsFinite(FloatResult)
|
||||
&& FMath::IsNearlyEqual(FloatResult / LargeFiniteValue, 1.0f, 1.e-5f));
|
||||
FloatResult = 123.0f;
|
||||
TestFalse("Root mean square rejects non-finite values",
|
||||
UDirectiveUtilMathFunctionLibrary::GetFloatArrayRootMeanSquare(
|
||||
{1.0f, std::numeric_limits<float>::infinity()}, FloatResult));
|
||||
TestTrue("A rejected root mean square resets its output", FMath::IsNearlyZero(FloatResult));
|
||||
|
||||
TestTrue("Signed angle is invariant under positive vector scaling",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(
|
||||
FVector(20.0, 0.0, 5.0), FVector(0.0, 30.0, -7.0), FVector(0.0, 0.0, 9.0)), 90.0f, 1.e-4f));
|
||||
TestTrue("A half-turn signed angle has the expected magnitude",
|
||||
FMath::IsNearlyEqual(FMath::Abs(UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(
|
||||
FVector::ForwardVector, -FVector::ForwardVector, FVector::UpVector)), 180.0f, 1.e-4f));
|
||||
TestEqual("Signed angle rejects non-finite vectors",
|
||||
UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(
|
||||
FVector(std::numeric_limits<double>::infinity(), 0.0, 0.0), FVector::RightVector, FVector::UpVector),
|
||||
0.0f);
|
||||
TestTrue("Delta angle ignores complete turns",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::DeltaAngle(-1080.0f + 15.0f, 1440.0f - 25.0f), -40.0f));
|
||||
TestTrue("Angle interpolation permits negative extrapolation",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::LerpAngle(10.0f, 350.0f, -1.0f), 30.0f));
|
||||
TestTrue("Angle interpolation ignores complete turns in the delta",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::LerpAngle(
|
||||
-1080.0f + 15.0f, 1440.0f - 25.0f, 0.5f), -1085.0f));
|
||||
|
||||
TestTrue("Ping Pong repeats across multiple positive periods",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::PingPong(123.0f, -2.0f, 3.0f), 3.0f));
|
||||
TestTrue("Ping Pong repeats across multiple negative periods",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::PingPong(-122.0f, -2.0f, 3.0f), -2.0f));
|
||||
TestTrue("Smooth Step clamps above its range",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmoothStep(100.0f, -2.0f, 3.0f), 1.0f));
|
||||
TestTrue("Smoother Step accepts reversed bounds",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmootherStep(0.25f, 1.0f, 0.0f), 0.103515625f));
|
||||
TestTrue("Step functions reject non-finite values",
|
||||
FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::SmoothStep(
|
||||
std::numeric_limits<float>::quiet_NaN(), 0.0f, 1.0f))
|
||||
&& FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::SmootherStep(
|
||||
0.5f, 0.0f, std::numeric_limits<float>::infinity())));
|
||||
|
||||
TestTrue("Range Falloff accepts reversed radii",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(5.0f, 10.0f, 0.0f), 0.5f));
|
||||
TestTrue("Range Falloff clamps negative distances to zero",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(-5.0f, 2.0f, 10.0f), 1.0f));
|
||||
TestTrue("Range Falloff treats non-positive exponents as a hard inner range",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(9.0f, 2.0f, 10.0f, -3.0f), 1.0f));
|
||||
TestTrue("Range Falloff is full strength at a collapsed shared radius",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(5.0f, 5.0f, 5.0f), 1.0f));
|
||||
TestTrue("Range Falloff is full strength at the origin when both radii are zero",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(0.0f, 0.0f, 0.0f), 1.0f));
|
||||
TestTrue("Range Falloff is zero outside a collapsed shared radius",
|
||||
FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::RangeFalloff(6.0f, 5.0f, 5.0f)));
|
||||
|
||||
TestTrue("A full-width cone includes the opposite direction",
|
||||
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(
|
||||
-FVector::ForwardVector, FVector::ForwardVector, 180.0f));
|
||||
TestFalse("A zero-width cone excludes the opposite direction",
|
||||
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(
|
||||
-FVector::ForwardVector, FVector::ForwardVector, 0.0f));
|
||||
TestTrue("A point on both cone boundaries is included",
|
||||
UDirectiveUtilMathFunctionLibrary::IsPointWithinCone(
|
||||
FVector(5.0, 5.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 45.0f,
|
||||
FMath::Sqrt(50.0)));
|
||||
TestTrue("Negative cone distance is treated as unlimited",
|
||||
UDirectiveUtilMathFunctionLibrary::IsPointWithinCone(
|
||||
FVector(100.0, 0.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 0.0f, -1.0));
|
||||
|
||||
TestTrue("Rotating by a complete turn preserves a translated point",
|
||||
UDirectiveUtilMathFunctionLibrary::RotatePointAroundPivot2D(
|
||||
FVector2D(1000003.0, -1999995.0), FVector2D(1000000.0, -2000000.0), 1080.0f)
|
||||
.Equals(FVector2D(1000003.0, -1999995.0), 1.e-8));
|
||||
TestTrue("Signed plane distance is translation invariant",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedDistanceToPlane(
|
||||
FVector(1000000.0, -2000000.0, 3000007.0), FVector(1000000.0, -2000000.0, 3000000.0),
|
||||
FVector(0.0, 0.0, 123.0)), 7.0, 1.e-9));
|
||||
|
||||
FRandomStream PositiveRadiusStream(424242);
|
||||
FRandomStream NegativeRadiusStream(424242);
|
||||
TestTrue("Random circle stream treats negative radius as magnitude",
|
||||
UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(PositiveRadiusStream, 5.0f).Equals(
|
||||
UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(NegativeRadiusStream, -5.0f), 1.e-12));
|
||||
FRandomStream OrderedAnnulusStream(31337);
|
||||
FRandomStream ReversedAnnulusStream(31337);
|
||||
TestTrue("Random annulus stream accepts negative reversed radii",
|
||||
UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(OrderedAnnulusStream, 2.0f, 5.0f).Equals(
|
||||
UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(ReversedAnnulusStream, -5.0f, -2.0f),
|
||||
1.e-12));
|
||||
FRandomStream PositiveSphereStream(8675309);
|
||||
FRandomStream NegativeSphereStream(8675309);
|
||||
TestTrue("Random sphere stream treats negative radius as magnitude",
|
||||
UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(PositiveSphereStream, 5.0f).Equals(
|
||||
UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(NegativeSphereStream, -5.0f), 1.e-12));
|
||||
|
||||
return !HasAnyErrors();
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMathFunctionLibraryTest, "DirectiveUtilities.MathFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
#include <limits>
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMathFunctionLibraryTest, "DirectiveUtilities.MathFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
@@ -11,8 +15,84 @@ bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ForwardVector, FVector::RightVector), 90.0f, 0.01f));
|
||||
TestTrue("AngleBetweenVectors should return ~180 for opposite vectors",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ForwardVector, -FVector::ForwardVector), 180.0f, 0.01f));
|
||||
TestTrue("AngleBetweenVectors should handle zero vector gracefully",
|
||||
FMath::IsFinite(UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ZeroVector, FVector::ForwardVector)));
|
||||
TestEqual("AngleBetweenVectors should return 0 for a zero vector",
|
||||
UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ZeroVector, FVector::ForwardVector), 0.0f);
|
||||
TestEqual("AngleBetweenVectors should return 0 for two zero vectors",
|
||||
UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ZeroVector, FVector::ZeroVector), 0.0f);
|
||||
|
||||
TestTrue("SignedAngleBetweenVectors returns a positive counterclockwise angle around the axis",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(
|
||||
FVector::ForwardVector, FVector::RightVector, FVector::UpVector), 90.0f, 1.e-4f));
|
||||
TestTrue("SignedAngleBetweenVectors returns a negative clockwise angle around the axis",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(
|
||||
FVector::RightVector, FVector::ForwardVector, FVector::UpVector), -90.0f, 1.e-4f));
|
||||
TestTrue("SignedAngleBetweenVectors reverses sign with the axis",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(
|
||||
FVector::ForwardVector, FVector::RightVector, -FVector::UpVector), -90.0f, 1.e-4f));
|
||||
TestTrue("SignedAngleBetweenVectors projects directions onto the axis plane",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(
|
||||
FVector(1.0, 0.0, 4.0), FVector(0.0, 1.0, -3.0), FVector::UpVector), 90.0f, 1.e-4f));
|
||||
TestEqual("SignedAngleBetweenVectors returns zero for a zero direction",
|
||||
UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(FVector::ZeroVector, FVector::RightVector, FVector::UpVector), 0.0f);
|
||||
TestEqual("SignedAngleBetweenVectors returns zero for a direction parallel to the axis",
|
||||
UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(FVector::UpVector, FVector::RightVector, FVector::UpVector), 0.0f);
|
||||
TestEqual("SignedAngleBetweenVectors returns zero for a zero axis",
|
||||
UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(FVector::ForwardVector, FVector::RightVector, FVector::ZeroVector), 0.0f);
|
||||
|
||||
TestTrue("DeltaAngle crosses the positive angle seam by the shortest path",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::DeltaAngle(350.0f, 10.0f), 20.0f, 1.e-4f));
|
||||
TestTrue("DeltaAngle crosses the negative angle seam by the shortest path",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::DeltaAngle(10.0f, 350.0f), -20.0f, 1.e-4f));
|
||||
TestEqual("DeltaAngle returns zero for equivalent wrapped angles",
|
||||
UDirectiveUtilMathFunctionLibrary::DeltaAngle(-180.0f, 180.0f), 0.0f);
|
||||
TestTrue("DeltaAngle canonicalizes an exactly opposite pair to +180",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::DeltaAngle(0.0f, 180.0f), 180.0f, 1.e-4f)
|
||||
&& FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::DeltaAngle(0.0f, -180.0f), 180.0f, 1.e-4f)
|
||||
&& FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::DeltaAngle(0.0f, 540.0f), 180.0f, 1.e-4f)
|
||||
&& FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::DeltaAngle(0.0f, -540.0f), 180.0f, 1.e-4f));
|
||||
TestEqual("DeltaAngle returns zero for non-finite input",
|
||||
UDirectiveUtilMathFunctionLibrary::DeltaAngle(std::numeric_limits<float>::infinity(), 0.0f), 0.0f);
|
||||
|
||||
TestTrue("LerpAngle crosses the angle seam by the shortest path",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::LerpAngle(350.0f, 10.0f, 0.5f), 360.0f, 1.e-4f));
|
||||
TestTrue("LerpAngle returns A at Alpha 0",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::LerpAngle(350.0f, 10.0f, 0.0f), 350.0f, 1.e-4f));
|
||||
TestTrue("LerpAngle reaches A plus the shortest delta at Alpha 1",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::LerpAngle(350.0f, 10.0f, 1.0f), 370.0f, 1.e-4f));
|
||||
TestTrue("LerpAngle permits extrapolation without wrapping the result",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::LerpAngle(0.0f, 90.0f, 2.0f), 180.0f, 1.e-4f)
|
||||
&& FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::LerpAngle(0.0f, 90.0f, 3.0f), 270.0f, 1.e-4f));
|
||||
TestEqual("LerpAngle returns zero for non-finite input",
|
||||
UDirectiveUtilMathFunctionLibrary::LerpAngle(0.0f, 90.0f, std::numeric_limits<float>::quiet_NaN()), 0.0f);
|
||||
|
||||
TestTrue("PingPong reaches the middle of an ascending range",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::PingPong(0.5f, 0.0f, 1.0f), 0.5f, 1.e-4f));
|
||||
TestTrue("PingPong reverses after the upper bound",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::PingPong(1.5f, 0.0f, 1.0f), 0.5f, 1.e-4f));
|
||||
TestTrue("PingPong supports negative values",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::PingPong(-0.25f, 0.0f, 1.0f), 0.25f, 1.e-4f));
|
||||
TestTrue("PingPong accepts reversed bounds",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::PingPong(12.5f, 20.0f, 10.0f), 12.5f, 1.e-4f));
|
||||
TestEqual("PingPong returns the shared bound for a zero-sized range",
|
||||
UDirectiveUtilMathFunctionLibrary::PingPong(100.0f, 7.0f, 7.0f), 7.0f);
|
||||
TestEqual("PingPong returns zero for non-finite input",
|
||||
UDirectiveUtilMathFunctionLibrary::PingPong(std::numeric_limits<float>::infinity(), 0.0f, 1.0f), 0.0f);
|
||||
|
||||
TestTrue("IsDirectionWithinCone includes a direction inside the cone",
|
||||
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(FVector(1.0, 1.0, 0.0), FVector::ForwardVector, 46.0f));
|
||||
TestTrue("IsDirectionWithinCone includes a direction on the cone boundary",
|
||||
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(FVector(1.0, 1.0, 0.0), FVector::ForwardVector, 45.0f));
|
||||
TestFalse("IsDirectionWithinCone excludes a direction outside the cone",
|
||||
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(FVector::RightVector, FVector::ForwardVector, 45.0f));
|
||||
TestTrue("IsDirectionWithinCone clamps angles above 180 degrees",
|
||||
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(-FVector::ForwardVector, FVector::ForwardVector, 270.0f));
|
||||
TestFalse("IsDirectionWithinCone clamps negative angles to zero",
|
||||
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(FVector(1.0, 0.1, 0.0), FVector::ForwardVector, -20.0f));
|
||||
TestFalse("IsDirectionWithinCone rejects a zero direction",
|
||||
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(FVector::ZeroVector, FVector::ForwardVector, 45.0f));
|
||||
TestFalse("IsDirectionWithinCone rejects a non-finite angle",
|
||||
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(
|
||||
FVector::ForwardVector, FVector::ForwardVector, std::numeric_limits<float>::quiet_NaN()));
|
||||
|
||||
{
|
||||
const FVector2D Sample2D(12.34f, 56.78f);
|
||||
@@ -75,15 +155,16 @@ bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
const TArray<EDirectiveUtilEaseType> AllEaseTypes = {
|
||||
EDirectiveUtilEaseType::BackIn, EDirectiveUtilEaseType::BackOut, EDirectiveUtilEaseType::BackInOut,
|
||||
EDirectiveUtilEaseType::ElasticIn, EDirectiveUtilEaseType::ElasticOut, EDirectiveUtilEaseType::ElasticInOut,
|
||||
EDirectiveUtilEaseType::BounceIn, EDirectiveUtilEaseType::BounceOut, EDirectiveUtilEaseType::BounceInOut
|
||||
EDirectiveUtilEaseType::BounceIn, EDirectiveUtilEaseType::BounceOut, EDirectiveUtilEaseType::BounceInOut,
|
||||
EDirectiveUtilEaseType::Linear
|
||||
};
|
||||
for (const EDirectiveUtilEaseType EaseType : AllEaseTypes)
|
||||
{
|
||||
const FString TypeName = FString::FromInt(static_cast<int32>(EaseType));
|
||||
TestTrue(FString::Printf(TEXT("EaseAlpha(0) should be ~0 for type %s"), *TypeName),
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::EaseAlpha(0.0f, EaseType), 0.0f, 1.e-3f));
|
||||
TestTrue(FString::Printf(TEXT("EaseAlpha(1) should be ~1 for type %s"), *TypeName),
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::EaseAlpha(1.0f, EaseType), 1.0f, 1.e-3f));
|
||||
TestEqual(FString::Printf(TEXT("EaseAlpha(0) should be exactly 0 for type %s"), *TypeName),
|
||||
UDirectiveUtilMathFunctionLibrary::EaseAlpha(0.0f, EaseType), 0.0f);
|
||||
TestEqual(FString::Printf(TEXT("EaseAlpha(1) should be exactly 1 for type %s"), *TypeName),
|
||||
UDirectiveUtilMathFunctionLibrary::EaseAlpha(1.0f, EaseType), 1.0f);
|
||||
for (const float Sample : {0.0f, 0.25f, 0.5f, 0.75f, 1.0f})
|
||||
{
|
||||
TestTrue(FString::Printf(TEXT("EaseAlpha(%.2f) should be finite for type %s"), Sample, *TypeName),
|
||||
@@ -94,6 +175,22 @@ bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
TestEqual("EaseAlpha should clamp alpha below 0",
|
||||
UDirectiveUtilMathFunctionLibrary::EaseAlpha(-1.0f, EDirectiveUtilEaseType::BounceOut),
|
||||
UDirectiveUtilMathFunctionLibrary::EaseAlpha(0.0f, EDirectiveUtilEaseType::BounceOut));
|
||||
TestEqual("Linear ease should pass the clamped alpha through",
|
||||
UDirectiveUtilMathFunctionLibrary::EaseAlpha(0.3f, EDirectiveUtilEaseType::Linear), 0.3f);
|
||||
|
||||
const FTransform EaseStart(FRotator::ZeroRotator, FVector::ZeroVector, FVector::OneVector);
|
||||
const FTransform EaseTarget(FRotator(0.0, 90.0, 0.0), FVector(10.0, 0.0, 0.0), FVector(3.0));
|
||||
const FTransform EasedMidpoint = UDirectiveUtilMathFunctionLibrary::EaseTransform(
|
||||
EaseStart, EaseTarget, 0.5f, EDirectiveUtilEaseType::Linear);
|
||||
TestTrue("EaseTransform should blend location, rotation, and scale",
|
||||
EasedMidpoint.GetLocation().Equals(FVector(5.0, 0.0, 0.0), 1.e-4)
|
||||
&& EasedMidpoint.GetRotation().Equals(FRotator(0.0, 45.0, 0.0).Quaternion(), 1.e-4)
|
||||
&& EasedMidpoint.GetScale3D().Equals(FVector(2.0), 1.e-4));
|
||||
TestTrue("EaseTransform should return its endpoints at alpha 0 and 1",
|
||||
UDirectiveUtilMathFunctionLibrary::EaseTransform(
|
||||
EaseStart, EaseTarget, 0.0f, EDirectiveUtilEaseType::BounceOut).Equals(EaseStart, 1.e-4)
|
||||
&& UDirectiveUtilMathFunctionLibrary::EaseTransform(
|
||||
EaseStart, EaseTarget, 1.0f, EDirectiveUtilEaseType::BounceOut).Equals(EaseTarget, 1.e-4));
|
||||
TestEqual("EaseAlpha should clamp alpha above 1",
|
||||
UDirectiveUtilMathFunctionLibrary::EaseAlpha(2.0f, EDirectiveUtilEaseType::BounceOut),
|
||||
UDirectiveUtilMathFunctionLibrary::EaseAlpha(1.0f, EDirectiveUtilEaseType::BounceOut));
|
||||
@@ -147,6 +244,10 @@ bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights(TArray<float>()), static_cast<int32>(INDEX_NONE));
|
||||
TestEqual("Weighted random with all-zero weights returns INDEX_NONE",
|
||||
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights({0.0f, 0.0f, 0.0f}), static_cast<int32>(INDEX_NONE));
|
||||
TestEqual("Weighted random ignores non-finite weights",
|
||||
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights({std::numeric_limits<float>::quiet_NaN(), 1.0f, std::numeric_limits<float>::infinity()}), 1);
|
||||
TestTrue("Weighted random handles large finite totals",
|
||||
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights({MAX_flt, MAX_flt}) != INDEX_NONE);
|
||||
for (int32 Iteration = 0; Iteration < 25; ++Iteration)
|
||||
{
|
||||
TestEqual("Weighted random {0,1,0} always selects index 1",
|
||||
@@ -172,6 +273,10 @@ bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
UDirectiveUtilMathFunctionLibrary::FormatDuration(45.0f).ToString(), FString(TEXT("45s")));
|
||||
TestEqual("FormatDuration(-90) is \"-1m 30s\"",
|
||||
UDirectiveUtilMathFunctionLibrary::FormatDuration(-90.0f).ToString(), FString(TEXT("-1m 30s")));
|
||||
TestEqual("FormatDuration(-45, false) is \"0m\"",
|
||||
UDirectiveUtilMathFunctionLibrary::FormatDuration(-45.0f, false).ToString(), FString(TEXT("0m")));
|
||||
TestFalse("FormatDuration handles the largest finite float without wrapping negative",
|
||||
UDirectiveUtilMathFunctionLibrary::FormatDuration(MAX_flt).ToString().StartsWith(TEXT("-")));
|
||||
|
||||
TestEqual("FormatRelativeTime 5 minutes back reads \"5 minutes ago\"",
|
||||
UDirectiveUtilMathFunctionLibrary::FormatRelativeTime(FDateTime::Now() - FTimespan::FromMinutes(5)).ToString(), FString(TEXT("5 minutes ago")));
|
||||
@@ -231,6 +336,50 @@ bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
UDirectiveUtilMathFunctionLibrary::GetFloatArrayStandardDeviation(EmptyFloats), 0.0f);
|
||||
}
|
||||
|
||||
TestTrue("GetIntArrayMedian should average the full int32 range without overflow",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian({MIN_int32, MAX_int32}), -0.5f, 1.e-4f));
|
||||
TestEqual("GetIntArrayMedian should handle repeated values",
|
||||
UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian({7, 7, 7, 7, 7}), 7.0f);
|
||||
TestEqual("GetFloatArrayMedian should handle repeated values",
|
||||
UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian({7.5f, 7.5f, 7.5f, 7.5f}), 7.5f);
|
||||
TestTrue("GetFloatArrayMedian should return NaN when any input is NaN",
|
||||
FMath::IsNaN(UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian({1.0f, std::numeric_limits<float>::quiet_NaN(), 3.0f})));
|
||||
|
||||
FRandomStream MedianStream(481516);
|
||||
for (int32 Iteration = 0; Iteration < 200; ++Iteration)
|
||||
{
|
||||
const int32 Count = MedianStream.RandRange(1, 257);
|
||||
TArray<int32> IntValues;
|
||||
TArray<float> FloatValues;
|
||||
IntValues.Reserve(Count);
|
||||
FloatValues.Reserve(Count);
|
||||
for (int32 Index = 0; Index < Count; ++Index)
|
||||
{
|
||||
const int32 Value = MedianStream.RandRange(-1000, 1000);
|
||||
IntValues.Add(Value);
|
||||
FloatValues.Add(static_cast<float>(Value) * 0.25f);
|
||||
}
|
||||
|
||||
TArray<int32> SortedInts = IntValues;
|
||||
SortedInts.Sort();
|
||||
const int32 Middle = SortedInts.Num() / 2;
|
||||
const float ExpectedIntMedian = SortedInts.Num() % 2 == 0
|
||||
? static_cast<float>((static_cast<double>(SortedInts[Middle - 1]) + static_cast<double>(SortedInts[Middle])) * 0.5)
|
||||
: static_cast<float>(SortedInts[Middle]);
|
||||
TestTrue(
|
||||
FString::Printf(TEXT("Integer median fuzz case %d"), Iteration),
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian(IntValues), ExpectedIntMedian, 1.e-4f));
|
||||
|
||||
TArray<float> SortedFloats = FloatValues;
|
||||
SortedFloats.Sort();
|
||||
const float ExpectedFloatMedian = SortedFloats.Num() % 2 == 0
|
||||
? static_cast<float>((static_cast<double>(SortedFloats[Middle - 1]) + static_cast<double>(SortedFloats[Middle])) * 0.5)
|
||||
: SortedFloats[Middle];
|
||||
TestTrue(
|
||||
FString::Printf(TEXT("Float median fuzz case %d"), Iteration),
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian(FloatValues), ExpectedFloatMedian, 1.e-4f));
|
||||
}
|
||||
|
||||
FRandomStream StreamA(12345);
|
||||
FRandomStream StreamB(12345);
|
||||
const int32 StreamIndexA = UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeightsFromStream(StreamA, {1.0f, 1.0f, 1.0f, 1.0f});
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
template <typename ValueType>
|
||||
double GetSortedMedian(TArray<ValueType> Values)
|
||||
{
|
||||
Values.Sort();
|
||||
const int32 Middle = Values.Num() / 2;
|
||||
if (Values.Num() % 2 != 0)
|
||||
{
|
||||
return static_cast<double>(Values[Middle]);
|
||||
}
|
||||
return (static_cast<double>(Values[Middle - 1]) + static_cast<double>(Values[Middle])) * 0.5;
|
||||
}
|
||||
|
||||
TArray<int32> MakeMedianValues(const int32 Count, const int32 Pattern)
|
||||
{
|
||||
TArray<int32> Values;
|
||||
Values.Reserve(Count);
|
||||
for (int32 Index = 0; Index < Count; ++Index)
|
||||
{
|
||||
switch (Pattern)
|
||||
{
|
||||
case 0:
|
||||
Values.Add(Index - Count / 2);
|
||||
break;
|
||||
case 1:
|
||||
Values.Add(Count - Index);
|
||||
break;
|
||||
case 2:
|
||||
Values.Add(Index % 7);
|
||||
break;
|
||||
default:
|
||||
Values.Add(Index % 2 == 0 ? MIN_int32 : MAX_int32);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Values;
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilMathCardinalityTest,
|
||||
"DirectiveUtilities.MathScenarios.MedianCardinality",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilMathCardinalityTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
for (const int32 ItemCount : {1, 2, 3, 4, 5, 16, 17, 256, 257, 4096, 4097})
|
||||
{
|
||||
for (int32 Pattern = 0; Pattern < 4; ++Pattern)
|
||||
{
|
||||
const TArray<int32> IntegerValues = MakeMedianValues(ItemCount, Pattern);
|
||||
TArray<float> FloatValues;
|
||||
FloatValues.Reserve(ItemCount);
|
||||
for (const int32 Value : IntegerValues)
|
||||
{
|
||||
FloatValues.Add(static_cast<float>(Value) * 0.25f);
|
||||
}
|
||||
|
||||
const FString Label = FString::Printf(TEXT("items=%d pattern=%d"), ItemCount, Pattern);
|
||||
TestTrue(
|
||||
Label + TEXT(" integer median"),
|
||||
FMath::IsNearlyEqual(
|
||||
UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian(IntegerValues),
|
||||
static_cast<float>(GetSortedMedian(IntegerValues)),
|
||||
1.e-4f));
|
||||
TestTrue(
|
||||
Label + TEXT(" float median"),
|
||||
FMath::IsNearlyEqual(
|
||||
UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian(FloatValues),
|
||||
static_cast<float>(GetSortedMedian(FloatValues)),
|
||||
1.e-4f));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,837 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
|
||||
|
||||
#include "Components/SplineComponent.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "UObject/Class.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace
|
||||
{
|
||||
bool PointsEqual(const TArray<FVector>& A, const TArray<FVector>& B, const double Tolerance = 1.e-9)
|
||||
{
|
||||
if (A.Num() != B.Num())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (int32 Index = 0; Index < A.Num(); ++Index)
|
||||
{
|
||||
if (!A[Index].Equals(B[Index], Tolerance))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PointsEqualAfterTranslation(const TArray<FVector>& BasePoints, const TArray<FVector>& TranslatedPoints,
|
||||
const FVector& Translation, const double Tolerance = 1.e-6)
|
||||
{
|
||||
if (BasePoints.Num() != TranslatedPoints.Num())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (int32 Index = 0; Index < BasePoints.Num(); ++Index)
|
||||
{
|
||||
if (!(TranslatedPoints[Index] - Translation).Equals(BasePoints[Index], Tolerance))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PointsMatchLocation(const TArray<FVector>& Points, const FVector& Location, const double Tolerance = 1.e-9)
|
||||
{
|
||||
for (const FVector& Point : Points)
|
||||
{
|
||||
if (!Point.Equals(Location, Tolerance))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilPointGenerationTest,
|
||||
"DirectiveUtilities.Math.PointGeneration",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilPointGenerationTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
const FName CallableGeneratorNames[] = {
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateGridPoints2D),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateGridPoints3D),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateGridTransforms2D),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateGridTransforms3D),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateRectangularHexGrid),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateRectangularHexGridTransforms),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GetRectangularHexGridCoordinates),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateHexagonalHexGrid),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateHexagonalHexGridTransforms),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GetHexesInRange),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GetHexRing),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GetHexLine),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GeneratePointsAlongDirection),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GeneratePointsBetweenLocations),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GeneratePointsOnCircle),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateTransformsOnCircle),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GeneratePointsOnArc),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateTransformsOnArc),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GeneratePointsOnDisc),
|
||||
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GeneratePointsOnSphere)
|
||||
};
|
||||
for (const FName FunctionName : CallableGeneratorNames)
|
||||
{
|
||||
const UFunction* Function = UDirectiveUtilMathFunctionLibrary::StaticClass()->FindFunctionByName(FunctionName);
|
||||
TestNotNull(*FString::Printf(TEXT("%s should be exposed to Blueprint"), *FunctionName.ToString()), Function);
|
||||
if (Function)
|
||||
{
|
||||
TestTrue(*FString::Printf(TEXT("%s should be callable"), *FunctionName.ToString()),
|
||||
Function->HasAnyFunctionFlags(FUNC_BlueprintCallable));
|
||||
TestFalse(*FString::Printf(TEXT("%s should not execute as a pure node"), *FunctionName.ToString()),
|
||||
Function->HasAnyFunctionFlags(FUNC_BlueprintPure));
|
||||
#if WITH_EDITOR
|
||||
TestFalse(*FString::Printf(TEXT("%s should not advertise inert Blueprint thread safety"), *FunctionName.ToString()),
|
||||
Function->HasMetaData(TEXT("BlueprintThreadSafe")));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
const TArray<FVector> Grid2D = UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(3, 2), FVector2D(10.0, 20.0), true);
|
||||
const TArray<FVector> ExpectedGrid2D = {
|
||||
FVector(-10.0, -10.0, 0.0), FVector(0.0, -10.0, 0.0), FVector(10.0, -10.0, 0.0),
|
||||
FVector(-10.0, 10.0, 0.0), FVector(0.0, 10.0, 0.0), FVector(10.0, 10.0, 0.0)
|
||||
};
|
||||
TestTrue(TEXT("2D grid is centered and ordered by X then Y"), PointsEqual(Grid2D, ExpectedGrid2D));
|
||||
|
||||
const TArray<FVector> RotatedGrid2D = UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D(
|
||||
FVector(5.0, 7.0, 9.0), FRotator(0.0, 90.0, 0.0), FIntPoint(2, 1), FVector2D(3.0, 0.0), false);
|
||||
TestTrue(TEXT("2D grid rotation places its local X axis in world space"),
|
||||
RotatedGrid2D.Num() == 2
|
||||
&& RotatedGrid2D[0].Equals(FVector(5.0, 7.0, 9.0), 1.e-9)
|
||||
&& RotatedGrid2D[1].Equals(FVector(5.0, 10.0, 9.0), 1.e-9));
|
||||
|
||||
const TArray<FVector> Grid3D = UDirectiveUtilMathFunctionLibrary::GenerateGridPoints3D(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, FIntVector(2, 2, 2), FVector(2.0, 4.0, 6.0), true);
|
||||
TestTrue(TEXT("3D grid is centered and ordered by X then Y then Z"),
|
||||
Grid3D.Num() == 8
|
||||
&& Grid3D[0].Equals(FVector(-1.0, -2.0, -3.0), 1.e-9)
|
||||
&& Grid3D[1].Equals(FVector(1.0, -2.0, -3.0), 1.e-9)
|
||||
&& Grid3D[2].Equals(FVector(-1.0, 2.0, -3.0), 1.e-9)
|
||||
&& Grid3D.Last().Equals(FVector(1.0, 2.0, 3.0), 1.e-9));
|
||||
TestTrue(TEXT("Grid generation rejects non-positive dimensions"),
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(2, 0), FVector2D(1.0), true).IsEmpty());
|
||||
TestTrue(TEXT("Grid generation rejects point-count overflow"),
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateGridPoints3D(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, FIntVector(MAX_int32, 2, 2), FVector::OneVector, true).IsEmpty());
|
||||
|
||||
const FRotator GridInstanceRotation(10.0, 20.0, 30.0);
|
||||
const FQuat GridInstanceQuaternion = GridInstanceRotation.Quaternion();
|
||||
const FVector GridInstanceScale(0.25, 0.5, 0.75);
|
||||
const TArray<FTransform> GridTransforms2D = UDirectiveUtilMathFunctionLibrary::GenerateGridTransforms2D(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(3, 2), FVector2D(10.0, 20.0), true,
|
||||
GridInstanceRotation, GridInstanceScale);
|
||||
bool bGridTransforms2DValid = GridTransforms2D.Num() == Grid2D.Num();
|
||||
for (int32 Index = 0; Index < GridTransforms2D.Num(); ++Index)
|
||||
{
|
||||
bGridTransforms2DValid &= GridTransforms2D[Index].GetLocation().Equals(Grid2D[Index], 1.e-9);
|
||||
bGridTransforms2DValid &= GridTransforms2D[Index].GetRotation().Equals(GridInstanceQuaternion, 1.e-12);
|
||||
bGridTransforms2DValid &= GridTransforms2D[Index].GetScale3D() == GridInstanceScale;
|
||||
}
|
||||
TestTrue(TEXT("2D grid transforms match point locations and broadcast rotation and scale"),
|
||||
bGridTransforms2DValid);
|
||||
|
||||
const TArray<FTransform> GridTransforms3D = UDirectiveUtilMathFunctionLibrary::GenerateGridTransforms3D(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, FIntVector(2, 2, 2), FVector(2.0, 4.0, 6.0), true,
|
||||
GridInstanceRotation, GridInstanceScale);
|
||||
bool bGridTransforms3DValid = GridTransforms3D.Num() == Grid3D.Num();
|
||||
for (int32 Index = 0; Index < GridTransforms3D.Num(); ++Index)
|
||||
{
|
||||
bGridTransforms3DValid &= GridTransforms3D[Index].GetLocation().Equals(Grid3D[Index], 1.e-9);
|
||||
bGridTransforms3DValid &= GridTransforms3D[Index].GetRotation().Equals(GridInstanceQuaternion, 1.e-12);
|
||||
bGridTransforms3DValid &= GridTransforms3D[Index].GetScale3D() == GridInstanceScale;
|
||||
}
|
||||
TestTrue(TEXT("3D grid transforms match point locations and broadcast rotation and scale"),
|
||||
bGridTransforms3DValid);
|
||||
TestTrue(TEXT("Grid transform generation rejects invalid dimensions and non-finite scale"),
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateGridTransforms2D(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(2, 0), FVector2D(1.0), true).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GenerateGridTransforms3D(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, FIntVector(2, 2, 2), FVector::OneVector, true,
|
||||
FRotator::ZeroRotator, FVector(std::numeric_limits<double>::infinity())).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GenerateGridTransforms3D(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, FIntVector(MAX_int32, 2, 2), FVector::OneVector, true)
|
||||
.IsEmpty());
|
||||
|
||||
const double HexRadius = 10.0;
|
||||
const FVector PointyQ = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation(
|
||||
FIntPoint(1, 0), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||
EDirectiveUtilHexOrientation::PointyTop);
|
||||
const FVector PointyR = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation(
|
||||
FIntPoint(0, 1), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||
EDirectiveUtilHexOrientation::PointyTop);
|
||||
TestTrue(TEXT("Pointy-top axial coordinates use the expected basis"),
|
||||
PointyQ.Equals(FVector(UE_DOUBLE_SQRT_3 * HexRadius, 0.0, 0.0), 1.e-9)
|
||||
&& PointyR.Equals(FVector(UE_DOUBLE_SQRT_3 * 0.5 * HexRadius, 15.0, 0.0), 1.e-9));
|
||||
|
||||
const FVector FlatQ = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation(
|
||||
FIntPoint(1, 0), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||
EDirectiveUtilHexOrientation::FlatTop);
|
||||
const FVector FlatR = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation(
|
||||
FIntPoint(0, 1), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||
EDirectiveUtilHexOrientation::FlatTop);
|
||||
TestTrue(TEXT("Flat-top axial coordinates use the expected basis"),
|
||||
FlatQ.Equals(FVector(15.0, UE_DOUBLE_SQRT_3 * 0.5 * HexRadius, 0.0), 1.e-9)
|
||||
&& FlatR.Equals(FVector(0.0, UE_DOUBLE_SQRT_3 * HexRadius, 0.0), 1.e-9));
|
||||
|
||||
const FVector HexOrigin(11.0, 13.0, 17.0);
|
||||
const FRotator HexRotation(23.0, 37.0, 11.0);
|
||||
const FVector HexPlaneNormal = HexRotation.Quaternion().GetAxisZ();
|
||||
for (const EDirectiveUtilHexOrientation HexOrientation : {
|
||||
EDirectiveUtilHexOrientation::PointyTop, EDirectiveUtilHexOrientation::FlatTop })
|
||||
{
|
||||
const FIntPoint Coordinate(-7, 4);
|
||||
const FVector Location = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation(
|
||||
Coordinate, HexOrigin, HexRotation, 25.0, HexOrientation, 3.0);
|
||||
TestEqual(TEXT("Hex coordinate conversion round trips through a rotated layout"),
|
||||
UDirectiveUtilMathFunctionLibrary::LocationToHexCoordinate(
|
||||
Location, HexOrigin, HexRotation, 25.0, HexOrientation, 3.0), Coordinate);
|
||||
TestEqual(TEXT("Location conversion projects onto the hex plane"),
|
||||
UDirectiveUtilMathFunctionLibrary::LocationToHexCoordinate(
|
||||
Location + HexPlaneNormal * 500.0, HexOrigin, HexRotation, 25.0, HexOrientation, 3.0), Coordinate);
|
||||
}
|
||||
|
||||
const FVector GappedHex = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation(
|
||||
FIntPoint(1, 0), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||
EDirectiveUtilHexOrientation::PointyTop, 2.0);
|
||||
TestTrue(TEXT("Hex gap adds to the adjacent edge distance"),
|
||||
FMath::IsNearlyEqual(GappedHex.Size(), UE_DOUBLE_SQRT_3 * HexRadius + 2.0, 1.e-9));
|
||||
|
||||
const TArray<FIntPoint> HexNeighbors = UDirectiveUtilMathFunctionLibrary::GetHexNeighbors(FIntPoint(3, -2));
|
||||
const TArray<FIntPoint> ExpectedHexNeighbors = {
|
||||
FIntPoint(4, -2), FIntPoint(4, -3), FIntPoint(3, -3),
|
||||
FIntPoint(2, -2), FIntPoint(2, -1), FIntPoint(3, -1)
|
||||
};
|
||||
TestTrue(TEXT("Hex neighbors use stable axial direction order"), HexNeighbors == ExpectedHexNeighbors);
|
||||
TestEqual(TEXT("Hex distance counts axial grid steps"),
|
||||
UDirectiveUtilMathFunctionLibrary::GetHexDistance(FIntPoint(0, 0), FIntPoint(3, -5)), 5LL);
|
||||
TestEqual(TEXT("Hex distance uses 64-bit intermediates"),
|
||||
UDirectiveUtilMathFunctionLibrary::GetHexDistance(
|
||||
FIntPoint(MAX_int32, MAX_int32), FIntPoint(MIN_int32, MIN_int32)), 8589934590LL);
|
||||
|
||||
const TArray<FVector> RectangularHexGrid = UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(3, 2), HexRadius,
|
||||
EDirectiveUtilHexOrientation::PointyTop, 0.0, true);
|
||||
TestTrue(TEXT("Rectangular hex grid is centered and ordered by row then column"),
|
||||
RectangularHexGrid.Num() == 6
|
||||
&& RectangularHexGrid[0].Equals(-RectangularHexGrid.Last(), 1.e-9)
|
||||
&& RectangularHexGrid[1].X < RectangularHexGrid[2].X
|
||||
&& RectangularHexGrid[2].Y < RectangularHexGrid[3].Y);
|
||||
const TArray<FVector> UncenteredHexGrid = UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid(
|
||||
HexOrigin, FRotator::ZeroRotator, FIntPoint(2, 2), HexRadius,
|
||||
EDirectiveUtilHexOrientation::FlatTop, 0.0, false);
|
||||
TestTrue(TEXT("Uncentered rectangular hex grid starts at its origin"),
|
||||
UncenteredHexGrid.Num() == 4 && UncenteredHexGrid[0] == HexOrigin);
|
||||
|
||||
const TArray<FVector> HexagonalGrid = UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, 2, HexRadius,
|
||||
EDirectiveUtilHexOrientation::PointyTop);
|
||||
TSet<FIntPoint> HexagonalCoordinates;
|
||||
bool bHexagonalGridValid = HexagonalGrid.Num() == 19;
|
||||
for (const FVector& Point : HexagonalGrid)
|
||||
{
|
||||
const FIntPoint Coordinate = UDirectiveUtilMathFunctionLibrary::LocationToHexCoordinate(
|
||||
Point, FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||
EDirectiveUtilHexOrientation::PointyTop);
|
||||
bHexagonalGridValid &= UDirectiveUtilMathFunctionLibrary::GetHexDistance(FIntPoint::ZeroValue, Coordinate) <= 2;
|
||||
HexagonalCoordinates.Add(Coordinate);
|
||||
}
|
||||
TestTrue(TEXT("Hexagonal grid contains every coordinate through its requested radius"),
|
||||
bHexagonalGridValid && HexagonalCoordinates.Num() == 19);
|
||||
TestEqual(TEXT("A zero-radius hexagonal grid contains its center"),
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid(
|
||||
HexOrigin, FRotator::ZeroRotator, 0, HexRadius).Num(), 1);
|
||||
TestTrue(TEXT("Hex generators reject invalid layouts and counts"),
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(0, 2), HexRadius).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(2, 2), 0.0).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, -1, HexRadius).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, 30000, HexRadius).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation(
|
||||
FIntPoint(1, 0), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||
EDirectiveUtilHexOrientation::PointyTop, -UE_DOUBLE_SQRT_3 * HexRadius).IsZero()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GetHexNeighbors(FIntPoint(MAX_int32, 0)).IsEmpty());
|
||||
|
||||
const TArray<FIntPoint> HexesInRange = UDirectiveUtilMathFunctionLibrary::GetHexesInRange(FIntPoint(2, -1), 1);
|
||||
const TArray<FIntPoint> ExpectedHexesInRange = {
|
||||
FIntPoint(2, -2), FIntPoint(3, -2), FIntPoint(1, -1), FIntPoint(2, -1),
|
||||
FIntPoint(3, -1), FIntPoint(1, 0), FIntPoint(2, 0)
|
||||
};
|
||||
TestTrue(TEXT("Hexes in range cover the center and its neighbors ordered by R then Q"),
|
||||
HexesInRange == ExpectedHexesInRange);
|
||||
const TArray<FIntPoint> HexagonalGridCoordinates =
|
||||
UDirectiveUtilMathFunctionLibrary::GetHexesInRange(FIntPoint::ZeroValue, 2);
|
||||
bool bHexagonalOrderValid = HexagonalGridCoordinates.Num() == HexagonalGrid.Num();
|
||||
for (int32 Index = 0; bHexagonalOrderValid && Index < HexagonalGrid.Num(); ++Index)
|
||||
{
|
||||
bHexagonalOrderValid &= HexagonalGrid[Index].Equals(
|
||||
UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation(
|
||||
HexagonalGridCoordinates[Index], FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||
EDirectiveUtilHexOrientation::PointyTop), 1.e-9);
|
||||
}
|
||||
TestTrue(TEXT("Hexes in range around zero pair with hexagonal grid cells by index"), bHexagonalOrderValid);
|
||||
|
||||
const TArray<FIntPoint> RectangularCoordinates =
|
||||
UDirectiveUtilMathFunctionLibrary::GetRectangularHexGridCoordinates(
|
||||
FIntPoint(2, 2), EDirectiveUtilHexOrientation::FlatTop);
|
||||
bool bRectangularOrderValid = RectangularCoordinates.Num() == UncenteredHexGrid.Num();
|
||||
for (int32 Index = 0; bRectangularOrderValid && Index < UncenteredHexGrid.Num(); ++Index)
|
||||
{
|
||||
bRectangularOrderValid &= UncenteredHexGrid[Index].Equals(
|
||||
UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation(
|
||||
RectangularCoordinates[Index], HexOrigin, FRotator::ZeroRotator, HexRadius,
|
||||
EDirectiveUtilHexOrientation::FlatTop), 1.e-9);
|
||||
}
|
||||
TestTrue(TEXT("Rectangular hex grid coordinates pair with grid cells by index"), bRectangularOrderValid);
|
||||
|
||||
const TArray<FIntPoint> HexRing = UDirectiveUtilMathFunctionLibrary::GetHexRing(FIntPoint(1, 1), 2);
|
||||
bool bRingValid = HexRing.Num() == 12;
|
||||
for (int32 Index = 0; bRingValid && Index < HexRing.Num(); ++Index)
|
||||
{
|
||||
bRingValid &= UDirectiveUtilMathFunctionLibrary::GetHexDistance(FIntPoint(1, 1), HexRing[Index]) == 2;
|
||||
bRingValid &= UDirectiveUtilMathFunctionLibrary::GetHexDistance(
|
||||
HexRing[Index], HexRing[(Index + 1) % HexRing.Num()]) == 1;
|
||||
}
|
||||
TestTrue(TEXT("A hex ring traces adjacent cells at the requested radius"), bRingValid);
|
||||
const TArray<FIntPoint> ZeroHexRing = UDirectiveUtilMathFunctionLibrary::GetHexRing(FIntPoint(4, 5), 0);
|
||||
TestTrue(TEXT("A zero-radius hex ring returns the center"),
|
||||
ZeroHexRing.Num() == 1 && ZeroHexRing[0] == FIntPoint(4, 5));
|
||||
|
||||
const TArray<FIntPoint> HexLine = UDirectiveUtilMathFunctionLibrary::GetHexLine(
|
||||
FIntPoint(0, 0), FIntPoint(3, -3));
|
||||
const TArray<FIntPoint> ExpectedHexLine = {
|
||||
FIntPoint(0, 0), FIntPoint(1, -1), FIntPoint(2, -2), FIntPoint(3, -3)
|
||||
};
|
||||
TestTrue(TEXT("A hex line follows a straight axial direction"), HexLine == ExpectedHexLine);
|
||||
const TArray<FIntPoint> DiagonalHexLine = UDirectiveUtilMathFunctionLibrary::GetHexLine(
|
||||
FIntPoint(-1, 2), FIntPoint(1, 3));
|
||||
bool bDiagonalLineValid = DiagonalHexLine.Num() == 4
|
||||
&& DiagonalHexLine[0] == FIntPoint(-1, 2) && DiagonalHexLine.Last() == FIntPoint(1, 3);
|
||||
for (int32 Index = 0; bDiagonalLineValid && Index < DiagonalHexLine.Num() - 1; ++Index)
|
||||
{
|
||||
bDiagonalLineValid &= UDirectiveUtilMathFunctionLibrary::GetHexDistance(
|
||||
DiagonalHexLine[Index], DiagonalHexLine[Index + 1]) == 1;
|
||||
}
|
||||
TestTrue(TEXT("A hex line steps through adjacent cells between its endpoints"), bDiagonalLineValid);
|
||||
const TArray<FIntPoint> SingleHexLine = UDirectiveUtilMathFunctionLibrary::GetHexLine(
|
||||
FIntPoint(7, -2), FIntPoint(7, -2));
|
||||
TestTrue(TEXT("A zero-length hex line returns its cell"),
|
||||
SingleHexLine.Num() == 1 && SingleHexLine[0] == FIntPoint(7, -2));
|
||||
|
||||
const TArray<FVector> HexCorners = UDirectiveUtilMathFunctionLibrary::GetHexCellCorners(
|
||||
FIntPoint::ZeroValue, FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||
EDirectiveUtilHexOrientation::PointyTop);
|
||||
bool bCornersValid = HexCorners.Num() == 6
|
||||
&& HexCorners[0].Equals(FVector(UE_DOUBLE_SQRT_3 * 0.5 * HexRadius, 0.5 * HexRadius, 0.0), 1.e-9);
|
||||
for (int32 Index = 0; bCornersValid && Index < 6; ++Index)
|
||||
{
|
||||
bCornersValid &= FMath::IsNearlyEqual(HexCorners[Index].Size(), HexRadius, 1.e-9);
|
||||
bCornersValid &= FMath::IsNearlyEqual(
|
||||
FVector::Distance(HexCorners[Index], HexCorners[(Index + 1) % 6]), HexRadius, 1.e-9);
|
||||
}
|
||||
TestTrue(TEXT("Pointy-top cell corners lie at the cell radius with matching side length"), bCornersValid);
|
||||
const TArray<FVector> FlatHexCorners = UDirectiveUtilMathFunctionLibrary::GetHexCellCorners(
|
||||
FIntPoint::ZeroValue, FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||
EDirectiveUtilHexOrientation::FlatTop);
|
||||
TestTrue(TEXT("Flat-top cell corners start on the local X axis"),
|
||||
FlatHexCorners.Num() == 6 && FlatHexCorners[0].Equals(FVector(HexRadius, 0.0, 0.0), 1.e-9));
|
||||
const TArray<FVector> GappedHexCorners = UDirectiveUtilMathFunctionLibrary::GetHexCellCorners(
|
||||
FIntPoint(1, 0), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||
EDirectiveUtilHexOrientation::PointyTop, 2.0);
|
||||
TestTrue(TEXT("Hex gap moves the cell center but not the corner distance"),
|
||||
GappedHexCorners.Num() == 6
|
||||
&& FMath::IsNearlyEqual(FVector::Distance(GappedHexCorners[0], GappedHex), HexRadius, 1.e-9));
|
||||
|
||||
TestTrue(TEXT("Hex queries reject invalid input"),
|
||||
UDirectiveUtilMathFunctionLibrary::GetHexesInRange(FIntPoint::ZeroValue, -1).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GetHexesInRange(FIntPoint(MAX_int32, 0), 1).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GetHexRing(FIntPoint::ZeroValue, -1).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GetHexRing(FIntPoint(MAX_int32, 0), 1).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GetHexLine(
|
||||
FIntPoint(MIN_int32, MIN_int32), FIntPoint(MAX_int32, MAX_int32)).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GetHexCellCorners(FIntPoint(1, 1), FVector::ZeroVector,
|
||||
FRotator::ZeroRotator, 0.0).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GetRectangularHexGridCoordinates(FIntPoint(0, 3)).IsEmpty());
|
||||
|
||||
const int32 MaximumGeneratedElementCount = UDirectiveUtilMathFunctionLibrary::MaximumGeneratedElementCount;
|
||||
TestEqual(
|
||||
TEXT("The maximum supported rectangular grid size remains available"),
|
||||
UDirectiveUtilMathFunctionLibrary::GetRectangularHexGridCoordinates(FIntPoint(1000, 1000)).Num(),
|
||||
MaximumGeneratedElementCount);
|
||||
TestTrue(TEXT("Generated collections reject the first unsupported count"),
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator,
|
||||
FIntPoint(MaximumGeneratedElementCount + 1, 1), FVector2D(1.0, 1.0)).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GetHexesInRange(FIntPoint::ZeroValue, 577).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GetHexRing(
|
||||
FIntPoint::ZeroValue, MaximumGeneratedElementCount / 6 + 1).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GetHexLine(
|
||||
FIntPoint::ZeroValue, FIntPoint(MaximumGeneratedElementCount, 0)).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection(
|
||||
FVector::ZeroVector, FVector::ForwardVector,
|
||||
MaximumGeneratedElementCount + 1, 1.0).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, 1.0,
|
||||
MaximumGeneratedElementCount + 1).IsEmpty());
|
||||
|
||||
const TArray<FVector> NoiseBase = {
|
||||
FVector::ZeroVector, FVector(37.0, 11.0, 5.0), FVector(250.0, -90.0, 40.0)
|
||||
};
|
||||
const TArray<FVector> NoisePoints = UDirectiveUtilMathFunctionLibrary::OffsetLocationsByNoise(
|
||||
NoiseBase, 100.0, 25.0);
|
||||
bool bNoiseValid = NoisePoints.Num() == 3;
|
||||
bool bAnyNoiseOffset = false;
|
||||
for (int32 Index = 0; bNoiseValid && Index < NoisePoints.Num(); ++Index)
|
||||
{
|
||||
const FVector NoiseDelta = NoisePoints[Index] - NoiseBase[Index];
|
||||
bNoiseValid &= FMath::IsNearlyZero(NoiseDelta.X) && FMath::IsNearlyZero(NoiseDelta.Y)
|
||||
&& FMath::Abs(NoiseDelta.Z) <= 25.0 + 1.e-6;
|
||||
bAnyNoiseOffset |= !FMath::IsNearlyZero(NoiseDelta.Z);
|
||||
}
|
||||
TestTrue(TEXT("Noise offsets displace along the requested direction within the amplitude"),
|
||||
bNoiseValid && bAnyNoiseOffset);
|
||||
TestTrue(TEXT("Noise offsets are deterministic"),
|
||||
UDirectiveUtilMathFunctionLibrary::OffsetLocationsByNoise(NoiseBase, 100.0, 25.0) == NoisePoints);
|
||||
TestTrue(TEXT("A zero noise amplitude leaves locations unchanged"),
|
||||
UDirectiveUtilMathFunctionLibrary::OffsetLocationsByNoise(NoiseBase, 100.0, 0.0) == NoiseBase);
|
||||
TestTrue(TEXT("Noise offsets reject invalid input"),
|
||||
UDirectiveUtilMathFunctionLibrary::OffsetLocationsByNoise(NoiseBase, 0.0, 25.0).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::OffsetLocationsByNoise(
|
||||
NoiseBase, 100.0, 25.0, FVector::ZeroVector).IsEmpty());
|
||||
|
||||
const TArray<FVector> DirectionPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection(
|
||||
FVector::ZeroVector, FVector(10.0, 0.0, 0.0), 4, 2.0, true);
|
||||
const TArray<FVector> ExpectedDirectionPoints = {
|
||||
FVector(-3.0, 0.0, 0.0), FVector(-1.0, 0.0, 0.0),
|
||||
FVector(1.0, 0.0, 0.0), FVector(3.0, 0.0, 0.0)
|
||||
};
|
||||
TestTrue(TEXT("Direction points normalize once and center around the origin"),
|
||||
PointsEqual(DirectionPoints, ExpectedDirectionPoints));
|
||||
const TArray<FVector> ReversedDirectionPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection(
|
||||
FVector::ZeroVector, FVector::ForwardVector, 3, -2.0, false);
|
||||
TestTrue(TEXT("Direction points preserve signed spacing"),
|
||||
ReversedDirectionPoints.Num() == 3
|
||||
&& ReversedDirectionPoints[0].Equals(FVector::ZeroVector)
|
||||
&& ReversedDirectionPoints[2].Equals(FVector(-4.0, 0.0, 0.0)));
|
||||
TestTrue(TEXT("Direction points reject a zero direction"),
|
||||
UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection(
|
||||
FVector::ZeroVector, FVector::ZeroVector, 3, 1.0, false).IsEmpty());
|
||||
|
||||
const TArray<FVector> SegmentPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations(
|
||||
FVector::ZeroVector, FVector(10.0, 0.0, 0.0), 3, true);
|
||||
TestTrue(TEXT("Segment points include exact endpoints"),
|
||||
SegmentPoints.Num() == 3
|
||||
&& SegmentPoints[0] == FVector::ZeroVector
|
||||
&& SegmentPoints[1].Equals(FVector(5.0, 0.0, 0.0))
|
||||
&& SegmentPoints[2] == FVector(10.0, 0.0, 0.0));
|
||||
const TArray<FVector> InteriorSegmentPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations(
|
||||
FVector::ZeroVector, FVector(9.0, 0.0, 0.0), 2, false);
|
||||
TestTrue(TEXT("Segment points can exclude both endpoints"),
|
||||
InteriorSegmentPoints.Num() == 2
|
||||
&& InteriorSegmentPoints[0].Equals(FVector(3.0, 0.0, 0.0))
|
||||
&& InteriorSegmentPoints[1].Equals(FVector(6.0, 0.0, 0.0)));
|
||||
const TArray<FVector> SingleSegmentPoint = UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations(
|
||||
FVector(2.0, 4.0, 6.0), FVector(6.0, 8.0, 10.0), 1, true);
|
||||
TestTrue(TEXT("A single segment point is the midpoint"),
|
||||
SingleSegmentPoint.Num() == 1 && SingleSegmentPoint[0].Equals(FVector(4.0, 6.0, 8.0)));
|
||||
|
||||
USplineComponent* Spline = NewObject<USplineComponent>();
|
||||
Spline->SetSplinePoints({ FVector::ZeroVector, FVector(100.0, 0.0, 0.0) },
|
||||
ESplineCoordinateSpace::Local, false);
|
||||
Spline->SetSplinePointType(0, ESplinePointType::Linear, false);
|
||||
Spline->SetSplinePointType(1, ESplinePointType::Linear, true);
|
||||
TestTrue(TEXT("Spline generators reject unsupported sample counts"),
|
||||
UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount(
|
||||
Spline, MaximumGeneratedElementCount + 1).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSplineByCount(
|
||||
Spline, MaximumGeneratedElementCount + 1).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||
Spline, 0.00005).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSpline(
|
||||
Spline, 0.00005).IsEmpty());
|
||||
const TArray<FVector> SplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||
Spline, 30.0, true, EDirectiveUtilSplineSpacingMode::Fixed);
|
||||
TestTrue(TEXT("Spline points use fixed spacing and append the exact open endpoint"),
|
||||
SplinePoints.Num() == 5
|
||||
&& SplinePoints[0].Equals(FVector::ZeroVector)
|
||||
&& SplinePoints[1].Equals(FVector(30.0, 0.0, 0.0), 1.e-4)
|
||||
&& SplinePoints[3].Equals(FVector(90.0, 0.0, 0.0), 1.e-4)
|
||||
&& SplinePoints[4].Equals(FVector(100.0, 0.0, 0.0), 1.e-4));
|
||||
TestEqual(TEXT("Spline endpoint can be excluded"),
|
||||
UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||
Spline, 30.0, false, EDirectiveUtilSplineSpacingMode::Fixed).Num(), 4);
|
||||
const TArray<FVector> EvenSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||
Spline, 30.0, true, EDirectiveUtilSplineSpacingMode::Even);
|
||||
TestTrue(TEXT("Even spline spacing divides the range without a short final interval"),
|
||||
EvenSplinePoints.Num() == 5
|
||||
&& EvenSplinePoints[1].Equals(FVector(25.0, 0.0, 0.0), 1.e-4)
|
||||
&& EvenSplinePoints[3].Equals(FVector(75.0, 0.0, 0.0), 1.e-4)
|
||||
&& EvenSplinePoints[4].Equals(FVector(100.0, 0.0, 0.0), 1.e-4));
|
||||
const TArray<FVector> RangedSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||
Spline, 25.0, true, EDirectiveUtilSplineSpacingMode::Fixed, ESplineCoordinateSpace::World, 20.0, 80.0);
|
||||
TestTrue(TEXT("Spline sampling honors a start and end distance"),
|
||||
RangedSplinePoints.Num() == 4
|
||||
&& RangedSplinePoints[0].Equals(FVector(20.0, 0.0, 0.0), 1.e-4)
|
||||
&& RangedSplinePoints[1].Equals(FVector(45.0, 0.0, 0.0), 1.e-4)
|
||||
&& RangedSplinePoints[3].Equals(FVector(80.0, 0.0, 0.0), 1.e-4));
|
||||
TestEqual(TEXT("Spline sampling drops a regular sample that lands on the endpoint"),
|
||||
UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||
Spline, 25.0, true, EDirectiveUtilSplineSpacingMode::Fixed, ESplineCoordinateSpace::World,
|
||||
0.0, 50.0 + 1.e-10).Num(), 3);
|
||||
const TArray<FVector> CountedSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount(
|
||||
Spline, 5, true);
|
||||
TestTrue(TEXT("Spline sampling by count includes both exact endpoints"),
|
||||
CountedSplinePoints.Num() == 5
|
||||
&& CountedSplinePoints[0].Equals(FVector::ZeroVector, 1.e-4)
|
||||
&& CountedSplinePoints[2].Equals(FVector(50.0, 0.0, 0.0), 1.e-4)
|
||||
&& CountedSplinePoints[4].Equals(FVector(100.0, 0.0, 0.0), 1.e-4));
|
||||
const TArray<FVector> InteriorSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount(
|
||||
Spline, 4, false);
|
||||
TestTrue(TEXT("Spline sampling by count can exclude both endpoints"),
|
||||
InteriorSplinePoints.Num() == 4
|
||||
&& InteriorSplinePoints[0].Equals(FVector(20.0, 0.0, 0.0), 1.e-4)
|
||||
&& InteriorSplinePoints[3].Equals(FVector(80.0, 0.0, 0.0), 1.e-4));
|
||||
const TArray<FVector> SingleSplinePoint = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount(
|
||||
Spline, 1, true);
|
||||
TestTrue(TEXT("A single spline point by count is the range midpoint"),
|
||||
SingleSplinePoint.Num() == 1 && SingleSplinePoint[0].Equals(FVector(50.0, 0.0, 0.0), 1.e-4));
|
||||
Spline->SetClosedLoop(true, true);
|
||||
const TArray<FVector> ClosedSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||
Spline, 30.0, true, EDirectiveUtilSplineSpacingMode::Fixed);
|
||||
TestTrue(TEXT("Closed spline sampling does not repeat its first point"),
|
||||
ClosedSplinePoints.Num() > 1 && !ClosedSplinePoints[0].Equals(ClosedSplinePoints.Last(), 1.e-4));
|
||||
const TArray<FVector> ClosedCountedPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount(
|
||||
Spline, 4, true);
|
||||
TestTrue(TEXT("Closed spline sampling by count spreads points around the loop"),
|
||||
ClosedCountedPoints.Num() == 4
|
||||
&& !ClosedCountedPoints[0].Equals(ClosedCountedPoints.Last(), 1.e-4));
|
||||
USplineComponent* SinglePointSpline = NewObject<USplineComponent>();
|
||||
SinglePointSpline->SetSplinePoints({ FVector(3.0, 4.0, 5.0) }, ESplineCoordinateSpace::Local, true);
|
||||
const TArray<FVector> ZeroLengthSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||
SinglePointSpline, 10.0);
|
||||
TestTrue(TEXT("A zero-length spline returns its only point"),
|
||||
ZeroLengthSplinePoints.Num() == 1 && ZeroLengthSplinePoints[0].Equals(FVector(3.0, 4.0, 5.0)));
|
||||
TestTrue(TEXT("Spline sampling rejects invalid input"),
|
||||
UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(nullptr, 10.0).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(Spline, 0.0).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||
Spline, 10.0, true, static_cast<EDirectiveUtilSplineSpacingMode>(255)).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||
Spline, 10.0, true, EDirectiveUtilSplineSpacingMode::Fixed,
|
||||
static_cast<ESplineCoordinateSpace::Type>(255)).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||
Spline, 10.0, true, EDirectiveUtilSplineSpacingMode::Fixed,
|
||||
ESplineCoordinateSpace::World, 80.0, 20.0).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount(Spline, 0).IsEmpty());
|
||||
|
||||
const TArray<FVector> CirclePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, 10.0, 4, 0.0);
|
||||
const TArray<FVector> ExpectedCirclePoints = {
|
||||
FVector(10.0, 0.0, 0.0), FVector(0.0, 10.0, 0.0),
|
||||
FVector(-10.0, 0.0, 0.0), FVector(0.0, -10.0, 0.0)
|
||||
};
|
||||
TestTrue(TEXT("Circle points are evenly spaced without repeating the first point"),
|
||||
PointsEqual(CirclePoints, ExpectedCirclePoints, 1.e-8));
|
||||
|
||||
const FRotator PlaneRotation(17.0, 31.0, 43.0);
|
||||
const FVector PlaneNormal = PlaneRotation.Quaternion().GetAxisZ();
|
||||
const FVector PlaneCenter(11.0, 13.0, 17.0);
|
||||
const TArray<FVector> RotatedCircle = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle(
|
||||
PlaneCenter, PlaneRotation, -7.0, 64, 15.0);
|
||||
bool bCirclePlaneValid = RotatedCircle.Num() == 64;
|
||||
for (const FVector& Point : RotatedCircle)
|
||||
{
|
||||
const FVector Offset = Point - PlaneCenter;
|
||||
bCirclePlaneValid &= FMath::IsNearlyEqual(Offset.Size(), 7.0, 1.e-8);
|
||||
bCirclePlaneValid &= FMath::IsNearlyZero(FVector::DotProduct(Offset, PlaneNormal), 1.e-8);
|
||||
}
|
||||
TestTrue(TEXT("Circle points honor rotation and negative radius"), bCirclePlaneValid);
|
||||
|
||||
const TArray<FVector> ArcPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, 10.0, 3, 0.0, 90.0, true);
|
||||
TestTrue(TEXT("Arc points include the requested endpoint"),
|
||||
ArcPoints.Num() == 3
|
||||
&& ArcPoints[0].Equals(FVector(10.0, 0.0, 0.0), 1.e-8)
|
||||
&& ArcPoints[1].Equals(FVector(UE_DOUBLE_INV_SQRT_2 * 10.0, UE_DOUBLE_INV_SQRT_2 * 10.0, 0.0), 1.e-8)
|
||||
&& ArcPoints[2].Equals(FVector(0.0, 10.0, 0.0), 1.e-8));
|
||||
const TArray<FVector> OpenArcPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, 10.0, 3, 0.0, 90.0, false);
|
||||
TestTrue(TEXT("Open arc points exclude the requested endpoint"),
|
||||
OpenArcPoints.Num() == 3
|
||||
&& OpenArcPoints.Last().Equals(
|
||||
FVector(FMath::Cos(UE_DOUBLE_PI / 3.0) * 10.0, FMath::Sin(UE_DOUBLE_PI / 3.0) * 10.0, 0.0), 1.e-8));
|
||||
|
||||
const TArray<FVector> DiscPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc(
|
||||
PlaneCenter, PlaneRotation, 25.0, 1024, 27.0);
|
||||
const TArray<FVector> RepeatedDiscPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc(
|
||||
PlaneCenter, PlaneRotation, 25.0, 1024, 27.0);
|
||||
bool bDiscValid = DiscPoints.Num() == 1024;
|
||||
for (const FVector& Point : DiscPoints)
|
||||
{
|
||||
const FVector Offset = Point - PlaneCenter;
|
||||
bDiscValid &= Offset.Size() < 25.0;
|
||||
bDiscValid &= FMath::IsNearlyZero(FVector::DotProduct(Offset, PlaneNormal), 1.e-8);
|
||||
}
|
||||
TestTrue(TEXT("Disc points are deterministic and remain inside the rotated disc"),
|
||||
bDiscValid && PointsEqual(DiscPoints, RepeatedDiscPoints));
|
||||
const TArray<FVector> SingleDiscPoint = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc(
|
||||
PlaneCenter, PlaneRotation, 25.0, 1, 27.0);
|
||||
TestTrue(TEXT("A single disc point is its center"),
|
||||
SingleDiscPoint.Num() == 1 && SingleDiscPoint[0] == PlaneCenter);
|
||||
|
||||
const TArray<FVector> SpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||
FVector::ZeroVector, PlaneRotation, 25.0, 1024, 27.0);
|
||||
const TArray<FVector> RepeatedSpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||
FVector::ZeroVector, PlaneRotation, 25.0, 1024, 27.0);
|
||||
FVector SphereMean = FVector::ZeroVector;
|
||||
bool bSphereValid = SpherePoints.Num() == 1024;
|
||||
for (const FVector& Point : SpherePoints)
|
||||
{
|
||||
bSphereValid &= FMath::IsNearlyEqual(Point.Size(), 25.0, 1.e-8);
|
||||
SphereMean += Point;
|
||||
}
|
||||
if (!SpherePoints.IsEmpty())
|
||||
{
|
||||
SphereMean /= SpherePoints.Num();
|
||||
}
|
||||
TestTrue(TEXT("Sphere points are deterministic and remain on the surface"),
|
||||
bSphereValid && SphereMean.Size() < 0.01 && PointsEqual(SpherePoints, RepeatedSpherePoints));
|
||||
|
||||
const FVector SphereCenter(-1200.0, 3400.0, -5600.0);
|
||||
const FRotator SphereRotation(-37.0, 123.0, 71.0);
|
||||
const FQuat SphereRotationQuaternion = SphereRotation.Quaternion();
|
||||
const TArray<FVector> LocalSpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, 17.0, 257, 23.5);
|
||||
const TArray<FVector> RotatedSpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||
SphereCenter, SphereRotation, 17.0, 257, 23.5);
|
||||
bool bSphereRotationValid = LocalSpherePoints.Num() == RotatedSpherePoints.Num();
|
||||
for (int32 Index = 0; Index < LocalSpherePoints.Num() && bSphereRotationValid; ++Index)
|
||||
{
|
||||
const FVector ExpectedPoint = SphereCenter
|
||||
+ SphereRotationQuaternion.RotateVector(LocalSpherePoints[Index]);
|
||||
bSphereRotationValid &= RotatedSpherePoints[Index].Equals(ExpectedPoint, 1.e-8);
|
||||
}
|
||||
TestTrue(TEXT("Sphere rotation transforms every local distribution point"), bSphereRotationValid);
|
||||
|
||||
constexpr double SphereAngleOffset = 1153.25;
|
||||
const TArray<FVector> UnoffsetSpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, 17.0, 257, 0.0);
|
||||
const TArray<FVector> OffsetSpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, 17.0, 257, SphereAngleOffset);
|
||||
const FQuat SphereOffsetRotation(FVector::UpVector,
|
||||
FMath::DegreesToRadians(FMath::Fmod(SphereAngleOffset, 360.0)));
|
||||
bool bSphereAngleOffsetValid = UnoffsetSpherePoints.Num() == OffsetSpherePoints.Num();
|
||||
for (int32 Index = 0; Index < UnoffsetSpherePoints.Num() && bSphereAngleOffsetValid; ++Index)
|
||||
{
|
||||
bSphereAngleOffsetValid &= OffsetSpherePoints[Index].Equals(
|
||||
SphereOffsetRotation.RotateVector(UnoffsetSpherePoints[Index]), 1.e-8);
|
||||
}
|
||||
TestTrue(TEXT("Sphere angle offset rotates the distribution around local Z"),
|
||||
bSphereAngleOffsetValid);
|
||||
|
||||
const TArray<FVector> SingleSpherePoint = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||
PlaneCenter, PlaneRotation, 25.0, 1, 27.0);
|
||||
TestTrue(TEXT("A single sphere point follows the rotated local Z axis"),
|
||||
SingleSpherePoint.Num() == 1
|
||||
&& SingleSpherePoint[0].Equals(PlaneCenter + PlaneNormal * 25.0, 1.e-8));
|
||||
|
||||
const FVector LargeTranslation(1000000.0, -2000000.0, 3000000.0);
|
||||
const FRotator AuditRotation(-37.0, 123.0, 71.0);
|
||||
TestTrue(TEXT("A translated center offsets every generated arc point exactly once"),
|
||||
PointsEqualAfterTranslation(
|
||||
UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc(
|
||||
FVector::ZeroVector, AuditRotation, -13.5, 11, 1080.25, -450.5, true),
|
||||
UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc(
|
||||
LargeTranslation, AuditRotation, -13.5, 11, 1080.25, -450.5, true),
|
||||
LargeTranslation));
|
||||
|
||||
TestTrue(TEXT("Translated origins preserve every spatial generator's local offsets"),
|
||||
PointsEqualAfterTranslation(
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D(
|
||||
FVector::ZeroVector, AuditRotation, FIntPoint(4, 3), FVector2D(-7.0, 11.0), true),
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D(
|
||||
LargeTranslation, AuditRotation, FIntPoint(4, 3), FVector2D(-7.0, 11.0), true),
|
||||
LargeTranslation)
|
||||
&& PointsEqualAfterTranslation(
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateGridPoints3D(
|
||||
FVector::ZeroVector, AuditRotation, FIntVector(3, 2, 2), FVector(5.0, -7.0, 0.0), false),
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateGridPoints3D(
|
||||
LargeTranslation, AuditRotation, FIntVector(3, 2, 2), FVector(5.0, -7.0, 0.0), false),
|
||||
LargeTranslation)
|
||||
&& PointsEqualAfterTranslation(
|
||||
UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection(
|
||||
FVector::ZeroVector, FVector(-2.0, 3.0, -5.0), 7, -3.25, true),
|
||||
UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection(
|
||||
LargeTranslation, FVector(-2.0, 3.0, -5.0), 7, -3.25, true),
|
||||
LargeTranslation)
|
||||
&& PointsEqualAfterTranslation(
|
||||
UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations(
|
||||
FVector(-11.0, 5.0, 8.0), FVector(17.0, -9.0, 3.0), 8, false),
|
||||
UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations(
|
||||
LargeTranslation + FVector(-11.0, 5.0, 8.0),
|
||||
LargeTranslation + FVector(17.0, -9.0, 3.0), 8, false),
|
||||
LargeTranslation)
|
||||
&& PointsEqualAfterTranslation(
|
||||
UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle(
|
||||
FVector::ZeroVector, AuditRotation, -13.5, 17, -725.25),
|
||||
UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle(
|
||||
LargeTranslation, AuditRotation, -13.5, 17, -725.25),
|
||||
LargeTranslation)
|
||||
&& PointsEqualAfterTranslation(
|
||||
UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc(
|
||||
FVector::ZeroVector, AuditRotation, -13.5, 257, 1080.25),
|
||||
UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc(
|
||||
LargeTranslation, AuditRotation, -13.5, 257, 1080.25),
|
||||
LargeTranslation)
|
||||
&& PointsEqualAfterTranslation(
|
||||
UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||
FVector::ZeroVector, AuditRotation, -13.5, 257, -1080.25),
|
||||
UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||
LargeTranslation, AuditRotation, -13.5, 257, -1080.25),
|
||||
LargeTranslation));
|
||||
|
||||
for (const EDirectiveUtilHexOrientation Orientation : {
|
||||
EDirectiveUtilHexOrientation::PointyTop, EDirectiveUtilHexOrientation::FlatTop })
|
||||
{
|
||||
TestTrue(TEXT("Translated origins preserve rectangular and hexagonal grid offsets"),
|
||||
PointsEqualAfterTranslation(
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid(
|
||||
FVector::ZeroVector, AuditRotation, FIntPoint(4, 3), 9.5, Orientation, -1.25, true),
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid(
|
||||
LargeTranslation, AuditRotation, FIntPoint(4, 3), 9.5, Orientation, -1.25, true),
|
||||
LargeTranslation)
|
||||
&& PointsEqualAfterTranslation(
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid(
|
||||
FVector::ZeroVector, AuditRotation, 4, 9.5, Orientation, -1.25),
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid(
|
||||
LargeTranslation, AuditRotation, 4, 9.5, Orientation, -1.25),
|
||||
LargeTranslation));
|
||||
|
||||
for (const FIntPoint Coordinate : {
|
||||
FIntPoint::ZeroValue, FIntPoint(-17, 29), FIntPoint(1234, -987), FIntPoint(-4096, -2048) })
|
||||
{
|
||||
const FVector Location = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation(
|
||||
Coordinate, LargeTranslation, AuditRotation, 9.5, Orientation, -1.25);
|
||||
TestEqual(TEXT("Odd signed hex coordinates round trip through translated rotated layouts"),
|
||||
UDirectiveUtilMathFunctionLibrary::LocationToHexCoordinate(
|
||||
Location, LargeTranslation, AuditRotation, 9.5, Orientation, -1.25), Coordinate);
|
||||
}
|
||||
}
|
||||
|
||||
const TArray<FVector> ZeroRadiusCircle = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle(
|
||||
LargeTranslation, AuditRotation, 0.0, 9, 123.0);
|
||||
const TArray<FVector> ZeroRadiusArc = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc(
|
||||
LargeTranslation, AuditRotation, 0.0, 9, -30.0, -720.0, false);
|
||||
const TArray<FVector> ZeroRadiusDisc = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc(
|
||||
LargeTranslation, AuditRotation, 0.0, 9, 123.0);
|
||||
const TArray<FVector> ZeroRadiusSphere = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||
LargeTranslation, AuditRotation, 0.0, 9, 123.0);
|
||||
TestTrue(TEXT("Zero-radius generators preserve their requested count at the center"),
|
||||
ZeroRadiusCircle.Num() == 9 && PointsMatchLocation(ZeroRadiusCircle, LargeTranslation)
|
||||
&& ZeroRadiusArc.Num() == 9 && PointsMatchLocation(ZeroRadiusArc, LargeTranslation)
|
||||
&& ZeroRadiusDisc.Num() == 9 && PointsMatchLocation(ZeroRadiusDisc, LargeTranslation)
|
||||
&& ZeroRadiusSphere.Num() == 9 && PointsMatchLocation(ZeroRadiusSphere, LargeTranslation));
|
||||
|
||||
TestTrue(TEXT("Degenerate linear generators preserve their requested count and location"),
|
||||
PointsMatchLocation(UDirectiveUtilMathFunctionLibrary::GenerateGridPoints3D(
|
||||
LargeTranslation, AuditRotation, FIntVector(2, 3, 4), FVector::ZeroVector, true), LargeTranslation)
|
||||
&& PointsMatchLocation(UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection(
|
||||
LargeTranslation, FVector(2.0, -3.0, 4.0), 7, 0.0, true), LargeTranslation)
|
||||
&& PointsMatchLocation(UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations(
|
||||
LargeTranslation, LargeTranslation, 7, false), LargeTranslation));
|
||||
|
||||
USplineComponent* TransformedSpline = NewObject<USplineComponent>();
|
||||
TransformedSpline->SetWorldLocation(LargeTranslation);
|
||||
TransformedSpline->SetWorldRotation(AuditRotation);
|
||||
TransformedSpline->SetWorldScale3D(FVector(2.0, 3.0, 0.5));
|
||||
TransformedSpline->SetSplinePoints({ FVector::ZeroVector, FVector(100.0, 0.0, 0.0) },
|
||||
ESplineCoordinateSpace::Local, false);
|
||||
TransformedSpline->SetSplinePointType(0, ESplinePointType::Linear, false);
|
||||
TransformedSpline->SetSplinePointType(1, ESplinePointType::Linear, true);
|
||||
const TArray<FVector> TransformedSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||
TransformedSpline, 60.0);
|
||||
TestEqual(TEXT("Scaled spline sampling produces the expected point count"), TransformedSplinePoints.Num(), 5);
|
||||
if (TransformedSplinePoints.Num() == 5)
|
||||
{
|
||||
TestTrue(TEXT("Spline sampling returns its world-space start"),
|
||||
TransformedSplinePoints[0].Equals(LargeTranslation, 1.e-4));
|
||||
TestTrue(TEXT("Spline sampling preserves regular world-space intervals"),
|
||||
FMath::IsNearlyEqual(FVector::Distance(TransformedSplinePoints[0], TransformedSplinePoints[1]), 60.0, 1.e-4)
|
||||
&& FMath::IsNearlyEqual(FVector::Distance(TransformedSplinePoints[1], TransformedSplinePoints[2]), 60.0, 1.e-4)
|
||||
&& FMath::IsNearlyEqual(FVector::Distance(TransformedSplinePoints[2], TransformedSplinePoints[3]), 60.0, 1.e-4));
|
||||
TestTrue(TEXT("Spline sampling appends its exact world-space endpoint"),
|
||||
TransformedSplinePoints.Last().Equals(TransformedSpline->GetLocationAtDistanceAlongSpline(
|
||||
TransformedSpline->GetSplineLength(), ESplineCoordinateSpace::World), 1.e-4));
|
||||
}
|
||||
const TArray<FVector> LocalSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||
TransformedSpline, 60.0, true, EDirectiveUtilSplineSpacingMode::Fixed, ESplineCoordinateSpace::Local);
|
||||
TestTrue(TEXT("Spline sampling can return local-space points"),
|
||||
LocalSplinePoints.Num() == TransformedSplinePoints.Num()
|
||||
&& LocalSplinePoints[0].Equals(FVector::ZeroVector, 1.e-4)
|
||||
&& LocalSplinePoints.Last().Equals(FVector(100.0, 0.0, 0.0), 1.e-4));
|
||||
|
||||
const double Infinity = std::numeric_limits<double>::infinity();
|
||||
const double NaN = std::numeric_limits<double>::quiet_NaN();
|
||||
const FRotator InvalidRotation(Infinity, 0.0, 0.0);
|
||||
TestTrue(TEXT("Point generators reject non-finite values"),
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(2, 2), FVector2D(Infinity, 1.0), true).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection(
|
||||
FVector::ZeroVector, FVector::ForwardVector, 2, Infinity, false).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle(
|
||||
FVector::ZeroVector, InvalidRotation, 1.0, 4, 0.0).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, 1.0, 4, 0.0, Infinity, true).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc(
|
||||
FVector(Infinity, 0.0, 0.0), FRotator::ZeroRotator, 1.0, 4, 0.0).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, Infinity, 4, 0.0).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(2, 2), Infinity).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(Spline, Infinity).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||
Spline, 10.0, true, EDirectiveUtilSplineSpacingMode::Fixed,
|
||||
ESplineCoordinateSpace::World, Infinity, -1.0).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount(
|
||||
Spline, 2, true, ESplineCoordinateSpace::World, 0.0, NaN).IsEmpty());
|
||||
TestTrue(TEXT("Every spatial generator rejects a non-finite origin or center"),
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateGridPoints3D(
|
||||
FVector(NaN, 0.0, 0.0), FRotator::ZeroRotator, FIntVector(1), FVector::OneVector, true).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations(
|
||||
FVector::ZeroVector, FVector(Infinity, 0.0, 0.0), 2, true).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc(
|
||||
FVector(NaN, 0.0, 0.0), FRotator::ZeroRotator, 1.0, 2, 0.0, 90.0, true).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid(
|
||||
FVector(Infinity, 0.0, 0.0), FRotator::ZeroRotator, 1, 1.0).IsEmpty());
|
||||
TestTrue(TEXT("Spline sampling rejects negative spacing"),
|
||||
UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(Spline, -1.0).IsEmpty());
|
||||
TestTrue(TEXT("Point generators return empty arrays for non-positive counts"),
|
||||
UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection(
|
||||
FVector::ZeroVector, FVector::ForwardVector, 0, 1.0, false).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations(
|
||||
FVector::ZeroVector, FVector::OneVector, -1, true).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle(
|
||||
FVector::ZeroVector, FRotator::ZeroRotator, 1.0, 0, 0.0).IsEmpty());
|
||||
|
||||
return !HasAnyErrors();
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilRegexFunctionLibrary.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilRegexFunctionLibraryTest, "DirectiveUtilities.RegexFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilRegexFunctionLibraryTest, "DirectiveUtilities.RegexFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilRegexFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilArrayFunctionLibrary.h"
|
||||
#include "Libraries/DirectiveUtilFunctionLibrary.h"
|
||||
#include "Libraries/DirectiveUtilGameplayTagFunctionLibrary.h"
|
||||
#include "Libraries/DirectiveUtilInputFunctionLibrary.h"
|
||||
#include "Libraries/DirectiveUtilMapFunctionLibrary.h"
|
||||
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
|
||||
#include "Libraries/DirectiveUtilRegexFunctionLibrary.h"
|
||||
#include "Libraries/DirectiveUtilSaveGameFunctionLibrary.h"
|
||||
#include "Libraries/DirectiveUtilStringFunctionLibrary.h"
|
||||
#include "Libraries/DirectiveUtilTextFunctionLibrary.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "Tasks/DirectiveUtilTask_AsyncLoadAsset.h"
|
||||
#include "Tasks/DirectiveUtilTask_AsyncTrace.h"
|
||||
#include "Tasks/DirectiveUtilTask_Delay.h"
|
||||
#include "Tasks/DirectiveUtilTask_Flow.h"
|
||||
#include "Tasks/DirectiveUtilTask_MoveToLocation.h"
|
||||
#include "UObject/Class.h"
|
||||
#include "UObject/Package.h"
|
||||
#include "UObject/UnrealType.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilRuntimeSurfaceTest,
|
||||
"DirectiveUtilities.Runtime.Surface",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilRuntimeSurfaceTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
struct FClassExpectation
|
||||
{
|
||||
UClass* Class;
|
||||
};
|
||||
|
||||
const FClassExpectation Expectations[] = {
|
||||
{ UDirectiveUtilArrayFunctionLibrary::StaticClass() },
|
||||
{ UDirectiveUtilFunctionLibrary::StaticClass() },
|
||||
{ UDirectiveUtilGameplayTagFunctionLibrary::StaticClass() },
|
||||
{ UDirectiveUtilInputFunctionLibrary::StaticClass() },
|
||||
{ UDirectiveUtilMapFunctionLibrary::StaticClass() },
|
||||
{ UDirectiveUtilMathFunctionLibrary::StaticClass() },
|
||||
{ UDirectiveUtilRegexFunctionLibrary::StaticClass() },
|
||||
{ UDirectiveUtilSaveGameFunctionLibrary::StaticClass() },
|
||||
{ UDirectiveUtilStringFunctionLibrary::StaticClass() },
|
||||
{ UDirectiveUtilTextFunctionLibrary::StaticClass() },
|
||||
{ UDirectiveUtilTask_AsyncLoadAsset::StaticClass() },
|
||||
{ UDirectiveUtilTask_AsyncLoadClass::StaticClass() },
|
||||
{ UDirectiveUtilTask_AsyncLoadAssets::StaticClass() },
|
||||
{ UDirectiveUtilTask_AsyncTrace::StaticClass() },
|
||||
{ UDirectiveUtilTask_Delay::StaticClass() },
|
||||
{ UDirectiveUtilTask_UpdateForDuration::StaticClass() },
|
||||
{ UDirectiveUtilTask_RepeatWithInterval::StaticClass() },
|
||||
{ UDirectiveUtilTask_MoveToLocation::StaticClass() },
|
||||
{ UDirectiveUtilTask_MoveToActor::StaticClass() }
|
||||
};
|
||||
|
||||
TestTrue(TEXT("The runtime module is loaded"), FModuleManager::Get().IsModuleLoaded(TEXT("DirectiveUtilitiesRuntime")));
|
||||
|
||||
#if !WITH_EDITOR
|
||||
TestFalse(TEXT("The editor module is absent from a game target"), FModuleManager::Get().IsModuleLoaded(TEXT("DirectiveUtilitiesEditor")));
|
||||
TestFalse(TEXT("The uncooked node module is absent from a game target"), FModuleManager::Get().IsModuleLoaded(TEXT("DirectiveUtilitiesBlueprintNodes")));
|
||||
#endif
|
||||
|
||||
for (const FClassExpectation& Expectation : Expectations)
|
||||
{
|
||||
TestNotNull(TEXT("Runtime class is reflected"), Expectation.Class);
|
||||
if (!Expectation.Class)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TestEqual(
|
||||
FString::Printf(TEXT("%s belongs to the runtime script package"), *Expectation.Class->GetName()),
|
||||
Expectation.Class->GetOutermost()->GetName(),
|
||||
FString(TEXT("/Script/DirectiveUtilitiesRuntime")));
|
||||
|
||||
TestFalse(
|
||||
FString::Printf(TEXT("%s is not in an editor-only package"), *Expectation.Class->GetName()),
|
||||
Expectation.Class->GetOutermost()->HasAnyPackageFlags(PKG_EditorOnly | PKG_UncookedOnly | PKG_Developer));
|
||||
|
||||
for (TFieldIterator<UFunction> FunctionIterator(Expectation.Class, EFieldIteratorFlags::ExcludeSuper); FunctionIterator; ++FunctionIterator)
|
||||
{
|
||||
const UFunction* Function = *FunctionIterator;
|
||||
if (!Function->HasAnyFunctionFlags(FUNC_BlueprintCallable | FUNC_BlueprintPure))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TestFalse(
|
||||
FString::Printf(TEXT("%s.%s is available outside the editor"), *Expectation.Class->GetName(), *Function->GetName()),
|
||||
Function->HasAnyFunctionFlags(FUNC_EditorOnly));
|
||||
|
||||
#if WITH_EDITOR
|
||||
const FString Category = Function->GetMetaData(TEXT("Category"));
|
||||
TestTrue(
|
||||
FString::Printf(TEXT("%s.%s has a Directive Utilities category"), *Expectation.Class->GetName(), *Function->GetName()),
|
||||
Category == TEXT("Directive Utilities") || Category.StartsWith(TEXT("Directive Utilities|")));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
return !HasAnyErrors();
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilSaveGameFunctionLibrary.h"
|
||||
#include "Tests/DirectiveUtilTestObject.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "GameFramework/SaveGame.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilSaveGameFunctionLibraryTest, "DirectiveUtilities.SaveGameFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilSaveGameFunctionLibraryTest, "DirectiveUtilities.SaveGameFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilSaveGameFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
@@ -84,6 +86,11 @@ bool FDirectiveUtilSaveGameFunctionLibraryTest::RunTest(const FString& Parameter
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotB));
|
||||
TestNotNull("A renamed slot should still deserialize",
|
||||
UGameplayStatics::LoadGameFromSlot(SlotB, 0));
|
||||
const FString SlotBCaseVariant = SlotB.ToLower();
|
||||
TestTrue("RenameSaveSlot should accept a case-only spelling change",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(SlotB, SlotBCaseVariant));
|
||||
TestNotNull("A case-only renamed slot should still deserialize",
|
||||
UGameplayStatics::LoadGameFromSlot(SlotBCaseVariant, 0));
|
||||
|
||||
USaveGame* OtherSave = NewObject<UDirectiveUtilTestSaveGame>();
|
||||
TestTrue("SaveGameToSlot should write the collision test slot",
|
||||
@@ -99,12 +106,18 @@ bool FDirectiveUtilSaveGameFunctionLibraryTest::RunTest(const FString& Parameter
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(TEXT("../escape")));
|
||||
TestFalse("DeleteSaveSlot should reject an invalid slot name",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DeleteSaveSlot(TEXT("../escape")));
|
||||
TestFalse("DoesSaveSlotExist should reject a nested slot path",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(TEXT("Profiles/Slot1")));
|
||||
TestFalse("RenameSaveSlot should reject an invalid source slot name",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(TEXT("../escape"), SlotC));
|
||||
TestFalse("RenameSaveSlot should reject an invalid destination slot name",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(SlotC, TEXT("../escape")));
|
||||
TestTrue("A rejected rename should leave the source slot intact",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotC));
|
||||
TestFalse("DoesSaveSlotExist should reject the reserved device name CON",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(TEXT("CON")));
|
||||
TestFalse("DoesSaveSlotExist should reject CON with an extension",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(TEXT("CON.sav")));
|
||||
|
||||
TestTrue("DeleteSaveSlot should delete an existing slot",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DeleteSaveSlot(SlotB));
|
||||
@@ -115,6 +128,7 @@ bool FDirectiveUtilSaveGameFunctionLibraryTest::RunTest(const FString& Parameter
|
||||
// Clean up
|
||||
UGameplayStatics::DeleteGameInSlot(SlotA, 0);
|
||||
UGameplayStatics::DeleteGameInSlot(SlotB, 0);
|
||||
UGameplayStatics::DeleteGameInSlot(SlotB.ToLower(), 0);
|
||||
UGameplayStatics::DeleteGameInSlot(SlotC, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,37 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilStringFunctionLibrary.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilStringFunctionLibraryTest, "DirectiveUtilities.StringFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
namespace
|
||||
{
|
||||
int32 ReferenceLevenshteinDistance(const FString& Left, const FString& Right)
|
||||
{
|
||||
TArray<int32> PreviousRow;
|
||||
TArray<int32> CurrentRow;
|
||||
PreviousRow.SetNumUninitialized(Right.Len() + 1);
|
||||
CurrentRow.SetNumUninitialized(Right.Len() + 1);
|
||||
for (int32 ColumnIndex = 0; ColumnIndex <= Right.Len(); ++ColumnIndex)
|
||||
{
|
||||
PreviousRow[ColumnIndex] = ColumnIndex;
|
||||
}
|
||||
for (int32 RowIndex = 1; RowIndex <= Left.Len(); ++RowIndex)
|
||||
{
|
||||
CurrentRow[0] = RowIndex;
|
||||
for (int32 ColumnIndex = 1; ColumnIndex <= Right.Len(); ++ColumnIndex)
|
||||
{
|
||||
CurrentRow[ColumnIndex] = FMath::Min3(
|
||||
PreviousRow[ColumnIndex] + 1,
|
||||
CurrentRow[ColumnIndex - 1] + 1,
|
||||
PreviousRow[ColumnIndex - 1] + (Left[RowIndex - 1] == Right[ColumnIndex - 1] ? 0 : 1));
|
||||
}
|
||||
Swap(PreviousRow, CurrentRow);
|
||||
}
|
||||
return PreviousRow.Last();
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilStringFunctionLibraryTest, "DirectiveUtilities.StringFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilStringFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
@@ -232,11 +262,26 @@ bool FDirectiveUtilStringFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
TestFalse("IsValidFileName should reject a backslash", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("a\\b")));
|
||||
TestFalse("IsValidFileName should reject an empty string", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("")));
|
||||
TestFalse("IsValidFileName should reject a reserved character", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("a:b")));
|
||||
TestFalse("IsValidFileName should reject the current-directory segment", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT(".")));
|
||||
TestFalse("IsValidFileName should reject the parent-directory segment", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("..")));
|
||||
TestFalse("IsValidFileName should reject the reserved device name CON", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("CON")));
|
||||
TestFalse("IsValidFileName should reject CON with an extension", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("CON.sav")));
|
||||
TestFalse("IsValidFileName should reject NUL case-insensitively", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("nul")));
|
||||
TestFalse("IsValidFileName should reject COM1", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("COM1")));
|
||||
TestFalse("IsValidFileName should reject LPT9", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("LPT9")));
|
||||
TestFalse("IsValidFileName should reject a trailing dot", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("trailing.")));
|
||||
TestFalse("IsValidFileName should reject a run of dots", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("...")));
|
||||
TestTrue("IsValidFileName should accept console, which is not a reserved device name",
|
||||
UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("console")));
|
||||
|
||||
{
|
||||
const FString Sanitized = UDirectiveUtilStringFunctionLibrary::SanitizeFileName(TEXT("../a/b?.sav"));
|
||||
TestTrue("SanitizeFileName should produce a name that IsValidFileName accepts",
|
||||
UDirectiveUtilStringFunctionLibrary::IsValidFileName(Sanitized));
|
||||
const FString SanitizedReserved = UDirectiveUtilStringFunctionLibrary::SanitizeFileName(TEXT("CON"));
|
||||
TestTrue("SanitizeFileName should rewrite a reserved device name into a valid file name",
|
||||
UDirectiveUtilStringFunctionLibrary::IsValidFileName(SanitizedReserved)
|
||||
&& SanitizedReserved == TEXT("_CON"));
|
||||
const FString Replaced = UDirectiveUtilStringFunctionLibrary::SanitizeFileName(TEXT("a/b"), TEXT("_"));
|
||||
TestTrue("SanitizeFileName should substitute the replacement character for stripped characters",
|
||||
Replaced.Contains(TEXT("_")));
|
||||
@@ -271,6 +316,43 @@ bool FDirectiveUtilStringFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(TEXT(""), {TEXT("abc")}, Similarity), 0);
|
||||
TestTrue("FindBestStringMatch should report similarity 0 for an empty input against a non-empty candidate",
|
||||
FMath::IsNearlyEqual(Similarity, 0.0f, 1.e-4f));
|
||||
|
||||
TestEqual("FindBestStringMatch should keep the first equally close candidate",
|
||||
UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(TEXT("cat"), {TEXT("bat"), TEXT("hat")}, Similarity), 0);
|
||||
}
|
||||
|
||||
TestEqual("Levenshtein should trim equal prefixes and suffixes without changing the result",
|
||||
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(TEXT("shared-prefix-A-shared-suffix"), TEXT("shared-prefix-B-shared-suffix")), 1);
|
||||
TestEqual("Levenshtein should support Unicode code units",
|
||||
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(TEXT("café-one"), TEXT("café-two")), 3);
|
||||
|
||||
FRandomStream LevenshteinStream(90210);
|
||||
for (int32 Iteration = 0; Iteration < 200; ++Iteration)
|
||||
{
|
||||
FString Left;
|
||||
FString Right;
|
||||
const int32 LeftLength = LevenshteinStream.RandRange(0, 64);
|
||||
const int32 RightLength = LevenshteinStream.RandRange(0, 64);
|
||||
Left.Reserve(LeftLength);
|
||||
Right.Reserve(RightLength);
|
||||
for (int32 Index = 0; Index < LeftLength; ++Index)
|
||||
{
|
||||
Left.AppendChar(static_cast<TCHAR>(TEXT('a') + LevenshteinStream.RandRange(0, 5)));
|
||||
}
|
||||
for (int32 Index = 0; Index < RightLength; ++Index)
|
||||
{
|
||||
Right.AppendChar(static_cast<TCHAR>(TEXT('a') + LevenshteinStream.RandRange(0, 5)));
|
||||
}
|
||||
|
||||
const int32 ExpectedDistance = ReferenceLevenshteinDistance(Left, Right);
|
||||
TestEqual(
|
||||
FString::Printf(TEXT("Levenshtein fuzz case %d"), Iteration),
|
||||
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(Left, Right),
|
||||
ExpectedDistance);
|
||||
TestEqual(
|
||||
FString::Printf(TEXT("Levenshtein symmetry case %d"), Iteration),
|
||||
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(Right, Left),
|
||||
ExpectedDistance);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilStringFunctionLibrary.h"
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilStringCardinalityTest,
|
||||
"DirectiveUtilities.StringScenarios.Cardinality",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilStringCardinalityTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
for (const int32 CharacterCount : {0, 1, 2, 31, 32, 255, 256, 1024})
|
||||
{
|
||||
FString Source;
|
||||
Source.Reserve(CharacterCount);
|
||||
for (int32 Index = 0; Index < CharacterCount; ++Index)
|
||||
{
|
||||
Source.AppendChar(TEXT('a'));
|
||||
}
|
||||
|
||||
const FString Label = FString::Printf(TEXT("characters=%d"), CharacterCount);
|
||||
TestEqual(
|
||||
Label + TEXT(" identical"),
|
||||
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(Source, Source),
|
||||
0);
|
||||
TestEqual(
|
||||
Label + TEXT(" empty comparison"),
|
||||
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(Source, FString()),
|
||||
CharacterCount);
|
||||
|
||||
if (!Source.IsEmpty())
|
||||
{
|
||||
for (const int32 ChangedIndex : {0, CharacterCount / 2, CharacterCount - 1})
|
||||
{
|
||||
FString Changed = Source;
|
||||
Changed[ChangedIndex] = TEXT('b');
|
||||
TestEqual(
|
||||
FString::Printf(TEXT("characters=%d changed=%d"), CharacterCount, ChangedIndex),
|
||||
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(Source, Changed),
|
||||
1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const int32 CandidateCount : {0, 1, 2, 31, 32, 256, 1000})
|
||||
{
|
||||
TArray<FString> Candidates;
|
||||
Candidates.Reserve(CandidateCount);
|
||||
for (int32 Index = 0; Index < CandidateCount; ++Index)
|
||||
{
|
||||
Candidates.Add(FString::Printf(TEXT("Candidate%04d"), Index));
|
||||
}
|
||||
|
||||
float Similarity = -1.0f;
|
||||
if (Candidates.IsEmpty())
|
||||
{
|
||||
TestEqual(
|
||||
"empty candidate set index",
|
||||
UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(TEXT("Candidate0000"), Candidates, Similarity),
|
||||
INDEX_NONE);
|
||||
TestEqual("empty candidate set similarity", Similarity, 0.0f);
|
||||
continue;
|
||||
}
|
||||
|
||||
const TArray<int32> ExpectedIndices = {0, CandidateCount / 2, CandidateCount - 1};
|
||||
for (const int32 ExpectedIndex : ExpectedIndices)
|
||||
{
|
||||
Similarity = -1.0f;
|
||||
const int32 MatchIndex = UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(
|
||||
Candidates[ExpectedIndex],
|
||||
Candidates,
|
||||
Similarity);
|
||||
const FString Label = FString::Printf(TEXT("candidates=%d expected=%d"), CandidateCount, ExpectedIndex);
|
||||
TestEqual(Label + TEXT(" index"), MatchIndex, ExpectedIndex);
|
||||
TestEqual(Label + TEXT(" similarity"), Similarity, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilTextFunctionLibrary.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilTextFunctionLibraryTest, "DirectiveUtilities.TextFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilTextFunctionLibraryTest, "DirectiveUtilities.TextFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilTextFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,485 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
|
||||
|
||||
#include "Components/SplineComponent.h"
|
||||
#include "Math/RotationMatrix.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FDirectiveUtilTransformArrayTest,
|
||||
"DirectiveUtilities.Math.TransformArrays",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilTransformArrayTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
const TArray<FVector> Locations = {
|
||||
FVector(1.0, 2.0, 3.0),
|
||||
FVector(4.0, 5.0, 6.0),
|
||||
FVector(7.0, 8.0, 9.0)
|
||||
};
|
||||
const FRotator SharedRotator(10.0, 20.0, 30.0);
|
||||
const FQuat SharedRotation = SharedRotator.Quaternion();
|
||||
const FVector SharedScale(2.0, 3.0, 4.0);
|
||||
|
||||
const TArray<FTransform> SharedTransforms = UDirectiveUtilMathFunctionLibrary::LocationsToTransforms(
|
||||
Locations, SharedRotator, SharedScale);
|
||||
bool bSharedTransformsValid = SharedTransforms.Num() == Locations.Num();
|
||||
for (int32 Index = 0; Index < SharedTransforms.Num(); ++Index)
|
||||
{
|
||||
bSharedTransformsValid &= SharedTransforms[Index].GetLocation() == Locations[Index];
|
||||
bSharedTransformsValid &= SharedTransforms[Index].GetRotation().Equals(SharedRotation, 1.e-12);
|
||||
bSharedTransformsValid &= SharedTransforms[Index].GetScale3D() == SharedScale;
|
||||
}
|
||||
TestTrue(TEXT("Locations to transforms preserves order and broadcasts rotation and scale"),
|
||||
bSharedTransformsValid);
|
||||
TestTrue(TEXT("Locations to transforms accepts an empty location array"),
|
||||
UDirectiveUtilMathFunctionLibrary::LocationsToTransforms(
|
||||
{}, SharedRotator, SharedScale).IsEmpty());
|
||||
|
||||
TArray<FTransform> Transforms;
|
||||
TestTrue(TEXT("Empty attribute arrays use identity rotation and scale"),
|
||||
UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays(Locations, {}, {}, Transforms));
|
||||
bool bIdentityAttributesValid = Transforms.Num() == Locations.Num();
|
||||
for (int32 Index = 0; Index < Transforms.Num(); ++Index)
|
||||
{
|
||||
bIdentityAttributesValid &= Transforms[Index].GetLocation() == Locations[Index];
|
||||
bIdentityAttributesValid &= Transforms[Index].GetRotation().Equals(FQuat::Identity, 1.e-12);
|
||||
bIdentityAttributesValid &= Transforms[Index].GetScale3D() == FVector::OneVector;
|
||||
}
|
||||
TestTrue(TEXT("Identity attributes preserve every location"), bIdentityAttributesValid);
|
||||
|
||||
TestTrue(TEXT("Single attribute values broadcast across the location array"),
|
||||
UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays(
|
||||
Locations, { SharedRotator }, { SharedScale }, Transforms));
|
||||
bool bBroadcastAttributesValid = Transforms.Num() == Locations.Num();
|
||||
for (const FTransform& Transform : Transforms)
|
||||
{
|
||||
bBroadcastAttributesValid &= Transform.GetRotation().Equals(SharedRotation, 1.e-12);
|
||||
bBroadcastAttributesValid &= Transform.GetScale3D() == SharedScale;
|
||||
}
|
||||
TestTrue(TEXT("Broadcast attributes are applied to every transform"), bBroadcastAttributesValid);
|
||||
|
||||
const TArray<FRotator> Rotations = {
|
||||
FRotator::ZeroRotator,
|
||||
FRotator(0.0, 90.0, 0.0),
|
||||
FRotator(45.0, 0.0, 0.0)
|
||||
};
|
||||
const TArray<FVector> Scales = {
|
||||
FVector::OneVector,
|
||||
FVector(2.0),
|
||||
FVector(-1.0, 1.0, 0.5)
|
||||
};
|
||||
TestTrue(TEXT("Full attribute arrays map element by element"),
|
||||
UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays(
|
||||
Locations, Rotations, Scales, Transforms));
|
||||
bool bPerTransformAttributesValid = Transforms.Num() == Locations.Num();
|
||||
for (int32 Index = 0; Index < Transforms.Num(); ++Index)
|
||||
{
|
||||
bPerTransformAttributesValid &= Transforms[Index].GetLocation() == Locations[Index];
|
||||
bPerTransformAttributesValid &= Transforms[Index].GetRotation().Equals(
|
||||
Rotations[Index].Quaternion(), 1.e-12);
|
||||
bPerTransformAttributesValid &= Transforms[Index].GetScale3D() == Scales[Index];
|
||||
}
|
||||
TestTrue(TEXT("Per-transform attributes preserve index alignment"), bPerTransformAttributesValid);
|
||||
|
||||
Transforms = { FTransform::Identity };
|
||||
TestFalse(TEXT("Mismatched rotation counts are rejected"),
|
||||
UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays(
|
||||
Locations, { FRotator::ZeroRotator, FRotator::ZeroRotator }, {}, Transforms));
|
||||
TestTrue(TEXT("A rejected attribute count clears the output"), Transforms.IsEmpty());
|
||||
TestFalse(TEXT("Mismatched scale counts are rejected"),
|
||||
UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays(
|
||||
Locations, {}, { FVector::OneVector, FVector::OneVector }, Transforms));
|
||||
TestTrue(TEXT("A rejected scale count clears the output"), Transforms.IsEmpty());
|
||||
|
||||
const double Infinity = std::numeric_limits<double>::infinity();
|
||||
const FRotator InvalidRotation(Infinity, 0.0, 0.0);
|
||||
TestTrue(TEXT("Locations to transforms rejects non-finite values"),
|
||||
UDirectiveUtilMathFunctionLibrary::LocationsToTransforms(
|
||||
{ FVector(Infinity, 0.0, 0.0) }, FRotator::ZeroRotator, FVector::OneVector).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::LocationsToTransforms(
|
||||
Locations, InvalidRotation, FVector::OneVector).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::LocationsToTransforms(
|
||||
Locations, FRotator::ZeroRotator, FVector(Infinity)).IsEmpty());
|
||||
TestFalse(TEXT("Transform arrays reject a non-finite location"),
|
||||
UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays(
|
||||
{ FVector::ZeroVector, FVector(Infinity) }, {}, {}, Transforms));
|
||||
TestTrue(TEXT("A non-finite location clears partial output"), Transforms.IsEmpty());
|
||||
TestFalse(TEXT("Transform arrays reject a non-finite rotation"),
|
||||
UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays(
|
||||
Locations, { InvalidRotation }, {}, Transforms));
|
||||
TestTrue(TEXT("A non-finite rotation leaves no output"), Transforms.IsEmpty());
|
||||
TestFalse(TEXT("Transform arrays reject a non-finite scale"),
|
||||
UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays(
|
||||
Locations, {}, { FVector(Infinity) }, Transforms));
|
||||
TestTrue(TEXT("A non-finite scale leaves no output"), Transforms.IsEmpty());
|
||||
|
||||
TestTrue(TEXT("Empty locations produce a valid empty transform array"),
|
||||
UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays(
|
||||
{}, { SharedRotator }, { SharedScale }, Transforms));
|
||||
TestTrue(TEXT("An empty transform result contains no values"), Transforms.IsEmpty());
|
||||
|
||||
const FVector LargeLocation(1000000.0, -2000000.0, 3000000.0);
|
||||
const FRotator WrappedRotation(-1080.0, 1440.0, 720.0);
|
||||
const FVector SignedScale(-2.0, 0.0, 4.0);
|
||||
const TArray<FTransform> OddTransforms = UDirectiveUtilMathFunctionLibrary::LocationsToTransforms(
|
||||
{LargeLocation, -LargeLocation}, WrappedRotation, SignedScale);
|
||||
TestTrue(TEXT("Locations to transforms preserves large locations and signed scales"),
|
||||
OddTransforms.Num() == 2
|
||||
&& OddTransforms[0].GetLocation() == LargeLocation
|
||||
&& OddTransforms[1].GetLocation() == -LargeLocation
|
||||
&& OddTransforms[0].GetScale3D() == SignedScale
|
||||
&& OddTransforms[1].GetScale3D() == SignedScale
|
||||
&& OddTransforms[0].GetRotation().Equals(WrappedRotation.Quaternion(), 1.e-12));
|
||||
|
||||
const FVector SingleLocation(-7.0, 11.0, -13.0);
|
||||
const FRotator SingleRotation(17.0, -29.0, 43.0);
|
||||
const FVector SingleScale(0.0, -1.0, 2.0);
|
||||
TestTrue(TEXT("Single-element attribute arrays map without special-case drift"),
|
||||
UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays(
|
||||
{SingleLocation}, {SingleRotation}, {SingleScale}, Transforms)
|
||||
&& Transforms.Num() == 1
|
||||
&& Transforms[0].GetLocation() == SingleLocation
|
||||
&& Transforms[0].GetRotation().Equals(SingleRotation.Quaternion(), 1.e-12)
|
||||
&& Transforms[0].GetScale3D() == SingleScale);
|
||||
|
||||
Transforms = {FTransform::Identity};
|
||||
TestFalse(TEXT("Empty locations still reject an impossible rotation count"),
|
||||
UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays(
|
||||
{}, {FRotator::ZeroRotator, SharedRotator}, {}, Transforms));
|
||||
TestTrue(TEXT("Rejected empty-location attributes clear the output"), Transforms.IsEmpty());
|
||||
Transforms = {FTransform::Identity};
|
||||
TestFalse(TEXT("Empty locations still reject an impossible scale count"),
|
||||
UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays(
|
||||
{}, {}, {FVector::OneVector, SharedScale}, Transforms));
|
||||
TestTrue(TEXT("Rejected empty-location scales clear the output"), Transforms.IsEmpty());
|
||||
|
||||
const FVector FacingTarget(100.0, -200.0, 300.0);
|
||||
const TArray<FVector> FacingLocations = {
|
||||
FacingTarget + FVector(10.0, 0.0, 0.0),
|
||||
FacingTarget + FVector(0.0, -20.0, 0.0),
|
||||
FacingTarget
|
||||
};
|
||||
const TArray<FTransform> FacingTransforms =
|
||||
UDirectiveUtilMathFunctionLibrary::LocationsToFacingTransforms(
|
||||
FacingLocations, FacingTarget, FVector::UpVector, FRotator::ZeroRotator, SharedScale, false);
|
||||
TestTrue(TEXT("Facing transforms preserve locations and face their target"),
|
||||
FacingTransforms.Num() == 3
|
||||
&& FacingTransforms[0].GetLocation() == FacingLocations[0]
|
||||
&& FacingTransforms[0].GetRotation().GetAxisX().Equals(FVector::BackwardVector, 1.e-8)
|
||||
&& FacingTransforms[1].GetRotation().GetAxisX().Equals(FVector::RightVector, 1.e-8)
|
||||
&& FacingTransforms[2].GetRotation().Equals(FQuat::Identity, 1.e-12)
|
||||
&& FacingTransforms[0].GetScale3D() == SharedScale);
|
||||
|
||||
const TArray<FTransform> AwayTransforms =
|
||||
UDirectiveUtilMathFunctionLibrary::LocationsToFacingTransforms(
|
||||
FacingLocations, FacingTarget, FVector::UpVector, FRotator::ZeroRotator, FVector::OneVector, true);
|
||||
TestTrue(TEXT("Facing transforms can point away from their target"),
|
||||
AwayTransforms.Num() == 3
|
||||
&& AwayTransforms[0].GetRotation().GetAxisX().Equals(FVector::ForwardVector, 1.e-8)
|
||||
&& AwayTransforms[1].GetRotation().GetAxisX().Equals(FVector::LeftVector, 1.e-8));
|
||||
|
||||
const FRotator FacingOffset(13.0, 17.0, 19.0);
|
||||
const FQuat ExpectedFacingOffset = FRotationMatrix::MakeFromXZ(
|
||||
FVector::BackwardVector, FVector::UpVector).ToQuat() * FacingOffset.Quaternion();
|
||||
const TArray<FTransform> OffsetFacingTransforms =
|
||||
UDirectiveUtilMathFunctionLibrary::LocationsToFacingTransforms(
|
||||
{FacingLocations[0]}, FacingTarget, FVector::UpVector, FacingOffset);
|
||||
TestTrue(TEXT("Facing transforms apply their rotation offset in local space"),
|
||||
OffsetFacingTransforms.Num() == 1
|
||||
&& OffsetFacingTransforms[0].GetRotation().Equals(ExpectedFacingOffset, 1.e-12));
|
||||
|
||||
TestTrue(TEXT("Facing transforms reject invalid shared inputs"),
|
||||
UDirectiveUtilMathFunctionLibrary::LocationsToFacingTransforms(
|
||||
FacingLocations, FacingTarget, FVector::ZeroVector).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::LocationsToFacingTransforms(
|
||||
FacingLocations, FVector(Infinity), FVector::UpVector).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::LocationsToFacingTransforms(
|
||||
FacingLocations, FacingTarget, FVector::UpVector, InvalidRotation).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::LocationsToFacingTransforms(
|
||||
FacingLocations, FacingTarget, FVector::UpVector, FRotator::ZeroRotator,
|
||||
FVector(Infinity)).IsEmpty());
|
||||
|
||||
const FVector RadialCenter(1000.0, -2000.0, 3000.0);
|
||||
const FRotator RadialPlane(17.0, 31.0, 43.0);
|
||||
const FQuat RadialPlaneQuaternion = RadialPlane.Quaternion();
|
||||
const FVector RadialNormal = RadialPlaneQuaternion.GetAxisZ();
|
||||
const TArray<FVector> CircleLocations = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle(
|
||||
RadialCenter, RadialPlane, 25.0, 12, 11.0);
|
||||
const TArray<FTransform> InwardCircleTransforms =
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnCircle(
|
||||
RadialCenter, RadialPlane, 25.0, 12, 11.0,
|
||||
EDirectiveUtilRadialOrientation::FaceCenter, FRotator::ZeroRotator, SharedScale);
|
||||
bool bInwardCircleValid = InwardCircleTransforms.Num() == CircleLocations.Num();
|
||||
for (int32 Index = 0; Index < InwardCircleTransforms.Num(); ++Index)
|
||||
{
|
||||
const FVector Inward = (RadialCenter - CircleLocations[Index]).GetSafeNormal();
|
||||
bInwardCircleValid &= InwardCircleTransforms[Index].GetLocation().Equals(CircleLocations[Index], 1.e-8);
|
||||
bInwardCircleValid &= InwardCircleTransforms[Index].GetRotation().GetAxisX().Equals(Inward, 1.e-8);
|
||||
bInwardCircleValid &= InwardCircleTransforms[Index].GetScale3D() == SharedScale;
|
||||
}
|
||||
TestTrue(TEXT("Circle transforms match point locations and face their center"), bInwardCircleValid);
|
||||
|
||||
const TArray<FTransform> OutwardCircleTransforms =
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnCircle(
|
||||
RadialCenter, RadialPlane, 25.0, 12, 11.0,
|
||||
EDirectiveUtilRadialOrientation::FaceAwayFromCenter);
|
||||
const TArray<FTransform> ForwardCircleTransforms =
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnCircle(
|
||||
RadialCenter, RadialPlane, 25.0, 12, 11.0,
|
||||
EDirectiveUtilRadialOrientation::FollowPath);
|
||||
const TArray<FTransform> ReverseCircleTransforms =
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnCircle(
|
||||
RadialCenter, RadialPlane, 25.0, 12, 11.0,
|
||||
EDirectiveUtilRadialOrientation::FaceAgainstPath);
|
||||
bool bCircleOrientationsValid = OutwardCircleTransforms.Num() == CircleLocations.Num()
|
||||
&& ForwardCircleTransforms.Num() == CircleLocations.Num()
|
||||
&& ReverseCircleTransforms.Num() == CircleLocations.Num();
|
||||
for (int32 Index = 0; Index < CircleLocations.Num() && bCircleOrientationsValid; ++Index)
|
||||
{
|
||||
const FVector Radial = (CircleLocations[Index] - RadialCenter).GetSafeNormal();
|
||||
const FVector Tangent = FVector::CrossProduct(RadialNormal, Radial).GetSafeNormal();
|
||||
bCircleOrientationsValid &= OutwardCircleTransforms[Index].GetRotation().GetAxisX().Equals(Radial, 1.e-8);
|
||||
bCircleOrientationsValid &= ForwardCircleTransforms[Index].GetRotation().GetAxisX().Equals(Tangent, 1.e-8);
|
||||
bCircleOrientationsValid &= ReverseCircleTransforms[Index].GetRotation().GetAxisX().Equals(-Tangent, 1.e-8);
|
||||
}
|
||||
TestTrue(TEXT("Circle transforms support outward and both path orientations"), bCircleOrientationsValid);
|
||||
|
||||
const TArray<FTransform> FixedCircleTransforms =
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnCircle(
|
||||
RadialCenter, RadialPlane, 25.0, 4, 0.0,
|
||||
EDirectiveUtilRadialOrientation::Fixed, FacingOffset);
|
||||
FQuat ExpectedFixedRotation = RadialPlaneQuaternion * FacingOffset.Quaternion();
|
||||
ExpectedFixedRotation.Normalize();
|
||||
TestTrue(TEXT("Fixed circle transforms preserve the plane rotation and local offset"),
|
||||
FixedCircleTransforms.Num() == 4
|
||||
&& FixedCircleTransforms[0].GetRotation().Equals(ExpectedFixedRotation, 1.e-12)
|
||||
&& FixedCircleTransforms[3].GetRotation().Equals(ExpectedFixedRotation, 1.e-12));
|
||||
|
||||
const TArray<FTransform> NegativeArcTransforms =
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnArc(
|
||||
RadialCenter, RadialPlane, 25.0, 3, 0.0, -90.0, true,
|
||||
EDirectiveUtilRadialOrientation::FollowPath);
|
||||
TestTrue(TEXT("A negative arc reverses follow-path orientation"),
|
||||
NegativeArcTransforms.Num() == 3
|
||||
&& NegativeArcTransforms[0].GetRotation().GetAxisX().Equals(
|
||||
-RadialPlaneQuaternion.GetAxisY(), 1.e-8));
|
||||
const TArray<FVector> NegativeArcLocations = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc(
|
||||
RadialCenter, RadialPlane, 25.0, 3, 0.0, -90.0, true);
|
||||
TestTrue(TEXT("Arc transforms match translated rotated point generation"),
|
||||
NegativeArcTransforms.Num() == NegativeArcLocations.Num()
|
||||
&& NegativeArcTransforms[0].GetLocation().Equals(NegativeArcLocations[0], 1.e-8)
|
||||
&& NegativeArcTransforms[1].GetLocation().Equals(NegativeArcLocations[1], 1.e-8)
|
||||
&& NegativeArcTransforms[2].GetLocation().Equals(NegativeArcLocations[2], 1.e-8));
|
||||
|
||||
const TArray<FTransform> ZeroRadiusArcTransforms =
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnArc(
|
||||
RadialCenter, RadialPlane, 0.0, 3, 0.0, 90.0, true,
|
||||
EDirectiveUtilRadialOrientation::FaceCenter);
|
||||
TestTrue(TEXT("Zero-radius arc transforms retain a deterministic radial orientation"),
|
||||
ZeroRadiusArcTransforms.Num() == 3
|
||||
&& ZeroRadiusArcTransforms[0].GetLocation() == RadialCenter
|
||||
&& ZeroRadiusArcTransforms[0].GetRotation().GetAxisX().Equals(
|
||||
-RadialPlaneQuaternion.GetAxisX(), 1.e-8));
|
||||
|
||||
TestTrue(TEXT("Radial transform generators reject invalid input"),
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnCircle(
|
||||
RadialCenter, RadialPlane, Infinity, 3).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnArc(
|
||||
RadialCenter, RadialPlane, 1.0, 3, 0.0, Infinity).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnCircle(
|
||||
RadialCenter, RadialPlane, 1.0, 3, 0.0,
|
||||
static_cast<EDirectiveUtilRadialOrientation>(255)).IsEmpty());
|
||||
|
||||
USplineComponent* Spline = NewObject<USplineComponent>();
|
||||
Spline->SetSplinePoints({FVector::ZeroVector, FVector(100.0, 0.0, 0.0)},
|
||||
ESplineCoordinateSpace::Local, false);
|
||||
Spline->SetSplinePointType(0, ESplinePointType::Linear, false);
|
||||
Spline->SetSplinePointType(1, ESplinePointType::Linear, false);
|
||||
Spline->SetScaleAtSplinePoint(0, FVector(1.0, 2.0, 3.0), false);
|
||||
Spline->SetScaleAtSplinePoint(1, FVector(3.0, 4.0, 5.0), true);
|
||||
const FVector SplineScaleMultiplier(2.0, 0.5, -1.0);
|
||||
const TArray<FTransform> SplineTransforms =
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSpline(
|
||||
Spline, 30.0, true, EDirectiveUtilSplineSpacingMode::Fixed,
|
||||
ESplineCoordinateSpace::World, true, FacingOffset, SplineScaleMultiplier);
|
||||
const double SplineDistances[] = {0.0, 30.0, 60.0, 90.0, 100.0};
|
||||
bool bSplineTransformsValid = SplineTransforms.Num() == 5;
|
||||
for (int32 Index = 0; Index < SplineTransforms.Num(); ++Index)
|
||||
{
|
||||
FTransform Expected = Spline->GetTransformAtDistanceAlongSpline(
|
||||
static_cast<float>(SplineDistances[Index]), ESplineCoordinateSpace::World, true);
|
||||
FQuat ExpectedRotation = Expected.GetRotation() * FacingOffset.Quaternion();
|
||||
ExpectedRotation.Normalize();
|
||||
bSplineTransformsValid &= SplineTransforms[Index].GetLocation().Equals(Expected.GetLocation(), 1.e-8);
|
||||
bSplineTransformsValid &= SplineTransforms[Index].GetRotation().Equals(ExpectedRotation, 1.e-8);
|
||||
bSplineTransformsValid &= SplineTransforms[Index].GetScale3D().Equals(
|
||||
Expected.GetScale3D() * SplineScaleMultiplier, 1.e-8);
|
||||
}
|
||||
TestTrue(TEXT("Spline transforms preserve sampling, spline rotation, and spline scale"),
|
||||
bSplineTransformsValid);
|
||||
|
||||
const TArray<FTransform> UnscaledSplineTransforms =
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSpline(
|
||||
Spline, 1000.0, true, EDirectiveUtilSplineSpacingMode::Fixed,
|
||||
ESplineCoordinateSpace::World, false, FRotator::ZeroRotator, SharedScale);
|
||||
TestTrue(TEXT("Spline scale can be replaced by a shared multiplier"),
|
||||
UnscaledSplineTransforms.Num() == 2
|
||||
&& UnscaledSplineTransforms[0].GetScale3D() == SharedScale
|
||||
&& UnscaledSplineTransforms[1].GetScale3D() == SharedScale);
|
||||
|
||||
const TArray<FTransform> CountedSplineTransforms =
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSplineByCount(Spline, 3, true);
|
||||
TestTrue(TEXT("Spline transforms by count include both exact endpoints"),
|
||||
CountedSplineTransforms.Num() == 3
|
||||
&& CountedSplineTransforms[0].GetLocation().Equals(FVector::ZeroVector, 1.e-4)
|
||||
&& CountedSplineTransforms[1].GetLocation().Equals(FVector(50.0, 0.0, 0.0), 1.e-4)
|
||||
&& CountedSplineTransforms[2].GetLocation().Equals(FVector(100.0, 0.0, 0.0), 1.e-4));
|
||||
|
||||
USplineComponent* SinglePointSpline = NewObject<USplineComponent>();
|
||||
SinglePointSpline->SetSplinePoints({FVector(3.0, 4.0, 5.0)}, ESplineCoordinateSpace::Local, true);
|
||||
const TArray<FTransform> SinglePointSplineTransforms =
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSpline(
|
||||
SinglePointSpline, 10.0);
|
||||
TestTrue(TEXT("A zero-length spline returns one transform"),
|
||||
SinglePointSplineTransforms.Num() == 1
|
||||
&& SinglePointSplineTransforms[0].GetLocation().Equals(FVector(3.0, 4.0, 5.0), 1.e-8));
|
||||
|
||||
TestTrue(TEXT("Spline transform generation rejects invalid input"),
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSpline(
|
||||
nullptr, 10.0).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSpline(
|
||||
Spline, 0.0).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSpline(
|
||||
Spline, 10.0, true, EDirectiveUtilSplineSpacingMode::Fixed,
|
||||
ESplineCoordinateSpace::World, true, InvalidRotation).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSpline(
|
||||
Spline, 10.0, true, EDirectiveUtilSplineSpacingMode::Fixed,
|
||||
ESplineCoordinateSpace::World, true, FRotator::ZeroRotator, FVector(Infinity)).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSplineByCount(
|
||||
Spline, 0).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSplineByCount(
|
||||
Spline, 3, true, ESplineCoordinateSpace::World, true, InvalidRotation).IsEmpty());
|
||||
|
||||
const TArray<FTransform> HexGridTransforms =
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGridTransforms(
|
||||
RadialCenter, FRotator::ZeroRotator, FIntPoint(2, 2), 25.0,
|
||||
EDirectiveUtilHexOrientation::PointyTop, 0.0, false, FacingOffset, SharedScale);
|
||||
const TArray<FVector> HexGridPoints = UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid(
|
||||
RadialCenter, FRotator::ZeroRotator, FIntPoint(2, 2), 25.0,
|
||||
EDirectiveUtilHexOrientation::PointyTop, 0.0, false);
|
||||
bool bHexTransformsValid = HexGridTransforms.Num() == 4 && HexGridPoints.Num() == 4;
|
||||
for (int32 Index = 0; bHexTransformsValid && Index < HexGridTransforms.Num(); ++Index)
|
||||
{
|
||||
bHexTransformsValid &= HexGridTransforms[Index].GetLocation().Equals(HexGridPoints[Index], 1.e-9)
|
||||
&& HexGridTransforms[Index].GetRotation().Equals(FacingOffset.Quaternion(), 1.e-8)
|
||||
&& HexGridTransforms[Index].GetScale3D() == SharedScale;
|
||||
}
|
||||
TestTrue(TEXT("Rectangular hex grid transforms share instance rotation and scale over grid cells"),
|
||||
bHexTransformsValid);
|
||||
TestEqual(TEXT("Hexagonal hex grid transforms cover the requested rings"),
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGridTransforms(
|
||||
RadialCenter, FRotator::ZeroRotator, 1, 25.0).Num(), 7);
|
||||
TestTrue(TEXT("Hex grid transform generators reject invalid input"),
|
||||
UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGridTransforms(
|
||||
RadialCenter, FRotator::ZeroRotator, FIntPoint(2, 2), 25.0,
|
||||
EDirectiveUtilHexOrientation::PointyTop, 0.0, true, InvalidRotation).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGridTransforms(
|
||||
RadialCenter, FRotator::ZeroRotator, 1, 0.0).IsEmpty());
|
||||
|
||||
const TArray<FVector> NoiseBaseLocations = {
|
||||
FVector::ZeroVector, FVector(37.0, 11.0, 5.0), FVector(250.0, -90.0, 40.0)
|
||||
};
|
||||
const TArray<FTransform> NoiseBaseTransforms = UDirectiveUtilMathFunctionLibrary::LocationsToTransforms(
|
||||
NoiseBaseLocations, FacingOffset, SharedScale);
|
||||
const TArray<FTransform> NoisedTransforms = UDirectiveUtilMathFunctionLibrary::OffsetTransformsByNoise(
|
||||
NoiseBaseTransforms, 100.0, 25.0);
|
||||
const TArray<FVector> NoisedLocations = UDirectiveUtilMathFunctionLibrary::OffsetLocationsByNoise(
|
||||
NoiseBaseLocations, 100.0, 25.0);
|
||||
bool bNoiseTransformsValid = NoisedTransforms.Num() == 3 && NoisedLocations.Num() == 3;
|
||||
for (int32 Index = 0; bNoiseTransformsValid && Index < NoisedTransforms.Num(); ++Index)
|
||||
{
|
||||
bNoiseTransformsValid &= NoisedTransforms[Index].GetLocation().Equals(NoisedLocations[Index], 1.e-9)
|
||||
&& NoisedTransforms[Index].GetRotation().Equals(NoiseBaseTransforms[Index].GetRotation(), 1.e-9)
|
||||
&& NoisedTransforms[Index].GetScale3D() == NoiseBaseTransforms[Index].GetScale3D();
|
||||
}
|
||||
TestTrue(TEXT("Transform noise offsets match location noise offsets and preserve rotation and scale"),
|
||||
bNoiseTransformsValid);
|
||||
TestTrue(TEXT("Transform noise offsets reject invalid input"),
|
||||
UDirectiveUtilMathFunctionLibrary::OffsetTransformsByNoise(NoiseBaseTransforms, -1.0, 25.0).IsEmpty());
|
||||
|
||||
const TArray<FVector> EaseFromLocations = {
|
||||
FVector::ZeroVector, FVector(10.0, 0.0, 0.0), FVector(20.0, 0.0, 0.0)
|
||||
};
|
||||
const TArray<FVector> EaseToLocations = {
|
||||
FVector(0.0, 10.0, 0.0), FVector(10.0, 10.0, 0.0), FVector(20.0, 10.0, 0.0)
|
||||
};
|
||||
const TArray<FVector> EasedLocations = UDirectiveUtilMathFunctionLibrary::EaseLocationArrays(
|
||||
EaseFromLocations, EaseToLocations, 0.5f, EDirectiveUtilEaseType::Linear, {});
|
||||
TestTrue(TEXT("Eased location arrays blend element-wise"),
|
||||
EasedLocations.Num() == 3
|
||||
&& EasedLocations[0].Equals(FVector(0.0, 5.0, 0.0), 1.e-4)
|
||||
&& EasedLocations[2].Equals(FVector(20.0, 5.0, 0.0), 1.e-4));
|
||||
const TArray<FVector> StaggeredLocations = UDirectiveUtilMathFunctionLibrary::EaseLocationArrays(
|
||||
EaseFromLocations, EaseToLocations, 0.0f, EDirectiveUtilEaseType::Linear, { 0.0f, 0.5f, 1.0f });
|
||||
TestTrue(TEXT("Per-element alphas stagger the blend"),
|
||||
StaggeredLocations.Num() == 3
|
||||
&& StaggeredLocations[0].Equals(EaseFromLocations[0], 1.e-4)
|
||||
&& StaggeredLocations[1].Equals(FVector(10.0, 5.0, 0.0), 1.e-4)
|
||||
&& StaggeredLocations[2].Equals(EaseToLocations[2], 1.e-4));
|
||||
TestTrue(TEXT("Eased arrays reject mismatched lengths"),
|
||||
UDirectiveUtilMathFunctionLibrary::EaseLocationArrays(
|
||||
EaseFromLocations, { FVector::ZeroVector }, 0.5f, EDirectiveUtilEaseType::Linear, {}).IsEmpty()
|
||||
&& UDirectiveUtilMathFunctionLibrary::EaseLocationArrays(
|
||||
EaseFromLocations, EaseToLocations, 0.5f, EDirectiveUtilEaseType::Linear, { 0.5f, 0.5f }).IsEmpty());
|
||||
|
||||
const TArray<FTransform> EaseFromTransforms = UDirectiveUtilMathFunctionLibrary::LocationsToTransforms(
|
||||
EaseFromLocations, FRotator::ZeroRotator, FVector::OneVector);
|
||||
const TArray<FTransform> EaseToTransforms = UDirectiveUtilMathFunctionLibrary::LocationsToTransforms(
|
||||
EaseToLocations, FRotator(0.0, 90.0, 0.0), FVector(3.0));
|
||||
const TArray<FTransform> EasedTransforms = UDirectiveUtilMathFunctionLibrary::EaseTransformArrays(
|
||||
EaseFromTransforms, EaseToTransforms, 0.5f, EDirectiveUtilEaseType::Linear, {});
|
||||
TestTrue(TEXT("Eased transform arrays blend location, rotation, and scale element-wise"),
|
||||
EasedTransforms.Num() == 3
|
||||
&& EasedTransforms[1].GetLocation().Equals(FVector(10.0, 5.0, 0.0), 1.e-4)
|
||||
&& EasedTransforms[1].GetRotation().Equals(FRotator(0.0, 45.0, 0.0).Quaternion(), 1.e-4)
|
||||
&& EasedTransforms[1].GetScale3D().Equals(FVector(2.0), 1.e-4));
|
||||
|
||||
const TArray<FVector> SamplePath = {
|
||||
FVector::ZeroVector, FVector(10.0, 0.0, 0.0), FVector(10.0, 10.0, 0.0)
|
||||
};
|
||||
TestTrue(TEXT("Location array sampling is distance-weighted"),
|
||||
UDirectiveUtilMathFunctionLibrary::SampleLocationArray(SamplePath, 0.75f).Equals(
|
||||
FVector(10.0, 5.0, 0.0), 1.e-4)
|
||||
&& UDirectiveUtilMathFunctionLibrary::SampleLocationArray(SamplePath, 0.0f).Equals(
|
||||
SamplePath[0], 1.e-4)
|
||||
&& UDirectiveUtilMathFunctionLibrary::SampleLocationArray(SamplePath, 1.0f).Equals(
|
||||
SamplePath.Last(), 1.e-4)
|
||||
&& UDirectiveUtilMathFunctionLibrary::SampleLocationArray(SamplePath, 1.5f).Equals(
|
||||
SamplePath.Last(), 1.e-4));
|
||||
TestTrue(TEXT("Closed-loop sampling wraps alpha back to the start"),
|
||||
UDirectiveUtilMathFunctionLibrary::SampleLocationArray(SamplePath, 1.0f, true).Equals(
|
||||
SamplePath[0], 1.e-4));
|
||||
TestTrue(TEXT("Degenerate location array sampling returns the only point or zero"),
|
||||
UDirectiveUtilMathFunctionLibrary::SampleLocationArray({ FVector(3.0, 4.0, 5.0) }, 0.7f).Equals(
|
||||
FVector(3.0, 4.0, 5.0), 1.e-4)
|
||||
&& UDirectiveUtilMathFunctionLibrary::SampleLocationArray({}, 0.5f).IsZero());
|
||||
|
||||
const TArray<FTransform> SampleTransformPath = {
|
||||
FTransform(FRotator::ZeroRotator, FVector::ZeroVector, FVector::OneVector),
|
||||
FTransform(FRotator(0.0, 90.0, 0.0), FVector(10.0, 0.0, 0.0), FVector(3.0))
|
||||
};
|
||||
const FTransform SampledTransform = UDirectiveUtilMathFunctionLibrary::SampleTransformArray(
|
||||
SampleTransformPath, 0.5f);
|
||||
TestTrue(TEXT("Transform array sampling blends location, rotation, and scale"),
|
||||
SampledTransform.GetLocation().Equals(FVector(5.0, 0.0, 0.0), 1.e-4)
|
||||
&& SampledTransform.GetRotation().Equals(FRotator(0.0, 45.0, 0.0).Quaternion(), 1.e-4)
|
||||
&& SampledTransform.GetScale3D().Equals(FVector(2.0), 1.e-4));
|
||||
TestTrue(TEXT("Empty transform array sampling returns the identity"),
|
||||
UDirectiveUtilMathFunctionLibrary::SampleTransformArray({}, 0.5f).Equals(FTransform::Identity));
|
||||
|
||||
return !HasAnyErrors();
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
#pragma once
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
@@ -1,12 +1,70 @@
|
||||
#pragma once
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "GameFramework/SaveGame.h"
|
||||
#include "Engine/HitResult.h"
|
||||
#include "Tasks/DirectiveUtilTask_MoveToLocation.h"
|
||||
#include "DirectiveUtilTestObject.generated.h"
|
||||
|
||||
class UWorld;
|
||||
class UGameInstance;
|
||||
|
||||
UINTERFACE()
|
||||
class UDirectiveUtilTestInterface : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
class IDirectiveUtilTestInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FDirectiveUtilCollisionValue
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Directive Utilities|Tests")
|
||||
int32 Value = 0;
|
||||
|
||||
bool operator==(const FDirectiveUtilCollisionValue& Other) const
|
||||
{
|
||||
return Value == Other.Value;
|
||||
}
|
||||
|
||||
friend uint32 GetTypeHash(const FDirectiveUtilCollisionValue&)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct TStructOpsTypeTraits<FDirectiveUtilCollisionValue> : TStructOpsTypeTraitsBase2<FDirectiveUtilCollisionValue>
|
||||
{
|
||||
enum
|
||||
{
|
||||
WithIdenticalViaEquality = true
|
||||
};
|
||||
};
|
||||
|
||||
USTRUCT()
|
||||
struct FDirectiveUtilPodValue
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY()
|
||||
int32 Index;
|
||||
|
||||
UPROPERTY()
|
||||
float Weight;
|
||||
};
|
||||
|
||||
static_assert(TIsPODType<FDirectiveUtilPodValue>::Value);
|
||||
|
||||
UCLASS()
|
||||
class UDirectiveUtilTestObject : public UObject
|
||||
@@ -17,6 +75,66 @@ public:
|
||||
UPROPERTY()
|
||||
TArray<int32> TestArray;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Directive Utilities|Tests")
|
||||
TArray<FString> TestStringArray;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<FName> TestNameArray;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<FText> TestTextArray;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Directive Utilities|Tests")
|
||||
TArray<bool> TestBoolArray;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Directive Utilities|Tests")
|
||||
TArray<FDirectiveUtilCollisionValue> TestCollisionArray;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<FDirectiveUtilPodValue> TestPodArray;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Directive Utilities|Tests")
|
||||
TArray<TObjectPtr<UObject>> TestObjectArray;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Directive Utilities|Tests")
|
||||
TArray<bool> TestBoolSourceArray;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Directive Utilities|Tests")
|
||||
TArray<FString> TestStringSourceArray;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Directive Utilities|Tests")
|
||||
TArray<FDirectiveUtilCollisionValue> TestCollisionSourceArray;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Directive Utilities|Tests")
|
||||
TArray<TObjectPtr<UObject>> TestObjectSourceArray;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Directive Utilities|Tests")
|
||||
TArray<int32> TestIndices;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Directive Utilities|Tests")
|
||||
bool TestBoolItem = false;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Directive Utilities|Tests")
|
||||
FString TestStringItem;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Directive Utilities|Tests")
|
||||
FDirectiveUtilCollisionValue TestCollisionItem;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Directive Utilities|Tests")
|
||||
TObjectPtr<UObject> TestObjectItem;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Directive Utilities|Tests")
|
||||
bool TestInsertResult = false;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Directive Utilities|Tests")
|
||||
int32 TestRemovedCount = 0;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Directive Utilities|Tests")
|
||||
bool TestRemoveAllResult = false;
|
||||
|
||||
UFUNCTION(BlueprintImplementableEvent)
|
||||
void RunArrayThunkScenario();
|
||||
|
||||
UPROPERTY()
|
||||
TMap<int32, int32> TestMap;
|
||||
|
||||
@@ -25,9 +143,17 @@ public:
|
||||
|
||||
UPROPERTY()
|
||||
TMap<FString, int32> TestStringKeyMap;
|
||||
|
||||
UPROPERTY()
|
||||
TMap<FString, FString> TestStringMap;
|
||||
|
||||
UPROPERTY()
|
||||
TMap<FString, FString> TestStringMap2;
|
||||
|
||||
UPROPERTY()
|
||||
TMap<FName, FDirectiveUtilCollisionValue> TestStructValueMap;
|
||||
};
|
||||
|
||||
/** Concrete save-game subclass for tests (USaveGame itself is abstract and cannot be instantiated). */
|
||||
UCLASS()
|
||||
class UDirectiveUtilTestSaveGame : public USaveGame
|
||||
{
|
||||
@@ -38,11 +164,6 @@ public:
|
||||
int32 TestValue = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Listener for exercising the async-task dynamic multicast delegates from automation tests.
|
||||
* Dynamic delegates can only bind to UFUNCTIONs, so the handlers live on a UObject. The
|
||||
* Keepalive property lets a rooted listener keep the async task itself alive across frames.
|
||||
*/
|
||||
UCLASS()
|
||||
class UDirectiveUtilDelegateListener : public UObject
|
||||
{
|
||||
@@ -58,58 +179,139 @@ public:
|
||||
UPROPERTY()
|
||||
int32 CompletedCount = 0;
|
||||
|
||||
/** Number of hits reported by the most recent trace completion. */
|
||||
UPROPERTY()
|
||||
int32 HitCount = 0;
|
||||
|
||||
/** Success flag from the most recent bool-payload completion (e.g. move-to-location). */
|
||||
UPROPERTY()
|
||||
int32 UpdatedCount = 0;
|
||||
|
||||
UPROPERTY()
|
||||
int32 IterationCount = 0;
|
||||
|
||||
UPROPERTY()
|
||||
float LastElapsedTime = 0.0f;
|
||||
|
||||
UPROPERTY()
|
||||
float LastDeltaTime = 0.0f;
|
||||
|
||||
UPROPERTY()
|
||||
float LastAlpha = 0.0f;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<int32> IterationIndices;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<int32> IterationRemaining;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<float> UpdateElapsedTimes;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<float> UpdateDeltaTimes;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<float> UpdateAlphas;
|
||||
|
||||
UPROPERTY()
|
||||
bool bLastSuccess = false;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UObject> LastObject = nullptr;
|
||||
|
||||
/** The object array from the most recent batch completion (e.g. async load assets). */
|
||||
UPROPERTY()
|
||||
TArray<TObjectPtr<UObject>> LastObjects;
|
||||
|
||||
/** Holds a strong reference to the async task so it survives GC while the test waits. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UObject> Keepalive = nullptr;
|
||||
|
||||
/** The transient world a latent delay scenario ticks and tears down when it settles. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UWorld> ScenarioWorld = nullptr;
|
||||
|
||||
/** Handler for parameterless completion delegates (e.g. the cancellable delay). */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UGameInstance> ScenarioGameInstance = nullptr;
|
||||
|
||||
UFUNCTION()
|
||||
void OnCompleted() { bCompleted = true; ++CompletedCount; }
|
||||
|
||||
/** Handler for object-payload completion delegates (e.g. async load asset). */
|
||||
UFUNCTION()
|
||||
void OnObjectCompleted(UObject* Object) { bCompleted = true; ++CompletedCount; LastObject = Object; }
|
||||
|
||||
/** Handler for object-payload failure delegates. */
|
||||
UFUNCTION()
|
||||
void OnObjectFailed(UObject* Object) { bFailed = true; }
|
||||
|
||||
/** Handler for object-array-payload completion delegates (e.g. async load assets). */
|
||||
UFUNCTION()
|
||||
void OnObjectsCompleted(const TArray<UObject*>& Objects) { bCompleted = true; ++CompletedCount; LastObjects.Reset(); LastObjects.Append(Objects); }
|
||||
|
||||
/** Handler for class-payload completion delegates (e.g. async load class). */
|
||||
UFUNCTION()
|
||||
void OnClassCompleted(UClass* Class) { bCompleted = true; ++CompletedCount; LastObject = Class; }
|
||||
|
||||
/** Handler for class-payload failure delegates. */
|
||||
UFUNCTION()
|
||||
void OnClassFailed(UClass* Class) { bFailed = true; }
|
||||
|
||||
/** Handler for trace completion delegates. */
|
||||
UFUNCTION()
|
||||
void OnTraceCompleted(const TArray<FHitResult>& Hits) { bCompleted = true; ++CompletedCount; HitCount = Hits.Num(); }
|
||||
|
||||
/** Handler for bool-payload completion delegates (e.g. move-to-location). */
|
||||
UFUNCTION()
|
||||
void OnBoolCompleted(bool bSuccess) { bCompleted = true; ++CompletedCount; bLastSuccess = bSuccess; }
|
||||
};
|
||||
|
||||
UFUNCTION()
|
||||
void OnDurationUpdated(float ElapsedTime, float DeltaTime, float Alpha) { ++UpdatedCount; LastElapsedTime = ElapsedTime; LastDeltaTime = DeltaTime; LastAlpha = Alpha; UpdateElapsedTimes.Add(ElapsedTime); UpdateDeltaTimes.Add(DeltaTime); UpdateAlphas.Add(Alpha); }
|
||||
|
||||
UFUNCTION()
|
||||
void OnRepeatIteration(int32 Index, int32 Remaining) { ++IterationCount; IterationIndices.Add(Index); IterationRemaining.Add(Remaining); }
|
||||
|
||||
};
|
||||
|
||||
UCLASS()
|
||||
class UDirectiveUtilTestMoveToLocationTask : public UDirectiveUtilTask_MoveToLocation
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
void Configure(AController* InController, const FVector InDestination, const bool bInCheckStuckMovement)
|
||||
{
|
||||
Controller = InController;
|
||||
Destination = InDestination;
|
||||
bCheckStuckMovement = bInCheckStuckMovement;
|
||||
}
|
||||
|
||||
void ClearController()
|
||||
{
|
||||
Controller = nullptr;
|
||||
}
|
||||
|
||||
void Complete()
|
||||
{
|
||||
ExecuteCompleted(false);
|
||||
}
|
||||
|
||||
void RegisterTimersForTest(UWorld* World);
|
||||
bool HasRegisteredTimers() const;
|
||||
};
|
||||
|
||||
UCLASS()
|
||||
class UDirectiveUtilTestMoveToActorTask : public UDirectiveUtilTask_MoveToActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
void Configure(AController* InController, AActor* InGoal, const bool bInCheckStuckMovement)
|
||||
{
|
||||
Controller = InController;
|
||||
Goal = InGoal;
|
||||
bCheckStuckMovement = bInCheckStuckMovement;
|
||||
}
|
||||
|
||||
void ClearController()
|
||||
{
|
||||
Controller = nullptr;
|
||||
}
|
||||
|
||||
void Complete()
|
||||
{
|
||||
ExecuteCompleted(false);
|
||||
}
|
||||
|
||||
void RegisterTimersForTest(UWorld* World);
|
||||
bool HasRegisteredTimers() const;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user