// Copyright Epic Games, Inc. All Rights Reserved. #include "CQTest.h" #include "CQTestUnitTestHelper.h" #include "CQTestSettings.h" TEST_CLASS(RunSequenceBasicTests, "TestFramework.CQTest.Core") { TSharedPtr Cmd1; TSharedPtr Cmd2; FString Cmd1Name = "One"; FString Cmd2Name = "Two"; TArray Log; BEFORE_EACH() { Cmd1 = MakeShared(*TestRunner, [&]() { Log.Add(Cmd1Name); }, *Cmd1Name); Cmd2 = MakeShared(*TestRunner, [&]() { Log.Add(Cmd2Name); }, *Cmd2Name); } TEST_METHOD(Update_WithRemainingCommands_ReturnsFalse) { FRunSequence sequence(Cmd1, Cmd2); ASSERT_THAT(IsFalse(sequence.Update())); } TEST_METHOD(Update_OnLastCommand_ReturnTrue) { FRunSequence sequence(Cmd1); ASSERT_THAT(IsTrue(sequence.Update())); } TEST_METHOD(Append_ANewCommand_AddsCommandToEnd) { FRunSequence sequence{ Cmd1 }; sequence.Append(Cmd2); sequence.Update(); sequence.Update(); ASSERT_THAT(AreEqual(2, Log.Num())); ASSERT_THAT(AreEqual(Cmd1Name, Log[0])); ASSERT_THAT(AreEqual(Cmd2Name, Log[1])); } TEST_METHOD(Prepend_ANewCommand_AddsCommandToBeginning) { FRunSequence sequence{ Cmd1 }; sequence.Prepend(Cmd2); sequence.Update(); sequence.Update(); ASSERT_THAT(AreEqual(2, Log.Num())); ASSERT_THAT(AreEqual(Cmd2Name, Log[0])); ASSERT_THAT(AreEqual(Cmd1Name, Log[1])); } }; struct FCommandLog { TArray Commands; }; class FNamedCommand : public IAutomationLatentCommand { public: FNamedCommand(TArray& CommandLog, FString Name) : Log(CommandLog), CommandName(Name) {} bool Update() override { Log.Add(CommandName); return true; } TArray& Log; FString CommandName; }; class FTickingNamedCommand : public FNamedCommand { public: FTickingNamedCommand(TArray& CommandLog, FString Name, int32 Ticks) : FNamedCommand(CommandLog, Name), ExpectedCount(Ticks) {} bool Update() override { if (CurrentCount == ExpectedCount) { return true; } Log.Add(CommandName); CurrentCount++; return false; } int32 ExpectedCount{ 0 }; int32 CurrentCount{ 0 }; }; TEST_CLASS(RunSequenceTests, "TestFramework.CQTest.Core") { const TArray Names = { "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight" }; TFunction Assertion; TArray CommandLog; AFTER_EACH() { ASSERT_THAT(IsTrue(Assertion(this))); } TEST_METHOD(RunSequence_WithZeroCommands_DoesNotFail) { AddCommand(new FRunSequence()); Assertion = [](RunSequenceTests* test) { return test->CommandLog.IsEmpty(); }; } TEST_METHOD(RunSequence_WithOneCommand_RunsCommand) { AddCommand(new FRunSequence(MakeShared(CommandLog, Names[0]))); Assertion = [](RunSequenceTests* test) { return test->CommandLog.Num() == 1 && test->CommandLog[0] == test->Names[0]; }; } TEST_METHOD(RunSequence_WithNamedCommands_RunsCommandsInOrder) { TArray> Commands; Commands.Add(MakeShared(CommandLog, Names[0])); Commands.Add(MakeShared(CommandLog, Names[1])); Commands.Add(MakeShared(CommandLog, Names[2])); Commands.Add(MakeShared(CommandLog, Names[3])); Commands.Add(MakeShared(CommandLog, Names[4])); AddCommand(new FRunSequence(Commands)); Assertion = [](RunSequenceTests* test) { if (test->CommandLog.Num() != 5) { return false; } for (int32 i = 0; i < 5; i++) { if (test->CommandLog[i] != test->Names[i]) { return false; } } return true; }; } TEST_METHOD(RunSequence_WithTickingCommands_RunsCommandsInOrder) { TArray> Commands; Commands.Add(MakeShared(CommandLog, Names[0], 3)); Commands.Add(MakeShared(CommandLog, Names[1], 3)); Commands.Add(MakeShared(CommandLog, Names[2], 3)); Commands.Add(MakeShared(CommandLog, Names[3], 3)); Commands.Add(MakeShared(CommandLog, Names[4], 3)); AddCommand(new FRunSequence(Commands)); Assertion = [](RunSequenceTests* test) { if (test->CommandLog.Num() != 15) { return false; } int32 NameIndex = -1; for (int32 CommandIndex = 0; CommandIndex < test->CommandLog.Num(); CommandIndex++) { if (CommandIndex % 3 == 0) { NameIndex++; } if (test->CommandLog[CommandIndex] != test->Names[NameIndex]) { return false; } } return true; }; } TEST_METHOD(RunSequence_WithSequences_RunsCommandsInOrder) { TArray> Cmds1; TArray> Cmds2; TArray> Cmds3; Cmds1.Add(MakeShared(CommandLog, Names[0])); Cmds1.Add(MakeShared(CommandLog, Names[1])); Cmds1.Add(MakeShared(CommandLog, Names[2])); Cmds2.Add(MakeShared(CommandLog, Names[3])); Cmds2.Add(MakeShared(CommandLog, Names[4])); Cmds2.Add(MakeShared(CommandLog, Names[5])); Cmds3.Add(MakeShared(CommandLog, Names[6])); Cmds3.Add(MakeShared(CommandLog, Names[7])); Cmds3.Add(MakeShared(CommandLog, Names[8])); AddCommand(new FRunSequence(MakeShared(Cmds1), MakeShared(Cmds2), MakeShared(Cmds3))); Assertion = [](RunSequenceTests* test) { for (int32 i = 0; i < 9; i++) { if (test->CommandLog[i] != test->Names[i]) { return false; } } return true; }; } TEST_METHOD(RunSequence_WithSeparateSequences_RunsCommandsInOrder) { TArray> Cmds1; TArray> Cmds2; TArray> Cmds3; Cmds1.Add(MakeShared(CommandLog, Names[0])); Cmds1.Add(MakeShared(CommandLog, Names[1])); Cmds1.Add(MakeShared(CommandLog, Names[2])); Cmds2.Add(MakeShared(CommandLog, Names[3])); Cmds2.Add(MakeShared(CommandLog, Names[4])); Cmds2.Add(MakeShared(CommandLog, Names[5])); Cmds3.Add(MakeShared(CommandLog, Names[6])); Cmds3.Add(MakeShared(CommandLog, Names[7])); Cmds3.Add(MakeShared(CommandLog, Names[8])); AddCommand(new FRunSequence(Cmds1)); AddCommand(new FRunSequence(Cmds2)); AddCommand(new FRunSequence(Cmds3)); Assertion = [](RunSequenceTests* test) { for (int32 i = 0; i < 9; i++) { if (test->CommandLog[i] != test->Names[i]) { return false; } } return true; }; } TEST_METHOD(RunSequence_WithUntilCommands_RunsCommandsInOrder) { TArray> Cmds; Cmds.Add(MakeShared(*TestRunner, [&]() { static int32 attempt = 0; CommandLog.Add(Names[0]); if (++attempt > 3) { attempt = 0; return true; } return false; })); Cmds.Add(MakeShared(*TestRunner, [&]() { static int32 attempt = 0; CommandLog.Add(Names[1]); if (++attempt > 4) { attempt = 0; return true; } return false; })); AddCommand(new FRunSequence(Cmds)); Assertion = [](RunSequenceTests* test) { return test->CommandLog.Num() == 9; }; } }; class FFakeAsyncTask { public: FFakeAsyncTask(FAutomationTestBase& InTestRunner) : TestRunner{ InTestRunner } {} ~FFakeAsyncTask() { if (InProgress()) { Complete(0); } } TAsyncResult Start() { if (TestRunner.AddErrorIfFalse(!IsRunning, TEXT("Async task has already been started"))) { IsRunning = true; Promise = MakeShared>(); return TAsyncResult(Promise->GetFuture(), nullptr, nullptr); } return TAsyncResult(); } void Complete(int Value) { if (TestRunner.AddErrorIfFalse(IsRunning, TEXT("Cannot set the async task result if it's not running"))) { IsRunning = false; Promise->SetValue(Value); } } bool InProgress() const { return IsRunning; } private: bool IsRunning = false; FAutomationTestBase& TestRunner; TSharedPtr> Promise; }; TEST_CLASS(AsyncExecuteBasicTests, "TestFramework.CQTest.Core") { TEST_METHOD(AsyncExecute_InvokesAsyncActionWhenUpdated) { FFakeAsyncTask Task(*TestRunner); TAsyncExecute AsyncExecute = TAsyncExecute( *TestRunner, [&]() { return Task.Start(); } ); ASSERT_THAT(IsFalse(Task.InProgress(), TEXT("Default state of async task is invalid"))); ASSERT_THAT(IsFalse(AsyncExecute.Update(), TEXT("Command stopped execution early"))); ASSERT_THAT(IsTrue(Task.InProgress(), TEXT("Async task hasn't been started"))); } TEST_METHOD(AsyncExecute_InvokesAsyncActionOnce) { FFakeAsyncTask Task(*TestRunner); int32 Counter = 0; TAsyncExecute AsyncExecute = TAsyncExecute( *TestRunner, [&]() { Counter++; return Task.Start(); } ); int32 Ticks = 3; bool bDone = false; while (!bDone && Ticks--) { bDone = AsyncExecute.Update(); } ASSERT_THAT(IsFalse(bDone, TEXT("Command stopped execution early"))); ASSERT_THAT(AreEqual(1, Counter, TEXT("Async action was invoked multiple times"))); } TEST_METHOD(AsyncExecute_WaitsUntilAsyncResultIsReady) { FFakeAsyncTask Task(*TestRunner); TAsyncExecute AsyncExecute = TAsyncExecute( *TestRunner, [&]() { return Task.Start(); } ); // Start async task ASSERT_THAT(IsFalse(AsyncExecute.Update(), TEXT("Command stopped execution early"))); bool bDone = false; for (int32 i = 0; !bDone && i < 10; i++) { bDone = AsyncExecute.Update(); } ASSERT_THAT(IsFalse(bDone, TEXT("Command stopped execution before the async task was completed"))); Task.Complete(0); ASSERT_THAT(IsTrue(AsyncExecute.Update(), TEXT("Command failed to stop execution after the async task was completed"))); } TEST_METHOD(AsyncExecute_FExecute_CompletesWhenAsyncActionIsCompleted) { FFakeAsyncTask Task(*TestRunner); TAsyncExecute AsyncExecute = TAsyncExecute( *TestRunner, [&]() { return Task.Start(); }, FTimespan::FromSeconds(1), nullptr, [](int) {} ); // Start async task ASSERT_THAT(IsFalse(AsyncExecute.Update(), TEXT("Command stopped execution early"))); Task.Complete(0); // Need two updates: the first to verify that the async result is ready, the second to invoke the callback. bool bDone = false; for (int32 i = 0; !bDone && i < 2; i++) { bDone = AsyncExecute.Update(); } ASSERT_THAT(IsTrue(bDone, TEXT("Command failed to stop execution after the async task was completed"))); } TEST_METHOD(AsyncExecute_FExecute_InvokesResultCallbackWhenAsyncActionIsCompleted) { FFakeAsyncTask Task(*TestRunner); bool bInvoked = false; TAsyncExecute AsyncExecute = TAsyncExecute( *TestRunner, [&]() { return Task.Start(); }, FTimespan::FromSeconds(1), nullptr, [&](int) { bInvoked = true; } ); // Start async task ASSERT_THAT(IsFalse(AsyncExecute.Update(), TEXT("Command stopped execution early"))); ASSERT_THAT(IsFalse(bInvoked, TEXT("Callback was invoked early"))); Task.Complete(0); // Need two updates: the first to verify that the async result is ready, the second to invoke the callback. bool bDone = false; for (int32 i = 0; !bDone && i < 2; i++) { bDone = AsyncExecute.Update(); } ASSERT_THAT(IsTrue(bDone, TEXT("Command did not stop execution"))); ASSERT_THAT(IsTrue(bInvoked, TEXT("Callback hasn't been invoked"))); } TEST_METHOD(AsyncExecute_FExecute_InvokesResultCallbackOnce) { FFakeAsyncTask Task(*TestRunner); int Counter = 0; TAsyncExecute AsyncExecute = TAsyncExecute( *TestRunner, [&]() { return Task.Start(); }, FTimespan::FromSeconds(1), nullptr, [&](int) { Counter++; } ); // Start async task ASSERT_THAT(IsFalse(AsyncExecute.Update(), TEXT("Command stopped execution early"))); Task.Complete(0); // Need two updates: the first to verify that the async result is ready, the second to invoke the callback. bool bDone = false; for (int32 i = 0; !bDone && i < 2; i++) { bDone = AsyncExecute.Update(); } ASSERT_THAT(IsTrue(bDone, TEXT("Command did not stop execution"))); ASSERT_THAT(AreEqual(1, Counter, "Callback was invoked multiple times")); } TEST_METHOD(AsyncExecute_FExecute_PassesValueToResultCallback) { FFakeAsyncTask Task(*TestRunner); int Result = 0; TAsyncExecute AsyncExecute = TAsyncExecute( *TestRunner, [&]() { return Task.Start(); }, FTimespan::FromSeconds(1), nullptr, [&](int InResult) { Result = InResult; } ); // Start async task ASSERT_THAT(IsFalse(AsyncExecute.Update(), TEXT("Command stopped execution early"))); const int ExpectedResult = 5; Task.Complete(ExpectedResult); // Need two updates: the first to verify that the async result is ready, the second to invoke the callback. bool bDone = false; for (int32 i = 0; !bDone && i < 2; i++) { bDone = AsyncExecute.Update(); } ASSERT_THAT(IsTrue(bDone, TEXT("Command did not stop execution"))); ASSERT_THAT(AreEqual(ExpectedResult, Result, TEXT("Incorrect value passed to result callback"))); } TEST_METHOD(AsyncExecute_FWaitUntil_InvokesResultCallbackOnEveryUpdateWhenAsyncActionIsCompleted) { FFakeAsyncTask Task(*TestRunner); int Counter = 0; TAsyncExecute AsyncExecute = TAsyncExecute( *TestRunner, [&]() { return Task.Start(); }, FTimespan::FromSeconds(1), nullptr, [&](int) { Counter++; return false; } ); // Start async task ASSERT_THAT(IsFalse(AsyncExecute.Update(), TEXT("Command stopped execution early"))); ASSERT_THAT(AreEqual(0, Counter, TEXT("Callback was invoked early"))); Task.Complete(0); const int32 Ticks = 10; bool bDone = false; for (int32 i = 0; !bDone && i < Ticks; i++) { bDone = AsyncExecute.Update(); } ASSERT_THAT(IsFalse(bDone, TEXT("Command execution stopped before the condition was met"))); // One tick will be used to verify that the async result is ready ASSERT_THAT(AreEqual(Ticks - 1, Counter, TEXT("Incorrect number of callback invocations"))); } TEST_METHOD(AsyncExecute_FWaitUntil_PassesValueToResultCallback) { FFakeAsyncTask Task(*TestRunner); const int ExpectedResult = 6; bool bAllValuesAreValid = true; bool bInvoked = false; TAsyncExecute AsyncExecute = TAsyncExecute( *TestRunner, [&]() { return Task.Start(); }, FTimespan::FromSeconds(1), nullptr, [&](int Value) { bInvoked = true; if (Value != ExpectedResult) { bAllValuesAreValid = false; } return false; } ); // Start async task ASSERT_THAT(IsFalse(AsyncExecute.Update(), TEXT("Command stopped execution early"))); Task.Complete(ExpectedResult); bool bDone = false; for (int32 i = 0; !bDone && i < 10; i++) { bDone = AsyncExecute.Update(); } ASSERT_THAT(IsTrue(bInvoked, TEXT("Result callback was not invoked"))); ASSERT_THAT(IsTrue(bAllValuesAreValid, TEXT("Incorrect value passed to result callback"))); } TEST_METHOD(AsyncExecute_FWaitUntil_CompletesWhenResultCallbackReturnsTrue) { FFakeAsyncTask Task(*TestRunner); bool bCallbackReturnValue = false; TAsyncExecute AsyncExecute = TAsyncExecute( *TestRunner, [&]() { return Task.Start(); }, FTimespan::FromSeconds(1), nullptr, [&](int) { return bCallbackReturnValue; } ); // Start async task ASSERT_THAT(IsFalse(AsyncExecute.Update(), TEXT("Command stopped execution early"))); Task.Complete(0); bool bDone = false; for (int i = 0; !bDone && i < 10; i++) { bDone = AsyncExecute.Update(); } ASSERT_THAT(IsFalse(bDone, TEXT("Command stopped execution before the condition was met"))); bCallbackReturnValue = true; ASSERT_THAT(IsTrue(AsyncExecute.Update(), TEXT("Command failed to stop execution after the condition was met"))); } }; TEST_CLASS(AsyncExecuteTimeoutTests, "TestFramework.CQTest.Core") { TSharedPtr Task; BEFORE_EACH() { Task = MakeShared(*TestRunner); } AFTER_EACH() { ASSERT_THAT(IsTrue(Task->InProgress(), TEXT("Async task hasn't been started"))); ClearExpectedError(*TestRunner, "Latent command timed out"); Task.Reset(); } TEST_METHOD(AsyncExecute_FExecute_DoesNotInvokeResultCallbackOnTimeout) { AddCommand(MakeShared>( *TestRunner, [this]() { return Task->Start(); }, FTimespan::FromMilliseconds(1), nullptr, [this](int) { AddError("Result callback should not be invoked after timeout"); } )); } TEST_METHOD(AsyncExecute_FWaitUntil_DoesNotInvokeResultCallbackOnTimeout) { AddCommand(MakeShared>( *TestRunner, [this]() { return Task->Start(); }, FTimespan::FromMilliseconds(1), nullptr, [this](int) { AddError("Result callback should not be invoked after timeout"); return true; } )); } }; TEST_CLASS(WaitUntilTests, "TestFramework.CQTest.Core") { TEST_METHOD(Timeout_WithNoValueProvided_IsGreaterThanZero) { FWaitUntil command(*TestRunner, []() { return true; }); ASSERT_THAT(IsTrue(command.Timeout > FTimespan::Zero())); } TEST_METHOD(Timeout_WithSpecificValue_IsUsed) { FTimespan Timeout = FTimespan::FromSeconds(100); FWaitUntil command(*TestRunner, []() { return true; }, Timeout); ASSERT_THAT(AreEqual(Timeout, command.Timeout)); } TEST_METHOD(Timeout_WithDefaultValue_UsesCVar) { FWaitUntil command(*TestRunner, []() { return true; }, CQTest::DefaultTimeout); ASSERT_THAT(IsNear(CQTestConsoleVariables::CommandTimeout, static_cast(command.Timeout.GetTotalSeconds()), SMALL_NUMBER)); } TEST_METHOD(Timeout_WithOverriddenCvar_UsesOverriddenValue) { IConsoleVariable* ConsoleVariable = IConsoleManager::Get().FindConsoleVariable(CQTestConsoleVariables::CommandTimeoutName); ASSERT_THAT(IsNotNull(ConsoleVariable)); const float NewTimeout = ConsoleVariable->GetFloat() + 1; FString NewTimeoutStr = FString::Printf(TEXT("%f"), NewTimeout); TSharedPtr TestEnvironment = UCQTestSettings::SetTestClassTimeouts(FTimespan::FromSeconds(NewTimeout)); FWaitUntil command(*TestRunner, []() { return true; }, CQTest::DefaultTimeout); ASSERT_THAT(IsNear(NewTimeout, static_cast(command.Timeout.GetTotalSeconds()), SMALL_NUMBER)); } }; TEST_CLASS(WaitUntilClassTimeoutTest, "TestFramework.CQTest.Core") { TEST_METHOD(Timeout_SetInBeforeEach_PersistsInTest) { IConsoleVariable* ConsoleVariable = IConsoleManager::Get().FindConsoleVariable(CQTestConsoleVariables::CommandTimeoutName); ASSERT_THAT(IsNotNull(ConsoleVariable)); const double TimeoutValue = ConsoleVariable->GetFloat() + 1; TSharedPtr TestEnvironment = UCQTestSettings::SetTestClassTimeouts(FTimespan::FromSeconds(TimeoutValue)); FWaitUntil command(*TestRunner, []() { return true; }, CQTest::DefaultTimeout); ASSERT_THAT(IsNear(TimeoutValue, command.Timeout.GetTotalSeconds(), 0.01)); } }; #if WITH_EDITOR // Test checks to make sure that editing the timeout property is reflected. Can only be done within the Editor. TEST_CLASS_WITH_FLAGS(WaitUntilUserSettingTimeout, "TestFramework.CQTest.Core", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) { double ExpectedTimeout{0.0}; FString OriginalTimeout; bool SetPropertyValue(const FName& PropertyName, const FString& PropertyValue, FString& OutError) { UCQTestSettings* DefaultSettings = GetMutableDefault(); if (!IsValid(DefaultSettings)) { OutError = TEXT("Could not load default CQTest Settings."); return false; } if (FProperty* Property = FindFProperty(UCQTestSettings::StaticClass(), PropertyName)) { Property->ImportText_InContainer(*PropertyValue, DefaultSettings, DefaultSettings, PPF_None); FPropertyChangedEvent ChangeEvent(Property, EPropertyChangeType::ValueSet); DefaultSettings->PostEditChangeProperty(ChangeEvent); return true; } OutError = FString::Format(TEXT("Property '{0}' was not found in the CQTest Settings."), { PropertyName.ToString() }); return false; } BEFORE_EACH() { float DefaultTimeout = GetDefault()->CommandTimeout; ExpectedTimeout = DefaultTimeout + 1; OriginalTimeout = FString::SanitizeFloat(DefaultTimeout); FString ErrorMessage; const bool bWasUpdated = SetPropertyValue(TEXT("CommandTimeout"), FString::SanitizeFloat(ExpectedTimeout), ErrorMessage); AddErrorIfFalse(bWasUpdated, ErrorMessage); } AFTER_EACH() { FString ErrorMessage; const bool bWasUpdated = SetPropertyValue(TEXT("CommandTimeout"), OriginalTimeout, ErrorMessage); AddErrorIfFalse(bWasUpdated, ErrorMessage); } TEST_METHOD(Timeout_SetInBeforeEach_PersistsInTest) { FWaitUntil command(*TestRunner, []() { return true; }, CQTest::DefaultTimeout); ASSERT_THAT(IsNear(ExpectedTimeout, command.Timeout.GetTotalSeconds(), 0.01)); } }; #endif // WITH_EDITOR