// Copyright Epic Games, Inc. All Rights Reserved. #include "CoreMinimal.h" #include "Misc/CoreMisc.h" #include "Misc/CommandLine.h" #include "Misc/Guid.h" #include "Misc/OutputDeviceRedirector.h" #include "Containers/Ticker.h" #include "Misc/App.h" #include "Modules/ModuleManager.h" #include "Misc/FilterCollection.h" #include "IAutomationControllerModule.h" #include "HAL/FileManager.h" #include "Misc/Paths.h" #include "Misc/FileHelper.h" #include "AutomationControllerSettings.h" #include "AutomationGroupFilter.h" #include "Containers/Ticker.h" DEFINE_LOG_CATEGORY_STATIC(LogAutomationCommandLine, Log, All); /** States for running the automation process */ enum class EAutomationTestState : uint8 { Idle, // Automation process is not running Initializing, // FindWorkers, // Find workers to run the tests RequestTests, // Find the tests that can be run on the workers DoingRequestedWork, // Do whatever was requested from the commandline Complete // The process is finished }; enum class EAutomationCommand : uint8 { ListAllTests, //List all tests for the session RunCommandLineTests, //Run only tests that are listed on the commandline RunAll, //Run all the tests that are supported RunFilter, //Run only tests that are tagged with this filter Quit, //quit the app when tests are done, uses forced exit SoftQuit //quit the app when tests are done without forced exit }; class FAutomationExecCmd : private FSelfRegisteringExec { public: static const float DefaultDelayTimer; static const float DefaultFindWorkersTimeout; FAutomationExecCmd() { DelayTimer = DefaultDelayTimer; FindWorkersTimeout = DefaultFindWorkersTimeout; FindWorkerAttempts = 0; TestCount = 0; } void Init() { SessionID = FApp::GetSessionId(); // Set state to FindWorkers to kick off the testing process AutomationTestState = EAutomationTestState::Initializing; DelayTimer = DefaultDelayTimer; // Load the automation controller IAutomationControllerModule* AutomationControllerModule = &FModuleManager::LoadModuleChecked("AutomationController"); AutomationController = AutomationControllerModule->GetAutomationController(); AutomationController->Init(); //TODO AUTOMATION Always use fullsize screenshots. const bool bFullSizeScreenshots = FParse::Param(FCommandLine::Get(), TEXT("FullSizeScreenshots")); const bool bSendAnalytics = FParse::Param(FCommandLine::Get(), TEXT("SendAutomationAnalytics")); // Register for the callback that tells us there are tests available if (!TestsRefreshedHandle.IsValid()) { TestsRefreshedHandle = AutomationController->OnTestsRefreshed().AddRaw(this, &FAutomationExecCmd::HandleRefreshTestCallback); } if (!TickHandler.IsValid()) { TickHandler = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateRaw(this, &FAutomationExecCmd::Tick)); } int32 NumTestLoops = 1; FParse::Value(FCommandLine::Get(), TEXT("TestLoops="), NumTestLoops); AutomationController->SetNumPasses(NumTestLoops); SetUpFilterMapping(); } void SetUpFilterMapping() { FilterMaps.Empty(); FilterMaps.Add("Engine", EAutomationTestFlags::EngineFilter); FilterMaps.Add("Smoke", EAutomationTestFlags::SmokeFilter); FilterMaps.Add("Stress", EAutomationTestFlags::StressFilter); FilterMaps.Add("Perf", EAutomationTestFlags::PerfFilter); FilterMaps.Add("Product", EAutomationTestFlags::ProductFilter); FilterMaps.Add("Standard", EAutomationTestFlags::SmokeFilter | EAutomationTestFlags::EngineFilter | EAutomationTestFlags::ProductFilter | EAutomationTestFlags::PerfFilter); FilterMaps.Add("Negative", EAutomationTestFlags::NegativeFilter); FilterMaps.Add("All", EAutomationTestFlags_FilterMask); } void Shutdown() { IAutomationControllerModule* AutomationControllerModule = FModuleManager::GetModulePtr("AutomationController"); if ( AutomationControllerModule ) { AutomationController = AutomationControllerModule->GetAutomationController(); AutomationController->OnTestsRefreshed().RemoveAll(this); } FTSTicker::GetCoreTicker().RemoveTicker(TickHandler); } bool IsTestingComplete() { // If the automation controller is no longer processing and we've reached the final stage of testing if ((AutomationController->GetTestState() != EAutomationControllerModuleState::Running) && (AutomationTestState == EAutomationTestState::Complete) && (AutomationCommandQueue.Num() == 0)) { UE_LOG(LogAutomationCommandLine, Display, TEXT("...Automation Test Queue Empty %d tests performed."), TestCount); TestCount = 0; return true; } return false; } void GenerateTestNamesFromCommandLine(TSharedPtr InFilters, TArray& OutFilteredTestNames) { OutFilteredTestNames.Empty(); //Split the argument names up on + TArray ArgumentNames; StringCommand.ParseIntoArray(ArgumentNames, TEXT("+"), true); // get our settings CDO where things are stored UAutomationControllerSettings* Settings = UAutomationControllerSettings::StaticClass()->GetDefaultObject(); // iterate through the arguments to build a filter list by doing the following - // 1) If argument is a filter (StartsWith:system) then make sure we only filter-in tests that start with that filter // 2) If argument is a group then expand that group into multiple filters based on ini entries // 3) Otherwise just substring match (default behavior in 4.22 and earlier). FAutomationGroupFilter* FilterAny = new FAutomationGroupFilter(); TArray FiltersList; for (int32 ArgumentIndex = 0; ArgumentIndex < ArgumentNames.Num(); ++ArgumentIndex) { const FString GroupPrefix = TEXT("Group:"); const FString FilterPrefix = TEXT("StartsWith:"); FString ArgumentName = ArgumentNames[ArgumentIndex].TrimStartAndEnd(); // if the argument is a filter (e.g. Filter:System) then create a filter that matches from the start if (ArgumentName.StartsWith(FilterPrefix)) { FString FilterName = ArgumentName.RightChop(FilterPrefix.Len()).TrimStart(); if (FilterName.EndsWith(TEXT(".")) == false) { FilterName += TEXT("."); } FiltersList.Add(FAutomatedTestFilter(FilterName, true, false)); } else if (ArgumentName.StartsWith(GroupPrefix)) { // if the argument is a group (e.g. Group:Rendering) then seach our groups for one that matches FString GroupName = ArgumentName.RightChop(GroupPrefix.Len()).TrimStart(); bool FoundGroup = false; for (int32 i = 0; i < Settings->Groups.Num(); ++i) { FAutomatedTestGroup* GroupEntry = &(Settings->Groups[i]); if (GroupEntry && GroupEntry->Name == GroupName) { FoundGroup = true; // if found add all this groups filters to our current list if (GroupEntry->Filters.Num() > 0) { FiltersList.Append(GroupEntry->Filters); } else { UE_LOG(LogAutomationCommandLine, Warning, TEXT("Group %s contains no filters"), *GroupName); } } } if (!FoundGroup) { UE_LOG(LogAutomationCommandLine, Error, TEXT("No matching group named %s"), *GroupName); } } else { bool bMatchFromStart = false; bool bMatchFromEnd = false; if (ArgumentName.StartsWith("^")) { bMatchFromStart = true; ArgumentName.RightChopInline(1); } if (ArgumentName.EndsWith("$")) { bMatchFromEnd = true; ArgumentName.LeftChopInline(1); } FiltersList.Add(FAutomatedTestFilter(ArgumentName, bMatchFromStart, bMatchFromEnd)); } } if (!FiltersList.IsEmpty()) { FilterAny->SetFilters(FiltersList); InFilters->Add(MakeShareable(FilterAny)); // SetFilter applies all filters from the AutomationFilters array AutomationController->SetFilter(InFilters); // Fill OutFilteredTestNames array with filtered test names AutomationController->GetFilteredTestNames(OutFilteredTestNames); } } void FindWorkers(float DeltaTime) { DelayTimer -= DeltaTime; if (DelayTimer <= 0) { // Request the workers AutomationController->RequestAvailableWorkers(SessionID); AutomationTestState = EAutomationTestState::RequestTests; FindWorkersTimeout = DefaultFindWorkersTimeout; FindWorkerAttempts++; } } void RequestTests(float DeltaTime) { FindWorkersTimeout -= DeltaTime; if (FindWorkersTimeout <= 0) { // Call the refresh callback manually HandleRefreshTimeout(); } } void HandleRefreshTimeout() { const float TimeOut = GetDefault()->GameInstanceLostTimerSeconds; if (FindWorkerAttempts * DefaultFindWorkersTimeout >= TimeOut) { LogCommandLineError(FString::Printf(TEXT("Failed to find workers after %.02f seconds. Giving up"), TimeOut)); AutomationTestState = EAutomationTestState::Complete; } else { // Go back to looking for workers UE_LOG(LogAutomationCommandLine, Log, TEXT("Can't find any workers! Searching again")); AutomationTestState = EAutomationTestState::FindWorkers; } } void ApplyCVarTagFilter(TSharedPtr AutomationFilters) { if (IConsoleVariable* TagCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("Automation.TestTagGlobalFilter"))) { FString TagFilterValue = TagCVar->GetString(); if (!TagFilterValue.IsEmpty()) { UE_LOG(LogAutomationCommandLine, Display, TEXT("Applying Automation Test tag filter '%s'"), *TagFilterValue); FAutomationGroupFilter* FilterTags = new FAutomationGroupFilter(); TArray TagList; TagList.Add(FAutomatedTestTagFilter(TagFilterValue)); FilterTags->SetTagFilter(TagList); AutomationFilters->Add(MakeShareable(FilterTags)); } } } void HandleRefreshTestCallback() { TArray FilteredTestNames; // This is called by the controller manager when it receives responses. We want to make sure it has a device, and we // want to make sure it's called while we're waiting for a response if (AutomationController->GetNumDeviceClusters() == 0 || AutomationTestState != EAutomationTestState::RequestTests) { UE_LOG(LogAutomationCommandLine, Log, TEXT("Ignoring refresh from ControllerManager. NumDeviceClusters=%d, CurrentState=%d"), AutomationController->GetNumDeviceClusters(), int(AutomationTestState)); return; } // We have found some workers // Create a filter to add to the automation controller, otherwise we don't get any reports TSharedPtr AutomationFilters = MakeShareable(new AutomationFilterCollection()); ApplyCVarTagFilter(AutomationFilters); AutomationController->SetFilter(AutomationFilters); AutomationController->SetVisibleTestsEnabled(true); AutomationController->GetEnabledTestNames(FilteredTestNames); //assume we won't run any tests bool bRunTests = false; if (AutomationCommand == EAutomationCommand::ListAllTests) { UE_LOG(LogAutomationCommandLine, Display, TEXT("Found %d Automation Tests"), FilteredTestNames.Num()); for ( const FString& TestName : FilteredTestNames) { UE_LOG(LogAutomationCommandLine, Display, TEXT("\t'%s'"), *TestName); } // Set state to complete AutomationTestState = EAutomationTestState::Complete; } else if (AutomationCommand == EAutomationCommand::RunCommandLineTests) { GenerateTestNamesFromCommandLine(AutomationFilters, FilteredTestNames); if (FilteredTestNames.Num() == 0) { LogCommandLineError(FString::Printf(TEXT("No automation tests matched '%s'"), *StringCommand)); } else { UE_LOG(LogAutomationCommandLine, Display, TEXT("Found %d automation tests based on '%s'"), FilteredTestNames.Num(), *StringCommand); } for ( const FString& TestName : FilteredTestNames ) { UE_LOG(LogAutomationCommandLine, Display, TEXT("\t%s"), *TestName); } if (FilteredTestNames.Num()) { bRunTests = true; } else { AutomationTestState = EAutomationTestState::Complete; } } else if (AutomationCommand == EAutomationCommand::RunFilter) { if (FilterMaps.Contains(StringCommand)) { UE_LOG(LogAutomationCommandLine, Display, TEXT("Running %i Automation Tests"), FilteredTestNames.Num()); if (FilteredTestNames.Num() > 0) { bRunTests = true; } else { AutomationTestState = EAutomationTestState::Complete; } } else { AutomationTestState = EAutomationTestState::Complete; UE_LOG(LogAutomationCommandLine, Display, TEXT("%s is not a valid flag to filter on! Valid options are: "), *StringCommand); TArray FlagNames; FilterMaps.GetKeys(FlagNames); for (int i = 0; i < FlagNames.Num(); i++) { UE_LOG(LogAutomationCommandLine, Display, TEXT("\t%s"), *FlagNames[i]); } } } else if (AutomationCommand == EAutomationCommand::RunAll) { bRunTests = true; } if (bRunTests) { AutomationController->StopTests(); AutomationController->SetEnabledTests(FilteredTestNames); TestCount = FilteredTestNames.Num(); // Clear delegate to avoid re-running tests due to multiple delegates being added or when refreshing session frontend // The delegate will be readded in Init whenever a new command is executed AutomationController->OnTestsRefreshed().Remove(TestsRefreshedHandle); TestsRefreshedHandle.Reset(); AutomationController->RunTests(); // Set state to monitoring to check for test completion AutomationTestState = EAutomationTestState::DoingRequestedWork; } } void MonitorTests() { if (AutomationController->GetTestState() != EAutomationControllerModuleState::Running) { // We have finished the testing, and results are available AutomationTestState = EAutomationTestState::Complete; } } bool Tick(float DeltaTime) { QUICK_SCOPE_CYCLE_COUNTER(STAT_FAutomationExecCmd_Tick); // Update the automation controller to keep it running AutomationController->Tick(); // Update the automation process switch (AutomationTestState) { case EAutomationTestState::Initializing: { if (AutomationController->IsReadyForTests()) { AutomationTestState = EAutomationTestState::Idle; UE_LOG(LogAutomationCommandLine, Display, TEXT("Ready to start automation")); } FindWorkerAttempts = 0; break; } case EAutomationTestState::FindWorkers: { FindWorkers(DeltaTime); break; } case EAutomationTestState::RequestTests: { RequestTests(DeltaTime); break; } case EAutomationTestState::DoingRequestedWork: { MonitorTests(); break; } case EAutomationTestState::Complete: case EAutomationTestState::Idle: default: { //pop next command if (AutomationCommandQueue.Num()) { AutomationCommand = AutomationCommandQueue[0]; AutomationCommandQueue.RemoveAt(0); if (AutomationCommand == EAutomationCommand::Quit || AutomationCommand == EAutomationCommand::SoftQuit) { if (AutomationCommandQueue.IsValidIndex(0) && !IsQuitQueued()) { // Add Quit and SoftQuit commands back to the end of the array. AutomationCommandQueue.Add(AutomationCommand); break; } } AutomationTestState = EAutomationTestState::FindWorkers; } // Only quit if Quit is the actual last element in the array. if (AutomationCommand == EAutomationCommand::Quit || AutomationCommand == EAutomationCommand::SoftQuit) { if (!GIsCriticalError) { if (AutomationController->ReportsHaveErrors() || Errors.Num()) { UE_LOG(LogAutomationCommandLine, Display, TEXT("Setting GIsCriticalError due to test failures (will cause non-zero exit code).")); GIsCriticalError = true; } } UE_LOG(LogAutomationCommandLine, Log, TEXT("Shutting down. GIsCriticalError=%d"), GIsCriticalError ? 1 : 0); // some tools parse this. UE_LOG(LogAutomationCommandLine, Display, TEXT("**** TEST COMPLETE. EXIT CODE: %d ****"), GIsCriticalError ? -1 : 0); FPlatformMisc::RequestExitWithStatus(AutomationCommand == EAutomationCommand::SoftQuit ? false : true, GIsCriticalError ? -1 : 0); // We have finished the testing, and results are available AutomationTestState = EAutomationTestState::Complete; } else if (!IsAboutToRunTest()) { // Register for the callback that tells us there are tests available if (!TestsRefreshedHandle.IsValid()) { TestsRefreshedHandle = AutomationController->OnTestsRefreshed().AddRaw(this, &FAutomationExecCmd::HandleRefreshTestCallback); } } break; } } if (IsTestingComplete()) { AutomationTestState = EAutomationTestState::Idle; TickHandler.Reset(); return false; } return true; } bool IsRunTestQueued() { for (auto Command : AutomationCommandQueue) { if (Command == EAutomationCommand::RunCommandLineTests || Command == EAutomationCommand::RunAll || Command == EAutomationCommand::RunFilter) { return true; } } return false; } bool IsAboutToRunTest() { return (AutomationCommand == EAutomationCommand::RunCommandLineTests || AutomationCommand == EAutomationCommand::RunAll || AutomationCommand == EAutomationCommand::RunFilter); } bool IsQuitQueued() { for (auto Command : AutomationCommandQueue) { if (Command == EAutomationCommand::Quit || Command == EAutomationCommand::SoftQuit) { return true; } } return false; } protected: /** Console commands, see embeded usage statement **/ virtual bool Exec_Dev(UWorld*, const TCHAR* Cmd, FOutputDevice& Ar) override { bool bHandled = false; // figure out if we are handling this request if (FParse::Command(&Cmd, TEXT("Automation"))) { // Early exit in case of a CVar input. ie: Automation.SkipStackWalk 1 if (FString(Cmd).StartsWith(TEXT("."))) { return false; } // Track whether we have a flag we care about passing through. FString FlagToUse = ""; TArray CommandList; FString(Cmd).ParseIntoArray(CommandList, TEXT(";"), true); Init(); //assume we handle this bHandled = true; for (int CommandIndex = 0; CommandIndex < CommandList.Num(); ++CommandIndex) { const TCHAR* TempCmd = *CommandList[CommandIndex]; if (FParse::Command(&TempCmd, TEXT("StartRemoteSession"))) { FString SessionString = TempCmd; if (!FGuid::Parse(SessionString, SessionID)) { Ar.Logf(TEXT("Automation: %s is not a valid session guid!"), *SessionString); bHandled = false; break; } } else if (FParse::Command(&TempCmd, TEXT("List"))) { AutomationCommandQueue.Add(EAutomationCommand::ListAllTests); } else if (FParse::Command(&TempCmd, TEXT("Now"))) { DelayTimer = 0.0f; } else if (FParse::Command(&TempCmd, TEXT("RunTests")) || FParse::Command(&TempCmd, TEXT("RunTest"))) { if (FParse::Command(&TempCmd, TEXT("Now"))) { DelayTimer = 0.0f; continue; } //only one of these should be used if (IsRunTestQueued()) { Ar.Logf(TEXT("Automation: A test run is already Queued: %s. Only one run is supported at a time."), *StringCommand); continue; } StringCommand = TempCmd; Ar.Logf(TEXT("Automation: RunTests='%s' Queued."), *StringCommand); AutomationCommandQueue.Add(EAutomationCommand::RunCommandLineTests); } else if (FParse::Command(&TempCmd, TEXT("SetMinimumPriority"))) { FlagToUse = TempCmd; Ar.Logf(TEXT("Automation: Setting minimum priority of cases to run to: %s"), *FlagToUse); if (FlagToUse.Contains(TEXT("Low"))) { AutomationController->SetRequestedTestFlags(EAutomationTestFlags_PriorityMask); } else if (FlagToUse.Contains(TEXT("Medium"))) { AutomationController->SetRequestedTestFlags(EAutomationTestFlags_MediumPriorityAndAbove); } else if (FlagToUse.Contains(TEXT("High"))) { AutomationController->SetRequestedTestFlags(EAutomationTestFlags_HighPriorityAndAbove); } else if (FlagToUse.Contains(TEXT("Critical"))) { AutomationController->SetRequestedTestFlags(EAutomationTestFlags::CriticalPriority); } else if (FlagToUse.Contains(TEXT("None"))) { AutomationController->SetRequestedTestFlags(EAutomationTestFlags::None); } else { Ar.Logf(TEXT("Automation: %s is not a valid priority!\nValid priorities are Critical, High, Medium, Low, None"), *FlagToUse); } } else if (FParse::Command(&TempCmd, TEXT("SetPriority"))) { FlagToUse = TempCmd; Ar.Logf(TEXT("Setting explicit priority of cases to run to: %s"), *FlagToUse); if (FlagToUse.Contains(TEXT("Low"))) { AutomationController->SetRequestedTestFlags(EAutomationTestFlags::LowPriority); } else if (FlagToUse.Contains(TEXT("Medium"))) { AutomationController->SetRequestedTestFlags(EAutomationTestFlags::MediumPriority); } else if (FlagToUse.Contains(TEXT("High"))) { AutomationController->SetRequestedTestFlags(EAutomationTestFlags::HighPriority); } else if (FlagToUse.Contains(TEXT("Critical"))) { AutomationController->SetRequestedTestFlags(EAutomationTestFlags::CriticalPriority); } else if (FlagToUse.Contains(TEXT("None"))) { AutomationController->SetRequestedTestFlags(EAutomationTestFlags::None); } else { Ar.Logf(TEXT("Automation: %s is not a valid priority!\nValid priorities are Critical, High, Medium, Low, None"), *StringCommand); } } else if (FParse::Command(&TempCmd, TEXT("RunFilter"))) { //only one of these should be used if (IsRunTestQueued()) { Ar.Logf(TEXT("Automation: A test run is already Queued: %s. Only one run is supported at a time."), *StringCommand); continue; } FlagToUse = TempCmd; StringCommand = TempCmd; if (FilterMaps.Contains(FlagToUse)) { AutomationController->SetRequestedTestFlags(FilterMaps[FlagToUse]); Ar.Logf(TEXT("Automation: RunFilter='%s' Queued."), *FlagToUse); } AutomationCommandQueue.Add(EAutomationCommand::RunFilter); } else if (FParse::Command(&TempCmd, TEXT("SetFilter"))) { FlagToUse = TempCmd; if (FilterMaps.Contains(FlagToUse)) { AutomationController->SetRequestedTestFlags(FilterMaps[FlagToUse]); Ar.Logf(TEXT("Automation: Setting test filter: %s"), *FlagToUse); } } else if (FParse::Command(&TempCmd, TEXT("RunAll"))) { //only one of these should be used if (IsRunTestQueued()) { Ar.Logf(TEXT("Automation: A test run is already Queued: %s. Only one run is supported at a time."), *StringCommand); continue; } AutomationCommandQueue.Add(EAutomationCommand::RunAll); AutomationController->SetRequestedTestFlags(FilterMaps["All"]); Ar.Logf(TEXT("Automation: RunAll Queued. NOTE: This may take a while.")); } else if (FParse::Command(&TempCmd, TEXT("Quit"))) { if (IsQuitQueued()) { Ar.Log(TEXT("Automation: Quit command is already Queued.")); continue; } AutomationCommandQueue.Add(EAutomationCommand::Quit); Ar.Logf(TEXT("Automation: Quit Command Queued.")); } else if (FParse::Command(&TempCmd, TEXT("SoftQuit"))) { if (IsQuitQueued()) { Ar.Log(TEXT("Automation: Quit command is already Queued.")); continue; } AutomationCommandQueue.Add(EAutomationCommand::SoftQuit); Ar.Logf(TEXT("Automation: SoftQuit Command Queued.")); } else if (FParse::Command(&TempCmd, TEXT("IgnoreLogEvents"))) { if (IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(TEXT("Automation.CaptureLogEvents"))) { Ar.Logf(TEXT("Automation: Suppressing Log Events")); CVar->Set(false); } } else if (FParse::Command(&TempCmd, TEXT("EnableStereoTests"))) { if (IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(TEXT("Automation.EnableStereoTestVariants"))) { Ar.Logf(TEXT("Automation: Enabling Stereo Test Variants")); CVar->Set(true); } } else if (FParse::Command(&TempCmd, TEXT("SetTagFilter"))) { if (IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(TEXT("Automation.TestTagGlobalFilter"))) { FString FilterValue(TempCmd); Ar.Logf(TEXT("Automation: Setting test tag global filter to: '%s'"), *FilterValue); CVar->Set(*FilterValue); } } else if (FParse::Command(&TempCmd, TEXT("Help"))) { Ar.Logf(TEXT("Supported commands are: ")); Ar.Logf(TEXT("\tAutomation StartRemoteSession ")); Ar.Logf(TEXT("\tAutomation List")); Ar.Logf(TEXT("\tAutomation RunTests ")); Ar.Logf(TEXT("\tAutomation RunAll")); Ar.Logf(TEXT("\tAutomation RunFilter ")); Ar.Logf(TEXT("\tAutomation SetFilter ")); Ar.Logf(TEXT("\tAutomation SetMinimumPriority ")); Ar.Logf(TEXT("\tAutomation SetPriority ")); Ar.Logf(TEXT("\tAutomation Now")); Ar.Logf(TEXT("\tAutomation Quit")); Ar.Logf(TEXT("\tAutomation SoftQuit")); Ar.Logf(TEXT("\tAutomation IgnoreLogEvents")); Ar.Logf(TEXT("\tAutomation EnableStereoTests")); Ar.Logf(TEXT("\tAutomation SetTagFilter")); bHandled = false; } else { Ar.Logf(TEXT("Unknown Automation command '%s'! Use Help command for a detailed list."), TempCmd); bHandled = false; } } } // Shutdown our service return bHandled; } private: // Logs and tracks an error void LogCommandLineError(const FString& InErrorMsg) { UE_LOG(LogAutomationCommandLine, Error, TEXT("%s"), *InErrorMsg); Errors.Add(InErrorMsg); } /** The automation controller running the tests */ IAutomationControllerManagerPtr AutomationController; /** The current state of the automation process */ EAutomationTestState AutomationTestState; /** What work was requested */ TArray AutomationCommandQueue; /** What work was requested */ EAutomationCommand AutomationCommand; /** Handle to Test Refresh delegate */ FDelegateHandle TestsRefreshedHandle; /** Delay used before finding workers on game instances. Just to ensure they have started up */ float DelayTimer; /** Timer Handle for giving up on workers */ float FindWorkersTimeout; /** How many times we attempted to find a worker... */ int FindWorkerAttempts; /** Holds the session ID */ FGuid SessionID; //so we can release control of the app and just get ticked like all other systems FTSTicker::FDelegateHandle TickHandler; //Extra commandline params FString StringCommand; //This is the numbers of tests that are found in the command line. int32 TestCount; //Dictionary that maps flag names to flag values. TMap FilterMaps; // Any that we encountered during processing. Used in 'Quit' to determine error code TArray Errors; }; const float FAutomationExecCmd::DefaultDelayTimer = 5.0f; const float FAutomationExecCmd::DefaultFindWorkersTimeout = 30.0f; static FAutomationExecCmd AutomationExecCmd; void EmptyLinkFunctionForStaticInitializationAutomationExecCmd() { // This function exists to prevent the object file containing this test from // being excluded by the linker, because it has no publicly referenced symbols. }