Files
UnrealEngine/Engine/Source/Editor/UnrealEd/Private/Tests/AutomationEditorCommon.cpp
2025-05-18 13:04:45 +08:00

1321 lines
46 KiB
C++

// 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<FScopedEditorWorld> FAutomationEditorCommonUtils::CreateScopedEditorWorld(
const FString& TemplateNameOrPath,
const UWorld::InitializationValues& InInitializationValues,
EWorldType::Type InWorldType)
{
// Get the list of template maps
TArray<FTemplateMapInfo> 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<FScopedEditorWorld>(TSoftObjectPtr<UWorld>(FSoftObjectPath(TemplatePath)), InInitializationValues, InWorldType);
}
// Create a new FScopedEditorWorld using the path to the level.
TUniquePtr<FScopedEditorWorld> ScopedWorld = MakeUnique<FScopedEditorWorld>(TSoftObjectPtr<UWorld>(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<UObject*> ReplaceableObjects;
TMap<UObject*, UObject*> 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<FProperty*> > 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<UObject*, int32> CurNumReferencesMap;
TMultiMap<UObject*, FProperty*> CurReferencingPropertiesMMap;
if (FindRefsArchive.GetReferenceCounts(CurNumReferencesMap, CurReferencingPropertiesMMap) > 0)
{
TArray<FProperty*> CurReferencedProperties;
CurReferencingPropertiesMMap.GenerateValueArray(CurReferencedProperties);
ReferencingPropertiesMap.Add(CurObject, CurReferencedProperties);
for (TArray<FProperty*>::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<FProperty*> >::TConstIterator MapIter(ReferencingPropertiesMap); MapIter; ++MapIter)
{
++NumObjsReplaced;
UObject* CurReplaceObj = MapIter.Key();
const TArray<FProperty*>& RefPropArray = MapIter.Value();
FArchiveReplaceObjectRef<UObject> ReplaceAr(CurReplaceObj, ReplacementMap, EArchiveReplaceObjectFlags::IgnoreOuterRef);
for (TArray<FProperty*>::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<UClass> ClassIt; ClassIt; ++ClassIt)
{
if ((*ClassIt)->IsChildOf(UFactory::StaticClass()) && !((*ClassIt)->HasAnyClassFlags(CLASS_Abstract)))
{
UFactory* Factory = Cast<UFactory>((*ClassIt)->GetDefaultObject());
if (Factory->bEditorImport)
{
TArray<FString> 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<FString>& PropertyChain, const FString& Value)
{
const FString PropertyName = PropertyChain[0];
PropertyChain.RemoveAt(0);
FProperty* TargetProperty = FindFProperty<FProperty>(InObject->GetClass(), *PropertyName);
if (TargetProperty)
{
if (PropertyChain.Num() == 0)
{
TargetProperty->ImportText_InContainer(*Value, InObject, InObject, 0);
}
else
{
FStructProperty* StructProperty = CastField<FStructProperty>(TargetProperty);
FObjectProperty* ObjectProperty = CastField<FObjectProperty>(TargetProperty);
UObject* SubObject = NULL;
bool bValidPropertyType = true;
if (StructProperty)
{
SubObject = StructProperty->Struct;
}
else if (ObjectProperty)
{
SubObject = ObjectProperty->GetObjectPropertyValue(ObjectProperty->ContainerPtrToValuePtr<UObject>(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<FImportFactorySettingValues>& 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<FString> 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<float>& 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<float>& 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<FString>& 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<UAutomationTestSettings>();
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<ITargetDeviceServicesModule*>(FModuleManager::Get().LoadModule(TEXT("TargetDeviceServices")));
// for each platform...
TArray<TSharedPtr<ITargetDeviceProxy>> DeviceProxies;
TargetDeviceServicesModule->GetDeviceProxyManager()->GetProxies(FName(*LaunchOnSettings), true, DeviceProxies);
// for each proxy...
for (auto DeviceProxyIt = DeviceProxies.CreateIterator(); DeviceProxyIt; ++DeviceProxyIt)
{
TSharedPtr<ITargetDeviceProxy> 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<UAutomationTestSettings>();
check(AutomationTestSettings);
//Output device name will default to "None".
OutDeviceID = "None";
// shared devices section
ITargetDeviceServicesModule* TargetDeviceServicesModule = static_cast<ITargetDeviceServicesModule*>(FModuleManager::Get().LoadModule(TEXT("TargetDeviceServices")));
// for each platform...
TArray<TSharedPtr<ITargetDeviceProxy>> DeviceProxies;
TargetDeviceServicesModule->GetDeviceProxyManager()->GetProxies(FName(*InDeviceName), true, DeviceProxies);
// for each proxy...
for (auto DeviceProxyIt = DeviceProxies.CreateIterator(); DeviceProxyIt; ++DeviceProxyIt)
{
TSharedPtr<ITargetDeviceProxy> 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<FLevelEditorModule>(TEXT("LevelEditor"));
TSharedPtr<IAssetViewport> 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<FAssetRegistryModule>(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<FString>& OutBeautifiedNames, TArray <FString>& OutTestCommands)
{
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
TArray<FAssetData> ObjectList;
AssetRegistryModule.Get().GetAssetsByClass(Class->GetClassPathName(), ObjectList);
for (TObjectIterator<UClass> 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<FString>& OutBeautifiedNames, TArray <FString>& OutTestCommands)
{
//Setting the Asset Registry
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
//Variable setups
TArray<FAssetData> 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<FString>& OutBeautifiedNames, TArray <FString>& OutTestCommands)
{
//Setting the Asset Registry
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
//Variable setups
TArray<FAssetData> 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<UObject> WeakObject = Object;
GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->OpenEditorForAsset(Object);
// If the object was destroyed, attempt to find it if it was recreated
if (!WeakObject.IsValid() && !AssetName.IsEmpty())
{
Object = FindObject<UObject>(nullptr, *AssetName);
}
//This checks to see if the asset sub editor is loaded.
if ( GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->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<UAssetEditorSubsystem>()->CloseAllAssetEditors();
//Get all assets currently being tracked with open editors and make sure they are not still opened.
if ( GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->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<FLevelEditorModule>(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<struct FBookmarkBaseJumpToSettings>(), 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<AStaticMeshActor>(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<int32>(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<FWorldContext>& 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<UAutomationTestSettings>();
check(AutomationTestSettings);
TArray<FString> 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<UGameMapsSettings>();
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<UEditorPerformanceSettings>();
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