// Copyright Epic Games, Inc. All Rights Reserved. #include "Tests/AutomationEditorCommon.h" #include "UObject/UnrealType.h" #include "HAL/FileManager.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "Modules/ModuleManager.h" #include "UObject/UObjectHash.h" #include "UObject/UObjectIterator.h" #include "Serialization/FindReferencersArchive.h" #include "Editor/UnrealEdEngine.h" #include "Factories/Factory.h" #include "Factories/TextureFactory.h" #include "Framework/Application/SlateApplication.h" #include "Engine/StaticMesh.h" #include "Misc/ConfigCacheIni.h" #include "Serialization/ArchiveReplaceObjectRef.h" #include "GameFramework/WorldSettings.h" #include "Engine/StaticMeshActor.h" #include "LevelEditorViewport.h" #include "EditorModeManager.h" #include "EditorModes.h" #include "FileHelpers.h" #include "UnrealEdGlobals.h" #include "AssetRegistry/ARFilter.h" #include "AssetRegistry/AssetRegistryModule.h" #include "Tests/AutomationCommon.h" #include "IAssetViewport.h" #include "LevelEditor.h" #include "Interfaces/IMainFrameModule.h" #include "ShaderCompiler.h" #include "AssetSelection.h" #include "ITargetDeviceProxy.h" #include "ITargetDeviceServicesModule.h" #include "ILauncherWorker.h" #include "CookOnTheSide/CookOnTheFlyServer.h" #include "LightingBuildOptions.h" #include "Subsystems/AssetEditorSubsystem.h" #include "Bookmarks/IBookmarkTypeTools.h" #include "GameMapsSettings.h" #include "Editor/EditorPerformanceSettings.h" #include "TextureCompiler.h" #if WITH_AUTOMATION_TESTS #define COOK_TIMEOUT 3600 DEFINE_LOG_CATEGORY_STATIC(LogAutomationEditorCommon, Log, All); UWorld* FAutomationEditorCommonUtils::CreateNewMap() { // Also change out of Landscape mode to ensure all references are cleared. if ( GLevelEditorModeTools().IsModeActive(FBuiltinEditorModes::EM_Landscape) ) { GLevelEditorModeTools().DeactivateMode(FBuiltinEditorModes::EM_Landscape); } // Also change out of Foliage mode to ensure all references are cleared. if ( GLevelEditorModeTools().IsModeActive(FBuiltinEditorModes::EM_Foliage) ) { GLevelEditorModeTools().DeactivateMode(FBuiltinEditorModes::EM_Foliage); } // Change out of mesh paint mode when opening a new map. if ( GLevelEditorModeTools().IsModeActive(FBuiltinEditorModes::EM_MeshPaint) ) { GLevelEditorModeTools().DeactivateMode(FBuiltinEditorModes::EM_MeshPaint); } return GEditor->NewMap(); } TUniquePtr FAutomationEditorCommonUtils::CreateScopedEditorWorld( const FString& TemplateNameOrPath, const UWorld::InitializationValues& InInitializationValues, EWorldType::Type InWorldType) { // Get the list of template maps TArray TemplateMaps; FString TemplatePath; if (GUnrealEd) { // Get the list of template maps from the editor engine TemplateMaps = GUnrealEd->GetTemplateMapInfos(); } // Search for the template by its display name for (const FTemplateMapInfo& Template : TemplateMaps) { if (Template.DisplayName.ToString() == TemplateNameOrPath) { TemplatePath = Template.Map.ToString(); break; } } // Create a new FScopedEditorWorld using the template. if (!TemplatePath.IsEmpty()) { return MakeUnique(TSoftObjectPtr(FSoftObjectPath(TemplatePath)), InInitializationValues, InWorldType); } // Create a new FScopedEditorWorld using the path to the level. TUniquePtr ScopedWorld = MakeUnique(TSoftObjectPtr(FSoftObjectPath(TemplateNameOrPath)), InInitializationValues, InWorldType); if (!ScopedWorld) { UE_LOG(LogAutomationEditorCommon, Error, TEXT("Failed to create scoped editor world. Could not resolve template name or path: %s"), *TemplateNameOrPath); } return ScopedWorld; } /** * Imports an object using a given factory * * @param ImportFactory - The factory to use to import the object * @param ObjectName - The name of the object to create * @param PackagePath - The full path of the package file to create * @param ImportPath - The path to the object to import */ UObject* FAutomationEditorCommonUtils::ImportAssetUsingFactory(UFactory* ImportFactory, const FString& ObjectName, const FString& PackagePath, const FString& ImportPath) { UObject* ImportedAsset = NULL; UPackage* Pkg = CreatePackage( *PackagePath); if (Pkg) { // Make sure the destination package is loaded Pkg->FullyLoad(); UClass* ImportAssetType = ImportFactory->ResolveSupportedClass(); bool bDummy = false; //If we are a texture factory suppress some warning dialog that we don't want if (ImportFactory->IsA(UTextureFactory::StaticClass())) { UTextureFactory::SuppressImportOverwriteDialog(); } bool OutCanceled = false; ImportedAsset = ImportFactory->ImportObject(ImportAssetType, Pkg, FName(*ObjectName), RF_Public | RF_Standalone, ImportPath, nullptr, OutCanceled); if (ImportedAsset != nullptr) { UE_LOG(LogAutomationEditorCommon, Display, TEXT("Imported %s"), *ImportPath); } else if (OutCanceled) { UE_LOG(LogAutomationEditorCommon, Display, TEXT("Canceled import of %s"), *ImportPath); } else { UE_LOG(LogAutomationEditorCommon, Error, TEXT("Failed to import asset using factory %s!"), *ImportFactory->GetName()); } } else { UE_LOG(LogAutomationEditorCommon, Error, TEXT("Failed to create a package!")); } return ImportedAsset; } /** * Nulls out references to a given object * * @param InObject - Object to null references to */ void FAutomationEditorCommonUtils::NullReferencesToObject(UObject* InObject) { TArray ReplaceableObjects; TMap ReplacementMap; ReplacementMap.Add(InObject, NULL); ReplacementMap.GenerateKeyArray(ReplaceableObjects); // Find all the properties (and their corresponding objects) that refer to any of the objects to be replaced TMap< UObject*, TArray > ReferencingPropertiesMap; for (FThreadSafeObjectIterator ObjIter; ObjIter; ++ObjIter) { UObject* CurObject = *ObjIter; // Find the referencers of the objects to be replaced FFindReferencersArchive FindRefsArchive(CurObject, ReplaceableObjects); // Inform the object referencing any of the objects to be replaced about the properties that are being forcefully // changed, and store both the object doing the referencing as well as the properties that were changed in a map (so that // we can correctly call PostEditChange later) TMap CurNumReferencesMap; TMultiMap CurReferencingPropertiesMMap; if (FindRefsArchive.GetReferenceCounts(CurNumReferencesMap, CurReferencingPropertiesMMap) > 0) { TArray CurReferencedProperties; CurReferencingPropertiesMMap.GenerateValueArray(CurReferencedProperties); ReferencingPropertiesMap.Add(CurObject, CurReferencedProperties); for (TArray::TConstIterator RefPropIter(CurReferencedProperties); RefPropIter; ++RefPropIter) { CurObject->PreEditChange(*RefPropIter); } } } // Iterate over the map of referencing objects/changed properties, forcefully replacing the references and then // alerting the referencing objects the change has completed via PostEditChange int32 NumObjsReplaced = 0; for (TMap< UObject*, TArray >::TConstIterator MapIter(ReferencingPropertiesMap); MapIter; ++MapIter) { ++NumObjsReplaced; UObject* CurReplaceObj = MapIter.Key(); const TArray& RefPropArray = MapIter.Value(); FArchiveReplaceObjectRef ReplaceAr(CurReplaceObj, ReplacementMap, EArchiveReplaceObjectFlags::IgnoreOuterRef); for (TArray::TConstIterator RefPropIter(RefPropArray); RefPropIter; ++RefPropIter) { FPropertyChangedEvent PropertyEvent(*RefPropIter); CurReplaceObj->PostEditChangeProperty(PropertyEvent); } if (!CurReplaceObj->HasAnyFlags(RF_Transient) && CurReplaceObj->GetOutermost() != GetTransientPackage()) { if (!CurReplaceObj->RootPackageHasAnyFlags(PKG_CompiledIn)) { CurReplaceObj->MarkPackageDirty(); } } } } /** * gets a factory class based off an asset file extension * * @param AssetExtension - The file extension to use to find a supporting UFactory */ UClass* FAutomationEditorCommonUtils::GetFactoryClassForType(const FString& AssetExtension) { // First instantiate one factory for each file extension encountered that supports the extension for (TObjectIterator ClassIt; ClassIt; ++ClassIt) { if ((*ClassIt)->IsChildOf(UFactory::StaticClass()) && !((*ClassIt)->HasAnyClassFlags(CLASS_Abstract))) { UFactory* Factory = Cast((*ClassIt)->GetDefaultObject()); if (Factory->bEditorImport) { TArray FactoryExtensions; Factory->GetSupportedFileExtensions(FactoryExtensions); // Case insensitive string compare with supported formats of this factory if (FactoryExtensions.Contains(AssetExtension)) { return *ClassIt; } } } } return NULL; } /** * Applies settings to an object by finding UProperties by name and calling ImportText * * @param InObject - The object to search for matching properties * @param PropertyChain - The list FProperty names recursively to search through * @param Value - The value to import on the found property */ void FAutomationEditorCommonUtils::ApplyCustomFactorySetting(UObject* InObject, TArray& PropertyChain, const FString& Value) { const FString PropertyName = PropertyChain[0]; PropertyChain.RemoveAt(0); FProperty* TargetProperty = FindFProperty(InObject->GetClass(), *PropertyName); if (TargetProperty) { if (PropertyChain.Num() == 0) { TargetProperty->ImportText_InContainer(*Value, InObject, InObject, 0); } else { FStructProperty* StructProperty = CastField(TargetProperty); FObjectProperty* ObjectProperty = CastField(TargetProperty); UObject* SubObject = NULL; bool bValidPropertyType = true; if (StructProperty) { SubObject = StructProperty->Struct; } else if (ObjectProperty) { SubObject = ObjectProperty->GetObjectPropertyValue(ObjectProperty->ContainerPtrToValuePtr(InObject)); } else { //Unknown nested object type bValidPropertyType = false; UE_LOG(LogAutomationEditorCommon, Error, TEXT("ERROR: Unknown nested object type for property: %s"), *PropertyName); } if (SubObject) { ApplyCustomFactorySetting(SubObject, PropertyChain, Value); } else if (bValidPropertyType) { UE_LOG(LogAutomationEditorCommon, Error, TEXT("Error accessing null property: %s"), *PropertyName); } } } else { UE_LOG(LogAutomationEditorCommon, Error, TEXT("ERROR: Could not find factory property: %s"), *PropertyName); } } /** * Applies the custom factory settings * * @param InFactory - The factory to apply custom settings to * @param FactorySettings - An array of custom settings to apply to the factory */ void FAutomationEditorCommonUtils::ApplyCustomFactorySettings(UFactory* InFactory, const TArray& FactorySettings) { bool bCallConfigureProperties = true; for (int32 i = 0; i < FactorySettings.Num(); ++i) { if (FactorySettings[i].SettingName.Len() > 0 && FactorySettings[i].Value.Len() > 0) { //Check if we are setting an FBX import type override. If we are, we don't want to call ConfigureProperties because that enables bDetectImportTypeOnImport if (FactorySettings[i].SettingName.Contains(TEXT("MeshTypeToImport"))) { bCallConfigureProperties = false; } TArray PropertyChain; FactorySettings[i].SettingName.ParseIntoArray(PropertyChain, TEXT("."), false); ApplyCustomFactorySetting(InFactory, PropertyChain, FactorySettings[i].Value); } } if (bCallConfigureProperties) { InFactory->ConfigureProperties(); } } /** * Writes a number to a text file. * @param InTestName is the folder that has the same name as the test. (For Example: "Performance"). * @param InItemBeingTested is the name for the thing that is being tested. (For Example: "MapName"). * @param InFileName is the name of the file with an extension * @param InEntry is the double-precision number that is expected to be written to the file. * @param Delimiter is the delimiter to be used. TEXT(",") */ void FAutomationEditorCommonUtils::WriteToTextFile(const FString& InTestName, const FString& InTestItem, const FString& InFileName, const double& InEntry, const FString& Delimiter) { //Performance file locations and setups. FString FileSaveLocation = FPaths::Combine(*FPaths::AutomationLogDir(), *InTestName, *InTestItem, *InFileName); if (FPaths::FileExists(FileSaveLocation)) { //The text files existing content. FString TextFileContents; //Write to the text file the combined contents from the text file with the number to write. FFileHelper::LoadFileToString(TextFileContents, *FileSaveLocation); FString FileSetup = TextFileContents + Delimiter + FString::SanitizeFloat(InEntry); FFileHelper::SaveStringToFile(FileSetup, *FileSaveLocation); return; } FFileHelper::SaveStringToFile(FString::SanitizeFloat(InEntry), *FileSaveLocation); } /** * Returns the sum of the numbers available in an array of float. * @param InFloatArray is the name of the array intended to be used. * @param bisAveragedInstead will return the average of the available numbers instead of the sum. */ float FAutomationEditorCommonUtils::TotalFromFloatArray(const TArray& InFloatArray, bool bIsAveragedInstead) { //Total Value holds the sum of all the numbers available in the array. float TotalValue = 0; //Get the sum of the array. for (int32 I = 0; I < InFloatArray.Num(); ++I) { TotalValue += InFloatArray[I]; } //If bAverageInstead equals true then only the average is returned. if (bIsAveragedInstead) { UE_LOG(LogEditorAutomationTests, VeryVerbose, TEXT("Average value of the Array is %f"), (TotalValue / InFloatArray.Num())); return (TotalValue / InFloatArray.Num()); } UE_LOG(LogEditorAutomationTests, VeryVerbose, TEXT("Total Value of the Array is %f"), TotalValue); return TotalValue; } /** * Returns the largest value from an array of float numbers. * @param InFloatArray is the name of the array intended to be used. */ float FAutomationEditorCommonUtils::LargestValueInFloatArray(const TArray& InFloatArray) { //Total Value holds the sum of all the numbers available in the array. float LargestValue = 0; //Find the largest value for (int32 I = 0; I < InFloatArray.Num(); ++I) { if (LargestValue < InFloatArray[I]) { LargestValue = InFloatArray[I]; } } UE_LOG(LogEditorAutomationTests, VeryVerbose, TEXT("The Largest value of the array is %f"), LargestValue); return LargestValue; } /** * Returns the contents of a text file as an array of FString. * @param InFileLocation - is the location of the file. * @param OutArray - The name of the array where the */ void FAutomationEditorCommonUtils::CreateArrayFromFile(const FString& InFileLocation, TArray& OutArray) { FString RawData; if (FPaths::FileExists(InFileLocation)) { UE_LOG(LogEditorAutomationTests, VeryVerbose, TEXT("Loading and parsing the data from '%s' into an array."), *InFileLocation); FFileHelper::LoadFileToString(RawData, *InFileLocation); RawData.ParseIntoArray(OutArray, TEXT(","), false); } UE_LOG(LogEditorAutomationTests, Warning, TEXT("Unable to create an array. '%s' does not exist."), *InFileLocation); RawData = TEXT("0"); OutArray.Add(RawData); } /** * Returns true if the archive/file can be written to otherwise false.. * @param InFilePath - is the location of the file. * @param InArchiveName - is the name of the archive to be used. */ bool FAutomationEditorCommonUtils::IsArchiveWriteable(const FString& InFilePath, const FArchive* InArchiveName) { if (!InArchiveName) { UE_LOG(LogEditorAutomationTests, Error, TEXT("Failed to write to the csv file: %s"), *FPaths::ConvertRelativePathToFull(InFilePath)); return false; } return true; } void FAutomationEditorCommonUtils::GetLaunchOnDeviceID(FString& OutDeviceID, const FString& InMapName) { UAutomationTestSettings const* AutomationTestSettings = GetDefault(); check(AutomationTestSettings); OutDeviceID = "None"; FString LaunchOnDeviceId; for (auto LaunchIter = AutomationTestSettings->LaunchOnSettings.CreateConstIterator(); LaunchIter; LaunchIter++) { FString LaunchOnSettings = LaunchIter->DeviceID; FString LaunchOnMap = FPaths::GetBaseFilename(LaunchIter->LaunchOnTestmap.FilePath); if (LaunchOnMap.Equals(InMapName)) { // shared devices section ITargetDeviceServicesModule* TargetDeviceServicesModule = static_cast(FModuleManager::Get().LoadModule(TEXT("TargetDeviceServices"))); // for each platform... TArray> DeviceProxies; TargetDeviceServicesModule->GetDeviceProxyManager()->GetProxies(FName(*LaunchOnSettings), true, DeviceProxies); // for each proxy... for (auto DeviceProxyIt = DeviceProxies.CreateIterator(); DeviceProxyIt; ++DeviceProxyIt) { TSharedPtr DeviceProxy = *DeviceProxyIt; if (DeviceProxy->IsConnected()) { OutDeviceID = DeviceProxy->GetTargetDeviceId((FName)*LaunchOnSettings); break; } } } } } void FAutomationEditorCommonUtils::GetLaunchOnDeviceID(FString& OutDeviceID, const FString& InMapName, const FString& InDeviceName) { UAutomationTestSettings const* AutomationTestSettings = GetDefault(); check(AutomationTestSettings); //Output device name will default to "None". OutDeviceID = "None"; // shared devices section ITargetDeviceServicesModule* TargetDeviceServicesModule = static_cast(FModuleManager::Get().LoadModule(TEXT("TargetDeviceServices"))); // for each platform... TArray> DeviceProxies; TargetDeviceServicesModule->GetDeviceProxyManager()->GetProxies(FName(*InDeviceName), true, DeviceProxies); // for each proxy... for (auto DeviceProxyIt = DeviceProxies.CreateIterator(); DeviceProxyIt; ++DeviceProxyIt) { TSharedPtr DeviceProxy = *DeviceProxyIt; if (DeviceProxy->IsConnected()) { OutDeviceID = DeviceProxy->GetTargetDeviceId((FName)*InDeviceName); break; } } } bool FAutomationEditorCommonUtils::SetOrthoViewportView(const FVector& ViewLocation, const FRotator& ViewRotation) { for (FLevelEditorViewportClient* ViewportClient : GEditor->GetLevelViewportClients()) { if (!ViewportClient->IsOrtho()) { ViewportClient->SetViewLocation(ViewLocation); ViewportClient->SetViewRotation(ViewRotation); return true; } } UE_LOG(LogEditorAutomationTests, Log, TEXT("An ortho viewport was not found. May affect the test results.")); return false; } bool FAutomationEditorCommonUtils::SetPlaySessionStartToActiveViewport(FRequestPlaySessionParams& OutParams) { FLevelEditorModule& LevelEditorModule = FModuleManager::GetModuleChecked(TEXT("LevelEditor")); TSharedPtr ActiveLevelViewport = LevelEditorModule.GetFirstActiveViewport(); // Make sure we can find a path to the view port. if (ActiveLevelViewport.IsValid() && FSlateApplication::Get().FindWidgetWindow(ActiveLevelViewport->AsWidget()).IsValid()) { // Start the player where the camera is if not forcing from player start OutParams.StartLocation = ActiveLevelViewport->GetAssetViewportClient().GetViewLocation(); OutParams.StartRotation = ActiveLevelViewport->GetAssetViewportClient().GetViewRotation(); return true; } return false; } ////////////////////////////////////////////////////////////////////// //Asset Path Commands /** * Converts a package path to an asset path * * @param PackagePath - The package path to convert */ FString FAutomationEditorCommonUtils::ConvertPackagePathToAssetPath(const FString& PackagePath) { const FString Filename = FPaths::ConvertRelativePathToFull(PackagePath); FString EngineFileName = Filename; FString GameFileName = Filename; FString ProjectPluginFileName = Filename; FString EnginePluginFileName = Filename; if (FPaths::MakePathRelativeTo(EngineFileName, *FPaths::EngineContentDir()) && !EngineFileName.Contains(TEXT("../"))) { const FString ShortName = FPaths::GetBaseFilename(EngineFileName); const FString PathName = FPaths::GetPath(EngineFileName); const FString AssetName = FString::Printf(TEXT("/Engine/%s/%s.%s"), *PathName, *ShortName, *ShortName); return AssetName; } else if (FPaths::MakePathRelativeTo(GameFileName, *FPaths::ProjectContentDir()) && !GameFileName.Contains(TEXT("../"))) { const FString ShortName = FPaths::GetBaseFilename(GameFileName); const FString PathName = FPaths::GetPath(GameFileName); const FString AssetName = FString::Printf(TEXT("/Game/%s/%s.%s"), *PathName, *ShortName, *ShortName); return AssetName; } else if (FPaths::MakePathRelativeTo(ProjectPluginFileName, *FPaths::ProjectPluginsDir()) && !ProjectPluginFileName.Contains(TEXT("../"))) { const FString ShortName = FPaths::GetBaseFilename(ProjectPluginFileName); const FString FullPathName = FPaths::GetPath(ProjectPluginFileName); const FString CleanedPathName = FullPathName.Replace(TEXT("Content/"), TEXT("")); const FString AssetName = FString::Printf(TEXT("/%s/%s.%s"), *CleanedPathName, *ShortName, *ShortName); return AssetName; } else if (FPaths::MakePathRelativeTo(EnginePluginFileName, *FPaths::EnginePluginsDir()) && !EnginePluginFileName.Contains(TEXT("../"))) { const FString ShortName = FPaths::GetBaseFilename(EnginePluginFileName); const FString FullPathName = FPaths::GetPath(EnginePluginFileName); const FString CleanedPathName = FullPathName.Replace(TEXT("Content/"), TEXT("")); const FString AssetName = FString::Printf(TEXT("/%s/%s.%s"), *CleanedPathName, *ShortName, *ShortName); return AssetName; } else { UE_LOG(LogAutomationEditorCommon, Error, TEXT("PackagePath (%s) is invalid for the current project"), *PackagePath); return TEXT(""); } } /** * Gets the asset data from a package path * * @param PackagePath - The package path used to look up the asset data */ FAssetData FAutomationEditorCommonUtils::GetAssetDataFromPackagePath(const FString& PackagePath) { FString AssetPath = FAutomationEditorCommonUtils::ConvertPackagePathToAssetPath(PackagePath); if (AssetPath.Len() > 0) { IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")).Get(); return AssetRegistry.GetAssetByObjectPath(FSoftObjectPath(AssetPath)); } return FAssetData(); } ////////////////////////////////////////////////////////////////////// //Find Asset Commands /** * Generates a list of assets from the ENGINE and the GAME by a specific type. * This is to be used by the GetTest() function. */ void FAutomationEditorCommonUtils::CollectTestsByClass(UClass * Class, TArray& OutBeautifiedNames, TArray & OutTestCommands) { FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); TArray ObjectList; AssetRegistryModule.Get().GetAssetsByClass(Class->GetClassPathName(), ObjectList); for (TObjectIterator AllClassesIt; AllClassesIt; ++AllClassesIt) { UClass* ClassList = *AllClassesIt; FName ClassName = ClassList->GetFName(); } for (auto ObjIter = ObjectList.CreateConstIterator(); ObjIter; ++ObjIter) { const FAssetData& Asset = *ObjIter; FString Filename = Asset.GetObjectPathString(); //convert to full paths Filename = FPackageName::LongPackageNameToFilename(Filename); if (FAutomationTestFramework::Get().ShouldTestContent(Filename)) { FString BeautifiedFilename = Asset.AssetName.ToString(); OutBeautifiedNames.Add(BeautifiedFilename); OutTestCommands.Add(Asset.GetObjectPathString()); } } } /** * Generates a list of assets from the GAME by a specific type. * This is to be used by the GetTest() function. */ void FAutomationEditorCommonUtils::CollectGameContentTestsByClass(UClass * Class, bool bRecursiveClass, TArray& OutBeautifiedNames, TArray & OutTestCommands) { //Setting the Asset Registry FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); //Variable setups TArray ObjectList; FARFilter AssetFilter; //Generating the list of assets. //This list is being filtered by the game folder and class type. The results are placed into the ObjectList variable. AssetFilter.ClassPaths.Add(Class->GetClassPathName()); //removed path as a filter as it causes two large lists to be sorted. Filtering on "game" directory on iteration //AssetFilter.PackagePaths.Add("/Game"); AssetFilter.bRecursiveClasses = bRecursiveClass; AssetFilter.bRecursivePaths = true; AssetRegistryModule.Get().GetAssets(AssetFilter, ObjectList); //Loop through the list of assets, make their path full and a string, then add them to the test. for (auto ObjIter = ObjectList.CreateConstIterator(); ObjIter; ++ObjIter) { const FAssetData& Asset = *ObjIter; FString Filename = Asset.GetObjectPathString(); if (Filename.StartsWith("/Game")) { //convert to full paths Filename = FPackageName::LongPackageNameToFilename(Filename); if (FAutomationTestFramework::Get().ShouldTestContent(Filename)) { FString BeautifiedFilename = Asset.AssetName.ToString(); OutBeautifiedNames.Add(BeautifiedFilename); OutTestCommands.Add(Asset.GetObjectPathString()); } } } } void FAutomationEditorCommonUtils::LoadMap(const FString& MapName) { bool bLoadAsTemplate = false; bool bShowProgress = false; FEditorFileUtils::LoadMap(MapName, bLoadAsTemplate, bShowProgress); } void FAutomationEditorCommonUtils::RunPIE(float PIEDuration) { bool bInSimulateInEditor = true; //once in the editor ADD_LATENT_AUTOMATION_COMMAND(FStartPIECommand(true)); ADD_LATENT_AUTOMATION_COMMAND(FWaitLatentCommand(PIEDuration)); ADD_LATENT_AUTOMATION_COMMAND(FEndPlayMapCommand()); //wait between tests ADD_LATENT_AUTOMATION_COMMAND(FWaitLatentCommand(1.0f)); //once not in the editor ADD_LATENT_AUTOMATION_COMMAND(FStartPIECommand(false)); ADD_LATENT_AUTOMATION_COMMAND(FWaitLatentCommand(PIEDuration)); ADD_LATENT_AUTOMATION_COMMAND(FEndPlayMapCommand()); } /** * Generates a list of assets from the GAME by a specific type. * This is to be used by the GetTest() function. */ void FAutomationEditorCommonUtils::CollectGameContentTests(TArray& OutBeautifiedNames, TArray & OutTestCommands) { //Setting the Asset Registry FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); //Variable setups TArray ObjectList; FARFilter AssetFilter; //removed path as a filter as it causes two large lists to be sorted. Filtering on "game" directory on iteration AssetFilter.PackagePaths.Add("/Game"); AssetFilter.bRecursiveClasses = true; AssetFilter.bRecursivePaths = true; AssetRegistryModule.Get().GetAssets(AssetFilter, ObjectList); //Loop through the list of assets, make their path full and a string, then add them to the test. for (auto ObjIter = ObjectList.CreateConstIterator(); ObjIter; ++ObjIter) { const FAssetData& Asset = *ObjIter; if (Asset.GetClass() == nullptr) { // a nullptr class is bad ! UE_LOG(LogAutomationEditorCommon, Warning, TEXT("GetClass for %s (%s) returned nullptr. Asset ignored"), *Asset.AssetName.ToString(), *Asset.GetObjectPathString()); } else { FString Filename = Asset.GetObjectPathString(); if (Filename.StartsWith("/Game")) { //convert to full paths Filename = FPackageName::LongPackageNameToFilename(Filename); if (FAutomationTestFramework::Get().ShouldTestContent(Filename)) { FString BeautifiedFilename = FString::Printf(TEXT("%s.%s"), *Asset.AssetClassPath.ToString(), *Asset.AssetName.ToString()); OutBeautifiedNames.Add(BeautifiedFilename); OutTestCommands.Add(Asset.GetObjectPathString()); } } } } } /////////////////////////////////////////////////////////////////////// // Common Latent commands //Latent Undo and Redo command //If bUndo is true then the undo action will occur otherwise a redo will happen. bool FUndoRedoCommand::Update() { if ( bUndo == true ) { //Undo GEditor->UndoTransaction(); } else { //Redo GEditor->RedoTransaction(); } return true; } /** * Open editor for a particular asset */ bool FOpenEditorForAssetCommand::Update() { UObject* Object = StaticLoadObject(UObject::StaticClass(), NULL, *AssetName); if ( Object ) { // Some assets (like UWorlds) may be destroyed and recreated as part of opening. To protect against this, keep the path to the asset and try to re-find it if it disappeared. TWeakObjectPtr WeakObject = Object; GEditor->GetEditorSubsystem()->OpenEditorForAsset(Object); // If the object was destroyed, attempt to find it if it was recreated if (!WeakObject.IsValid() && !AssetName.IsEmpty()) { Object = FindObject(nullptr, *AssetName); } //This checks to see if the asset sub editor is loaded. if ( GEditor->GetEditorSubsystem()->FindEditorForAsset(Object, true) != NULL ) { UE_LOG(LogEditorAutomationTests, Log, TEXT("Verified asset editor for: %s."), *AssetName); UE_LOG(LogEditorAutomationTests, Display, TEXT("The editor successfully loaded for: %s."), *AssetName); return true; } } else { UE_LOG(LogEditorAutomationTests, Error, TEXT("Failed to find object: %s."), *AssetName); } return true; } /** * Close all sub-editors */ bool FCloseAllAssetEditorsCommand::Update() { GEditor->GetEditorSubsystem()->CloseAllAssetEditors(); //Get all assets currently being tracked with open editors and make sure they are not still opened. if ( GEditor->GetEditorSubsystem()->GetAllEditedAssets().Num() >= 1 ) { UE_LOG(LogEditorAutomationTests, Warning, TEXT("Not all of the editors were closed.")); return true; } UE_LOG(LogEditorAutomationTests, Log, TEXT("Verified asset editors were closed")); UE_LOG(LogEditorAutomationTests, Display, TEXT("The asset editors closed successfully")); return true; } /** * Start PIE session */ bool FStartPIECommand::Update() { FLevelEditorModule& LevelEditorModule = FModuleManager::Get().GetModuleChecked(TEXT("LevelEditor")); FRequestPlaySessionParams Params; Params.DestinationSlateViewport = LevelEditorModule.GetFirstActiveViewport(); if (bSimulateInEditor) { Params.WorldType = EPlaySessionWorldType::SimulateInEditor; } // Make sure the player start location is a valid location. if (GUnrealEd->CheckForPlayerStart() == nullptr) { FAutomationEditorCommonUtils::SetPlaySessionStartToActiveViewport(Params); } GUnrealEd->RequestPlaySession(Params); return true; } /** * End PlayMap session */ bool FEndPlayMapCommand::Update() { GUnrealEd->RequestEndPlayMap(); return true; } /** * This this command loads a map into the editor. */ bool FEditorLoadMap::Update() { //Get the base filename for the map that will be used. FString ShortMapName = FPaths::GetBaseFilename(MapName); //Get the current number of seconds before loading the map. double MapLoadStartTime = FPlatformTime::Seconds(); //Load the map FAutomationEditorCommonUtils::LoadMap(MapName); //This is the time it took to load the map in the editor. double MapLoadTime = FPlatformTime::Seconds() - MapLoadStartTime; //Gets the main frame module to get the name of our current level. const IMainFrameModule& MainFrameModule = FModuleManager::GetModuleChecked< IMainFrameModule >("MainFrame"); FString LoadedMapName = MainFrameModule.GetLoadedLevelName(); UE_LOG(LogEditorAutomationTests, Log, TEXT("%s has been loaded."), *ShortMapName); //Log out to a text file the time it takes to load the map. FAutomationEditorCommonUtils::WriteToTextFile(TEXT("Performance"), LoadedMapName, TEXT("RAWMapLoadTime.txt"), MapLoadTime, TEXT(",")); UE_LOG(LogEditorAutomationTests, Display, TEXT("%s took %.3f to load."), *LoadedMapName, MapLoadTime); return true; } /** * This will cause the test to wait for the shaders to finish compiling before moving on. */ bool FWaitForShadersToFinishCompiling::Update() { static double TimeShadersFinishedCompiling = 0; static double LastReportTime = FPlatformTime::Seconds(); const double TimeToWaitForJobs = 2.0; bool ShadersCompiling = GShaderCompilingManager && GShaderCompilingManager->IsCompiling(); bool TexturesCompiling = FTextureCompilingManager::Get().GetNumRemainingTextures() > 0; double TimeNow = FPlatformTime::Seconds(); if (ShadersCompiling || TexturesCompiling) { if (TimeNow - LastReportTime > 5.0) { LastReportTime = TimeNow; if (ShadersCompiling) { UE_LOG(LogEditorAutomationTests, Log, TEXT("Waiting for %i shaders to finish."), GShaderCompilingManager->GetNumRemainingJobs() + GShaderCompilingManager->GetNumPendingJobs()); } if (TexturesCompiling) { UE_LOG(LogEditorAutomationTests, Log, TEXT("Waiting for %i texures to finish."), FTextureCompilingManager::Get().GetNumRemainingTextures()); } } TimeShadersFinishedCompiling = 0; return false; } // Current jobs are done, but things may still come in on subsequent frames.. if (TimeShadersFinishedCompiling == 0) { TimeShadersFinishedCompiling = FPlatformTime::Seconds(); } if (FPlatformTime::Seconds() - TimeShadersFinishedCompiling < TimeToWaitForJobs) { return false; } // may not be necessary, but just double-check everything is finished and ready GShaderCompilingManager->FinishAllCompilation(); UE_LOG(LogEditorAutomationTests, Log, TEXT("Done waiting for shaders to finish.")); return true; } /** * Latent command that changes the editor viewport to the first available bookmarked view. */ bool FChangeViewportToFirstAvailableBookmarkCommand::Update() { uint32 ViewportIndex = 0; UE_LOG(LogEditorAutomationTests, Log, TEXT("Attempting to change the editor viewports view to the first set bookmark.")); //Move the perspective viewport view to show the test. for (FLevelEditorViewportClient* ViewportClient : GEditor->GetLevelViewportClients()) { const uint32 NumberOfBookmarks = IBookmarkTypeTools::Get().GetMaxNumberOfBookmarks(ViewportClient); for ( ViewportIndex = 0; ViewportIndex <= NumberOfBookmarks; ViewportIndex++ ) { if (IBookmarkTypeTools::Get().CheckBookmark(ViewportIndex, ViewportClient) ) { UE_LOG(LogEditorAutomationTests, VeryVerbose, TEXT("Changing a viewport view to the set bookmark %i"), ViewportIndex); IBookmarkTypeTools::Get().JumpToBookmark(ViewportIndex, TSharedPtr(), ViewportClient); break; } } } return true; } /** * Latent command that adds a static mesh to the worlds origin. */ bool FAddStaticMeshCommand::Update() { //Gather assets. UObject* Cube = (UStaticMesh*)StaticLoadObject(UStaticMesh::StaticClass(), NULL, TEXT("/Engine/EngineMeshes/Cube.Cube"), NULL, LOAD_None, NULL); //Add Cube mesh to the world AStaticMeshActor* StaticMesh = Cast(FActorFactoryAssetProxy::AddActorForAsset(Cube)); StaticMesh->TeleportTo(FVector(0.0f, 0.0f, 0.0f), FRotator(0, 0, 0)); StaticMesh->SetActorRelativeScale3D(FVector(1.0f, 1.0f, 1.0f)); UE_LOG(LogEditorAutomationTests, Log, TEXT("Static Mesh cube has been added to 0, 0, 0.")) return true; } /** * Latent command that builds lighting for the current level. */ bool FBuildLightingCommand::Update() { //If we are running with -NullRHI then we have to skip this step. if ( GUsingNullRHI ) { UE_LOG(LogEditorAutomationTests, Log, TEXT("SKIPPED Build Lighting Step. You're currently running with -NullRHI.")); return true; } if ( GUnrealEd->WarnIfLightingBuildIsCurrentlyRunning() ) { UE_LOG(LogEditorAutomationTests, Warning, TEXT("Lighting is already being built.")); return true; } UWorld* CurrentWorld = GEditor->GetEditorWorldContext().World(); GUnrealEd->Exec(CurrentWorld, TEXT("MAP REBUILD")); FLightingBuildOptions LightingBuildOptions; // Retrieve settings from ini. GConfig->GetBool(TEXT("LightingBuildOptions"), TEXT("OnlyBuildSelected"), LightingBuildOptions.bOnlyBuildSelected, GEditorPerProjectIni); GConfig->GetBool(TEXT("LightingBuildOptions"), TEXT("OnlyBuildCurrentLevel"), LightingBuildOptions.bOnlyBuildCurrentLevel, GEditorPerProjectIni); GConfig->GetBool(TEXT("LightingBuildOptions"), TEXT("OnlyBuildSelectedLevels"), LightingBuildOptions.bOnlyBuildSelectedLevels, GEditorPerProjectIni); GConfig->GetBool(TEXT("LightingBuildOptions"), TEXT("OnlyBuildVisibility"), LightingBuildOptions.bOnlyBuildVisibility, GEditorPerProjectIni); GConfig->GetBool(TEXT("LightingBuildOptions"), TEXT("UseErrorColoring"), LightingBuildOptions.bUseErrorColoring, GEditorPerProjectIni); GConfig->GetBool(TEXT("LightingBuildOptions"), TEXT("ShowLightingBuildInfo"), LightingBuildOptions.bShowLightingBuildInfo, GEditorPerProjectIni); int32 QualityLevel; GConfig->GetInt(TEXT("LightingBuildOptions"), TEXT("QualityLevel"), QualityLevel, GEditorPerProjectIni); QualityLevel = FMath::Clamp(QualityLevel, Quality_Preview, Quality_Production); LightingBuildOptions.QualityLevel = Quality_Production; UE_LOG(LogEditorAutomationTests, Log, TEXT("Building lighting in Production Quality.")); GUnrealEd->BuildLighting(LightingBuildOptions); return true; } bool FSaveLevelCommand::Update() { if ( !GUnrealEd->IsLightingBuildCurrentlyExporting() && !GUnrealEd->IsLightingBuildCurrentlyRunning() ) { UWorld* World = GEditor->GetEditorWorldContext().World(); ULevel* Level = World->GetCurrentLevel(); MapName += TEXT("_Copy.umap"); FString TempMapLocation = FPaths::Combine(*FPaths::ProjectContentDir(), TEXT("Maps"), TEXT("Automation_TEMP"), *MapName); FEditorFileUtils::SaveLevel(Level, TempMapLocation); return true; } return false; } bool FLaunchOnCommand::Update() { FRequestPlaySessionParams::FLauncherDeviceInfo LaunchedDeviceInfo; LaunchedDeviceInfo.DeviceId = InLauncherDeviceID; LaunchedDeviceInfo.DeviceName = InLauncherDeviceID.Right(InLauncherDeviceID.Find(TEXT("@"))); FRequestPlaySessionParams Params; Params.LauncherTargetDevice = LaunchedDeviceInfo; GUnrealEd->RequestPlaySession(Params); // Immediately start our requested play session GUnrealEd->StartQueuedPlaySessionRequest(); return true; } bool FWaitToFinishCookByTheBookCommand::Update() { if ( !GUnrealEd->CookServer->IsCookByTheBookRunning() ) { if ( GUnrealEd->IsCookByTheBookInEditorFinished() ) { UE_LOG(LogEditorAutomationTests, Log, TEXT("The cook by the book operation has finished.")); } return true; } else if ( ( FPlatformTime::Seconds() - StartTime ) == COOK_TIMEOUT ) { GUnrealEd->CancelCookByTheBookInEditor(); UE_LOG(LogEditorAutomationTests, Error, TEXT("It has been an hour or more since the cook has started.")); return false; } return false; } bool FDeleteDirCommand::Update() { FString FullFolderPath = FPaths::ConvertRelativePathToFull(*InFolderLocation); if (IFileManager::Get().DirectoryExists(*FullFolderPath)) { IFileManager::Get().DeleteDirectory(*FullFolderPath, false, true); } return true; } bool FWaitToFinishBuildDeployCommand::Update() { if (GEditor->LauncherWorker->GetStatus() == ELauncherWorkerStatus::Completed) { UE_LOG(LogEditorAutomationTests, Log, TEXT("The build game and deploy operation has finished.")); return true; } else if (GEditor->LauncherWorker->GetStatus() == ELauncherWorkerStatus::Canceled || GEditor->LauncherWorker->GetStatus() == ELauncherWorkerStatus::Canceling) { UE_LOG(LogEditorAutomationTests, Warning, TEXT("The build was canceled.")); return true; } return false; } // agrant-todo: Expose the version in AutomationCommon.cpp for 4.27 namespace EditorAutomationPrivate { // @todo this is a temporary solution. Once we know how to get test's hands on a proper world // this function should be redone/removed UWorld* GetAnyGameWorld() { UWorld* TestWorld = nullptr; const TIndirectArray& WorldContexts = GEngine->GetWorldContexts(); for (const FWorldContext& Context : WorldContexts) { if (((Context.WorldType == EWorldType::PIE) || (Context.WorldType == EWorldType::Game)) && (Context.World() != NULL)) { TestWorld = Context.World(); break; } } return TestWorld; } } // agrant-todo: Use the standard version in AutomationCommon.cpp for 4.27 DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FWaitForSpecifiedPIEMapToEndCommand, FString, MapName); bool FWaitForSpecifiedPIEMapToEndCommand::Update() { UWorld* TestWorld = EditorAutomationPrivate::GetAnyGameWorld(); if (!TestWorld) { return true; } // remove any paths or extensions to match the name of the world FString ShortMapName = FPackageName::GetShortName(MapName); ShortMapName = FPaths::GetBaseFilename(ShortMapName); // Handle both ways the user may have specified this if (TestWorld->GetName() != ShortMapName) { return true; } return false; } // agrant-todo: Move this into BasicTests.cpp for 4.27 /** * Generic Pie Test for projects. * By default this test will PIE the lit of MapsToPIETest from automation settings. if that is empty it will PIE the default editor and game (if they're different) * maps. * * If the editor session was started with a map on the command line then that's the only map that will be PIE'd. This allows project to set up tests that PIE * a list of maps from an external source. */ IMPLEMENT_SIMPLE_AUTOMATION_TEST(FProjectMapsPIETest, "Project.Maps.PIE", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) /** * Execute the loading of one map to verify PIE works * * @param Parameters - Unused for this test * @return TRUE if the test was successful, FALSE otherwise */ bool FProjectMapsPIETest::RunTest(const FString& Parameters) { UAutomationTestSettings const* AutomationTestSettings = GetDefault(); check(AutomationTestSettings); TArray PIEMaps; // If the user has specified a map on the command line then that is what we'll PIE const TCHAR* ParsedCmdLine = FCommandLine::Get(); FString ParsedMapName; bool FirstMapAlreadyLoaded = false; // If there is an explicit list of maps on the command line via -map or -maps the use those. if (FParse::Value(FCommandLine::Get(), TEXT("-maps="), ParsedMapName) || FParse::Value(FCommandLine::Get(), TEXT("-map="), ParsedMapName)) { ParsedMapName.ParseIntoArray(PIEMaps, TEXT("+"), true); UE_LOG(LogEditorAutomationTests, Display, TEXT("Found Maps %s on command line. PIE Test will use these maps"), *ParsedMapName); } else if (FParse::Token(ParsedCmdLine, ParsedMapName, false) && ParsedMapName.StartsWith(TEXT("-")) == false) { // If the user specified a map as the first param after the project, we'll PIE that FString InitialMapName; // If the specified package exists if (FPackageName::SearchForPackageOnDisk(ParsedMapName, NULL, &InitialMapName) && // and it's a valid map file FPaths::GetExtension(InitialMapName, /*bIncludeDot=*/true) == FPackageName::GetMapPackageExtension()) { PIEMaps.Add(InitialMapName); FirstMapAlreadyLoaded = true; UE_LOG(LogEditorAutomationTests, Display, TEXT("Found Map %s on command line. PIE Test will be restricted to this map"), *InitialMapName); } } // Ok, at this point there were no command line maps so default to the project settings. We PIE the editor startup map and the game startup map if (PIEMaps.Num() == 0) { // If the project has maps configured for PIE then use those if (AutomationTestSettings->MapsToPIETest.Num()) { for (const FString& Map : AutomationTestSettings->MapsToPIETest) { PIEMaps.Add(Map); } } else { // Else pick the editor startup and game startup maps (if they are different). UE_LOG(LogEditorAutomationTests, Display, TEXT("No MapsToPIE or MapsToTest specified in DefaultEngine.ini [/Script/Engine.AutomationTestSettings]. Using GameStartup or EditorStartup Map")); UGameMapsSettings const* MapSettings = GetDefault(); if (MapSettings->EditorStartupMap.IsValid()) { PIEMaps.Add(MapSettings->EditorStartupMap.GetLongPackageName()); } if (MapSettings->GetGameDefaultMap().Len() && MapSettings->GetGameDefaultMap() != MapSettings->EditorStartupMap.GetLongPackageName()) { PIEMaps.Add(MapSettings->GetGameDefaultMap()); } } } // Uh-oh if (PIEMaps.Num() == 0) { UE_LOG(LogEditorAutomationTests, Fatal, TEXT("No automation or default maps are configured for PIE!")); } // Don't want these settings affecting metrics UEditorPerformanceSettings* Settings = GetMutableDefault(); Settings->bThrottleCPUWhenNotForeground = false; Settings->bMonitorEditorPerformance = false; Settings->PostEditChange(); for (const FString& Map : PIEMaps) { // Accept any of... // - MyMap // - /Game/MyMap // - /Game/MyMap.MyMap FString MapPackageName = Map; if (FPackageName::IsValidObjectPath(Map)) { MapPackageName = FPackageName::ObjectPathToPackageName(Map); } if (!FPackageName::SearchForPackageOnDisk(Map, NULL, &MapPackageName)) { UE_LOG(LogEditorAutomationTests, Error, TEXT("Couldn't resolve map for PIE test from %s to valid package name!"), *MapPackageName); continue; } UE_LOG(LogEditorAutomationTests, Display, TEXT("Queueing Map %s for PIE Automation"), *MapPackageName); AddCommand(new FEditorAutomationLogCommand(FString::Printf(TEXT("LoadMap-Begin: %s"), *MapPackageName))); if (!FirstMapAlreadyLoaded) { AddCommand(new FEditorLoadMap(MapPackageName)); } AddCommand(new FWaitLatentCommand(1.0f)); AddCommand(new FEditorAutomationLogCommand(FString::Printf(TEXT("LoadMap-End: %s"), *MapPackageName))); AddCommand(new FEditorAutomationLogCommand(FString::Printf(TEXT("PIE-Begin: %s"), *MapPackageName))); AddCommand(new FStartPIECommand(false)); AddCommand(new FWaitForSpecifiedMapToLoadCommand(MapPackageName)); // need at least some frames before starting & ending PIE AddCommand(new FWaitForInteractiveFrameRate()); // wait until the editor reaches something vaguely usable AddCommand(new FWaitLatentCommand(AutomationTestSettings->PIETestDuration)); AddCommand(new FEndPlayMapCommand()); AddCommand(new FWaitForSpecifiedPIEMapToEndCommand(MapPackageName)); // need at least some frames before starting & ending PIE AddCommand(new FEditorAutomationLogCommand(FString::Printf(TEXT("PIE-End: %s"), *MapPackageName))); FirstMapAlreadyLoaded = false; } return true; } #endif