// Copyright Epic Games, Inc. All Rights Reserved. #include "FunctionalTestingModule.h" #include "AssetRegistry/AssetData.h" #include "FunctionalTestingManager.h" #include "Misc/CoreMisc.h" #include "Misc/ConfigCacheIni.h" #include "Engine/World.h" #include "Engine/Engine.h" #include "EngineUtils.h" #include "FunctionalTest.h" #include "EngineGlobals.h" #include "AssetRegistry/ARFilter.h" #include "AssetRegistry/AssetRegistryModule.h" #include "Misc/CommandLine.h" #include "IAutomationControllerModule.h" #include "UObject/AssetRegistryTagsContext.h" #define LOCTEXT_NAMESPACE "FunctionalTesting" DEFINE_LOG_CATEGORY(LogFunctionalTest); class FFunctionalTestingModule : public IFunctionalTestingModule { public: virtual void StartupModule() override; virtual void ShutdownModule() override; virtual void RunAllTestsOnMap(bool bClearLog, bool bRunLooped) override; virtual void RunTestOnMap(const FString& TestName, bool bClearLog, bool bRunLooped) override; virtual void MarkPendingActivation() override; virtual bool IsActivationPending() const override; virtual bool IsRunning() const override; virtual bool IsFinished() const override; virtual void SetManager(class UFunctionalTestingManager* NewManager) override; virtual class UFunctionalTestingManager* GetCurrentManager(); virtual void SetLooping(const bool bLoop) override; virtual void GetMapTests(bool bEditorOnlyTests, TArray& OutBeautifiedNames, TArray& OutTestCommands, TArray& OutTestMapAssets) const override; virtual void GetMapTests(bool bEditorOnlyTests, TArray& OutTestInfo, TArray& OutTestMapAssets) const override; private: UWorld* GetTestWorld(); void OnGetAssetTagsForWorld(const UWorld* World, FAssetRegistryTagsContext Context); TWeakObjectPtr TestManager; bool bPendingActivation; }; void FFunctionalTestingModule::StartupModule() { bPendingActivation = false; #if WITH_EDITOR FWorldDelegates::GetAssetTagsWithContext.AddRaw(this, &FFunctionalTestingModule::OnGetAssetTagsForWorld); #endif } void FFunctionalTestingModule::ShutdownModule() { #if WITH_EDITOR FWorldDelegates::GetAssetTagsWithContext.RemoveAll(this); #endif } void FFunctionalTestingModule::OnGetAssetTagsForWorld(const UWorld* World, FAssetRegistryTagsContext Context) { #if WITH_EDITOR if (!World || World->HasAllFlags(RF_ClassDefaultObject)) { return; } TArray TestNamesRuntime; TArray TestNamesEditor; for (TActorIterator ActorItr(const_cast(World), AFunctionalTest::StaticClass(), EActorIteratorFlags::AllActors); ActorItr; ++ActorItr) { AFunctionalTest* FunctionalTest = *ActorItr; if (!FunctionalTest->IsPackageExternal()) { // Only include enabled tests in the list of functional tests to run. if (FunctionalTest->IsEnabledInWorld(World)) { TArray& TestNames = IsEditorOnlyObject(FunctionalTest) ? TestNamesEditor : TestNamesRuntime; TestNames.Add(FString::Printf(TEXT("%s|%s|%s;"), *FunctionalTest->GetActorLabel(), *FunctionalTest->GetName(), *FunctionalTest->TestTags)); } } } auto AddTestNames = [&Context](const TCHAR* TagName, TArray& TestNames) { if (!TestNames.IsEmpty()) { TestNames.Sort(); FString TestNamesStr = FString::Join(TestNames, TEXT("")); Context.AddTag(UObject::FAssetRegistryTag(TagName, MoveTemp(TestNamesStr), UObject::FAssetRegistryTag::TT_Hidden)); } }; AddTestNames(TEXT("TestNames"), TestNamesRuntime); AddTestNames(TEXT("TestNamesEditor"), TestNamesEditor); #endif } void FFunctionalTestingModule::GetMapTests(bool bEditorOnlyTests, TArray& OutBeautifiedNames, TArray& OutTestCommands, TArray& OutTestMapAssets) const { TArray TestInfo; GetMapTests(bEditorOnlyTests, TestInfo, OutTestMapAssets); for (FFunctionalTestInfo Info : TestInfo) { OutBeautifiedNames.Add(Info.BeautifiedName); OutTestCommands.Add(Info.TestCommand); } } void FFunctionalTestingModule::GetMapTests(bool bEditorOnlyTests, TArray& OutTestInfo, TArray& OutTestMapAssets) const { IAssetRegistry& AssetRegistry = FModuleManager::Get().LoadModuleChecked("AssetRegistry").Get(); if (!AssetRegistry.IsLoadingAssets()) { #if WITH_EDITOR static bool bDidScan = false; if (!GIsEditor && !bDidScan) { // For editor build -game, we need to do a full scan AssetRegistry.SearchAllAssets(true); bDidScan = true; } #endif TArray MapList; FARFilter Filter; Filter.ClassPaths.Add(UWorld::StaticClass()->GetClassPathName()); Filter.bRecursiveClasses = true; Filter.bIncludeOnlyOnDiskAssets = true; if (AssetRegistry.GetAssets(Filter, /*out*/ MapList)) { IAutomationControllerModule& AutomationControllerModule = FModuleManager::LoadModuleChecked(TEXT("AutomationController")); IAutomationControllerManagerPtr AutomationController = AutomationControllerModule.GetAutomationController(); bool IsDeveloperDirectoryIncluded = AutomationController->IsDeveloperDirectoryIncluded(); for (const FAssetData& MapAsset : MapList) { FString MapAssetPath = MapAsset.GetObjectPathString(); FString MapPackageName = MapAsset.PackageName.ToString(); if (!IsDeveloperDirectoryIncluded && MapPackageName.Find(TEXT("/Game/Developers")) == 0) continue; FString PartialSuiteName = MapPackageToAutomationPath(MapAsset); TArray MapTests; FAssetDataTagMapSharedView::FFindTagResult MapAssetNamesResult = MapAsset.TagsAndValues.FindTag(bEditorOnlyTests ? TEXT("TestNamesEditor") : TEXT("TestNames")); if (MapAssetNamesResult.IsSet()) { MapAssetNamesResult.GetValue().ParseIntoArray(MapTests, TEXT(";"), true); } #if WITH_EDITOR // Also append external functional test actors if (ULevel::GetIsLevelUsingExternalActorsFromAsset(MapAsset)) { const FString LevelExternalActorsPath = ULevel::GetExternalActorsPath(MapPackageName); // Do a synchronous scan of the level external actors path. AssetRegistry.ScanPathsSynchronous({ LevelExternalActorsPath }, /*bForceRescan*/false, /*bIgnoreDenyListScanFilters*/false); FARFilter ActorsFilter; ActorsFilter.bRecursivePaths = true; ActorsFilter.bIncludeOnlyOnDiskAssets = true; ActorsFilter.PackagePaths.Add(*LevelExternalActorsPath); TArray ActorList; AssetRegistry.GetAssets(ActorsFilter, ActorList); for (const FAssetData& ActorAsset : ActorList) { FAssetDataTagMapSharedView::FFindTagResult ActorTestName = ActorAsset.TagsAndValues.FindTag(bEditorOnlyTests ? TEXT("TestNameEditor") : TEXT("TestName")); if (ActorTestName.IsSet()) { MapTests.Add(ActorTestName.GetValue()); } } } #endif if (!MapTests.IsEmpty()) { for (const FString& MapTest : MapTests) { FString Remainder; FString BeautifulTestName; FString RealTestName; FString TestTags; if (MapTest.Split(TEXT("|"), &BeautifulTestName, &Remainder)) { if (!Remainder.Split(TEXT("|"), &RealTestName, &TestTags)) { // split fails when loading an old format test-asset without tags RealTestName = Remainder; // tags remain empty } FString FullBeautifiedName(PartialSuiteName + TEXT(".") + *BeautifulTestName); FString TestCommand(MapAssetPath + TEXT(";") + MapPackageName + TEXT(";") + *RealTestName); OutTestInfo.Add(FFunctionalTestInfo(FullBeautifiedName, TestCommand, TestTags)); OutTestMapAssets.AddUnique(MapAssetPath); } } } else if (!bEditorOnlyTests && MapAsset.AssetName.ToString().StartsWith(TEXT("FTEST_"))) { // add legacy functional test maps which autoplay FString FullBeautifiedName(MapAsset.AssetName.ToString()); FString TestCommand(MapAssetPath + TEXT(";") + MapPackageName); OutTestInfo.Add(FFunctionalTestInfo(FullBeautifiedName, TestCommand, "")); // Tags not supported OutTestMapAssets.AddUnique(MapAssetPath); } } } } } void FFunctionalTestingModule::SetManager(class UFunctionalTestingManager* NewManager) { TestManager = NewManager; } UFunctionalTestingManager* FFunctionalTestingModule::GetCurrentManager() { return TestManager.Get(); } bool FFunctionalTestingModule::IsRunning() const { return TestManager.IsValid() && TestManager->IsRunning(); } bool FFunctionalTestingModule::IsFinished() const { return (!TestManager.IsValid() || TestManager->IsFinished()); } void FFunctionalTestingModule::MarkPendingActivation() { bPendingActivation = true; } bool FFunctionalTestingModule::IsActivationPending() const { return bPendingActivation; } void FFunctionalTestingModule::SetLooping(const bool bLoop) { if (TestManager.IsValid()) { TestManager->SetLooped(bLoop); } } UWorld* FFunctionalTestingModule::GetTestWorld() { #if WITH_EDITOR const TIndirectArray& WorldContexts = GEngine->GetWorldContexts(); for (const FWorldContext& Context : WorldContexts) { if (Context.World() != nullptr) { if (Context.WorldType == EWorldType::PIE /*&& Context.PIEInstance == 0*/) { return Context.World(); } if (Context.WorldType == EWorldType::Game) { return Context.World(); } } } #endif return GWorld; } void FFunctionalTestingModule::RunAllTestsOnMap(bool bClearLog, bool bRunLooped) { if (UWorld* TestWorld = GetTestWorld()) { bPendingActivation = false; if (UFunctionalTestingManager::RunAllFunctionalTests(TestWorld, bClearLog, bRunLooped) == false) { UE_LOG(LogFunctionalTest, Error, TEXT("No functional testing script on map.")); } } } void FFunctionalTestingModule::RunTestOnMap(const FString& TestName, bool bClearLog, bool bRunLooped) { if (UWorld* TestWorld = GetTestWorld()) { bPendingActivation = false; if (UFunctionalTestingManager::RunAllFunctionalTests(TestWorld, bClearLog, bRunLooped, TestName) == false) { UE_LOG(LogFunctionalTest, Error, TEXT("No functional testing script on map.")); } } } ////////////////////////////////////////////////////////////////////////// // Exec ////////////////////////////////////////////////////////////////////////// static bool FuncTestExec(UWorld* InWorld, const TCHAR* Command, FOutputDevice& Ar) { if (FParse::Command(&Command, TEXT("ftest"))) { if (FParse::Command(&Command, TEXT("start"))) { const bool bLooped = FParse::Command(&Command, TEXT("loop")); //instead of allowing straight use of the functional test framework, this should go through the automation framework and kick off one of the Editor/Client functional tests IFunctionalTestingModule& Module = IFunctionalTestingModule::Get(); if (!Module.IsRunning() && !Module.IsActivationPending()) { Module.RunAllTestsOnMap(/*bClearLog=*/true, bLooped); } } return true; } return false; } FStaticSelfRegisteringExec FuncTestExecRegistration(FuncTestExec); IMPLEMENT_MODULE( FFunctionalTestingModule, FunctionalTesting ); #undef LOCTEXT_NAMESPACE