// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License. #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) { const FString OriginalClipboard = UDirectiveUtilFunctionLibrary::GetStringFromClipboard(); ADD_LATENT_AUTOMATION_COMMAND(DirectiveUtilFunctionLibraryTest::FClipboardRoundTripCommand(this, OriginalClipboard)); const FString ProjectVersion = UDirectiveUtilFunctionLibrary::GetProjectVersion(); TestNotEqual("GetProjectVersion should return a non-empty string", ProjectVersion, FString("")); #if WITH_EDITOR constexpr bool bExpectedEditorContext = true; #else constexpr bool bExpectedEditorContext = false; #endif TestEqual( "IsRunningInEditor should match the target context", UDirectiveUtilFunctionLibrary::IsRunningInEditor(), bExpectedEditorContext); 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(); 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(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()); TestNotNull("The build configuration enum should be reflected", StaticEnum()); TestNotNull("The build target type enum should be reflected", StaticEnum()); 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 } TArray RecursiveDerived; UDirectiveUtilFunctionLibrary::GetChildClasses(UBlueprintFunctionLibrary::StaticClass(), true, RecursiveDerived); TestTrue("GetChildClasses should return a non-empty list for a base class with subclasses", RecursiveDerived.Num() > 0); TestTrue("GetChildClasses should include a known derived class (UDirectiveUtilFunctionLibrary)", RecursiveDerived.Contains(UDirectiveUtilFunctionLibrary::StaticClass())); TestFalse("GetChildClasses should not include the base class itself", RecursiveDerived.Contains(UBlueprintFunctionLibrary::StaticClass())); 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 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 NonRecursiveDerived; UDirectiveUtilFunctionLibrary::GetChildClasses(UBlueprintFunctionLibrary::StaticClass(), false, NonRecursiveDerived); TestTrue("GetChildClasses non-recursive count should not exceed recursive count", NonRecursiveDerived.Num() <= RecursiveDerived.Num()); TArray LeafDerived; UDirectiveUtilFunctionLibrary::GetChildClasses(UDirectiveUtilFunctionLibrary::StaticClass(), true, LeafDerived); TestEqual("GetChildClasses should return an empty list for a class with no subclasses", LeafDerived.Num(), 0); TArray NullBaseDerived; UDirectiveUtilFunctionLibrary::GetChildClasses(nullptr, true, NullBaseDerived); TestEqual("GetChildClasses should return an empty list for a null base class", NullBaseDerived.Num(), 0); { const TCHAR* CommandLine = TEXT("-Fast -Mode=Quality -Name=\"Big Save\""); TestTrue("HasCommandLineSwitch should find a present switch", UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(CommandLine, TEXT("Fast"))); TestTrue("HasCommandLineSwitch should match case-insensitively", UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(CommandLine, TEXT("fast"))); TestFalse("HasCommandLineSwitch should not find an absent switch", UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(CommandLine, TEXT("Slow"))); TestFalse("HasCommandLineSwitch should return false for an empty switch", UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(CommandLine, TEXT(""))); FString Value; TestTrue("GetCommandLineOption should find a present key", UDirectiveUtilFunctionLibrary::GetCommandLineOption(CommandLine, TEXT("Mode"), Value)); TestEqual("GetCommandLineOption should return the key's value", Value, FString(TEXT("Quality"))); TestTrue("GetCommandLineOption should read a quoted value", UDirectiveUtilFunctionLibrary::GetCommandLineOption(CommandLine, TEXT("Name"), Value)); TestEqual("GetCommandLineOption should return the quoted value without quotes", Value, FString(TEXT("Big Save"))); TestFalse("GetCommandLineOption should not find an absent key", UDirectiveUtilFunctionLibrary::GetCommandLineOption(CommandLine, TEXT("Missing"), Value)); TestFalse("GetCommandLineOption should return false for an empty key", UDirectiveUtilFunctionLibrary::GetCommandLineOption(CommandLine, TEXT(""), Value)); TestFalse("HasCommandLineSwitch should not find a switch that was never passed", UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(TEXT("DirectiveUtilitiesDefinitelyNotPassed"))); } return true; }