401 lines
16 KiB
C++
401 lines
16 KiB
C++
// 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<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
|
|
}
|
|
|
|
TArray<UClass*> 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<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());
|
|
|
|
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);
|
|
|
|
TArray<UClass*> 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;
|
|
}
|