// 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 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(); Blueprint->GeneratedClass = UObject::StaticClass(); Blueprint->SkeletonGeneratedClass = UObject::StaticClass(); UEdGraph* Graph = NewObject(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(&Graph); Graph.AddNode(SourceNode); return SourceNode->CreatePin(EGPD_Output, PinType, Name); } UK2Node_CallArrayFunction* AddFunctionNode(UEdGraph& Graph, const FName FunctionName) { UK2Node_CallArrayFunction* Node = NewObject(&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 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 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; }