Files
UnrealEngineUWP/Engine/Source/Developer/FunctionalTesting/Private/FunctionalTest.cpp
Thomas Sarkanen fbd810241c Copying //UE4/Dev-AnimPhys to //UE4/Dev-Main (Source: //UE4/Dev-AnimPhys @ 3555219)
#lockdown Nick.Penwarden
#rb none

============================
  MAJOR FEATURES & CHANGES
============================

Change 3537750 by Ori.Cohen

	Added RootBone simulation space to RigidBody node. This is useful for cases where we rotate the skeletal mesh component and counter rotate the root bone and do not want to affect simulated bodies' velocities.

Change 3537756 by Ori.Cohen

	Added ExternalForce to rigid body node for faking inertia while simulating in component space

Change 3537758 by Ori.Cohen

	Made it so that linear and angular velocity are properly computed for kinematic targets in immediate physics and rigid body node.

Change 3538308 by Ethan.Geller

	#jira UE-47169: fix for streaming sources not getting properly destroyed in AudioMixer

Change 3538329 by Jon.Nabozny

	Fix ForEachBodyBelow to use the proper body index.

Change 3538528 by Ori.Cohen

	Fix gravity not being converted into the right simulation space for the RigidBody node

Change 3539741 by James.Golding

	Remove unused HACD library. We use V-HACD now.
	#jira UE-46618

Change 3539746 by James.Golding

	Enable research SoftBody plugin in QAGame

Change 3540115 by Ori.Cohen

	Turn joint pre-processing on for immediate mode. This helps with some stability issues.

	#tests none

Change 3543166 by Ori.Cohen

	Exposed an inertia scale for body instances

Change 3544097 by Ori.Cohen

	Fix incorrect bone mapping for rigid body node. (Only matters when first call to init has a different number of bodies, for example a different skin)

Change 3544221 by Ori.Cohen

	Fix CIS

Change 3544289 by Ori.Cohen

	Fix CIS

Change 3544581 by Ori.Cohen

	Fix CIS

Change 3545415 by Ethan.Geller

	Changes to AudioMixer's Delay class: Addition of SetDelayInSamples method, fix for delay line not working when being set to maximum delay

Change 3545426 by Ethan.Geller

	#jira UEAP-282 Add Flexiverb submix effect to Synthesis plugin.

Change 3545570 by Ethan.Geller

	Add Blueprint function library to sound utilities plugin

Change 3548160 by Martin.Wilson

	Clean up animation compression logging (change to compression category and downgraded from warning

	#Jira UE-47424

Change 3548368 by Danny.Bouimad

	Changing an audio test on TM-AnimPhys as it isn't supported on a large number of platforms (Yet)

Change 3548630 by Aaron.McLeran

	Don't send or mix built-in reverb submix if there's a plugin reverb enabled.

Change 3548631 by Aaron.McLeran

	Adding ability to define default device using macro vs assuming index 0.

Change 3548782 by Aaron.McLeran

	SDL backend implementation for windows, linux, and HTML.

	- Still need to hook up SDL to HTML5, but should be workable in Linux.

Change 3549034 by Aaron.McLeran

	Adding missed file for SDL2.lib

Change 3549102 by Aaron.McLeran

	Adding missing include

Change 3550388 by James.Golding

	Fix ApexDestructionLib project generation

	#jira UE-47637

Change 3550412 by James.Golding

	More fixes to ApexDestruction.Build.cs

Change 3550856 by Ori.Cohen

	Fix analysis for shared headers.

	#jira UE-47593

Change 3551046 by Aaron.McLeran

	Fix CIS build

Change 3551056 by Aaron.McLeran

	Fixing assert on launch of audio mixer, invalid assert.

Change 3552685 by Thomas.Sarkanen

	Fixed Fortnite warnings on editor startup/cook

	APEX destruction plugin was accessing the thumbnail manager before UnrealEd (and more pertinently, FortniteGame module) was loaded. Defering registration of the thumbnail rendering until later in the startup sequence means the the appropriate classes can be found.

	#jira UE-47595 - //UE4/Dev-AnimPhys: Cook Fortnite Win64 completed with 16 Warnings

Change 3552847 by Ori.Cohen

	Fix CIS

Change 3552916 by James.Golding

	Fix Win64 SDL2 deployment (path was wrong)

	#jira UE-47679

Change 3552919 by James.Golding

	Add 'RemapDirectories' entry for SoftBody plugin in QAGame (avoids package error)
	Also fix warning to give correct syntax

	#jira UE-47682

Change 3553168 by Ori.Cohen

	Fix CIS

[CL 3555263 by Thomas Sarkanen in Main branch]
2017-07-26 09:23:14 -04:00

1297 lines
38 KiB
C++

// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
#include "FunctionalTest.h"
#include "Misc/Paths.h"
#include "Engine/GameViewportClient.h"
#include "Engine/LatentActionManager.h"
#include "Components/BillboardComponent.h"
#include "HAL/FileManager.h"
#include "Misc/FileHelper.h"
#include "UObject/ConstructorHelpers.h"
#include "ProfilingDebugging/ProfilingHelpers.h"
#include "Misc/AutomationTest.h"
#include "GameFramework/PlayerController.h"
#include "Components/TextRenderComponent.h"
#include "Engine/Selection.h"
#include "FuncTestManager.h"
#include "FuncTestRenderingComponent.h"
#include "ObjectEditorUtils.h"
#include "VisualLogger/VisualLogger.h"
#include "EngineGlobals.h"
#include "Engine/Engine.h"
#include "Engine/Texture2D.h"
#include "DelayForFramesLatentAction.h"
#include "Engine/DebugCameraController.h"
#include "TraceQueryTestResults.h"
#include "Misc/RuntimeErrors.h"
namespace
{
template <typename T>
bool PerformComparison(const T& lhs, const T& rhs, EComparisonMethod comparison)
{
switch (comparison)
{
case EComparisonMethod::Equal_To:
return lhs == rhs;
case EComparisonMethod::Not_Equal_To:
return lhs != rhs;
case EComparisonMethod::Greater_Than_Or_Equal_To:
return lhs >= rhs;
case EComparisonMethod::Less_Than_Or_Equal_To:
return lhs <= rhs;
case EComparisonMethod::Greater_Than:
return lhs > rhs;
case EComparisonMethod::Less_Than:
return lhs < rhs;
}
UE_LOG(LogFunctionalTest, Error, TEXT("Invalid comparison method"));
return false;
}
FString GetComparisonAsString(EComparisonMethod comparison)
{
UEnum* Enum = FindObject<UEnum>(ANY_PACKAGE, TEXT("EComparisonMethod"), true);
return Enum->GetNameStringByValue((uint8)comparison).ToLower().Replace(TEXT("_"), TEXT(" "));
}
FString TransformToString(const FTransform &transform)
{
const FRotator R(transform.Rotator());
FVector T(transform.GetTranslation());
FVector S(transform.GetScale3D());
return FString::Printf(TEXT("Translation: %f, %f, %f | Rotation: %f, %f, %f | Scale: %f, %f, %f"), T.X, T.Y, T.Z, R.Pitch, R.Yaw, R.Roll, S.X, S.Y, S.Z);
}
void DelayForFramesCommon(UObject* WorldContextObject, FLatentActionInfo LatentInfo, int32 NumFrames)
{
if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))
{
FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
if (LatentActionManager.FindExistingAction<FDelayForFramesLatentAction>(LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr)
{
LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, new FDelayForFramesLatentAction(LatentInfo, NumFrames));
}
}
}
}
AFunctionalTest::AFunctionalTest( const FObjectInitializer& ObjectInitializer )
: Super(ObjectInitializer)
, bIsEnabled(true)
, bWarningsAsErrors(false)
, Result(EFunctionalTestResult::Invalid)
, PreparationTimeLimit(15.0f)
, TimeLimit(60.0f)
, TimesUpMessage( NSLOCTEXT("FunctionalTest", "DefaultTimesUpMessage", "Time's up!") )
, TimesUpResult(EFunctionalTestResult::Failed)
, bIsRunning(false)
, TotalTime(0.f)
, RunFrame(0)
, RunTime(0.0f)
, StartFrame(0)
, StartTime(0.0f)
, bIsReady(false)
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = false;
PrimaryActorTick.bTickEvenWhenPaused = true;
bCanBeDamaged = false;
SpriteComponent = CreateDefaultSubobject<UBillboardComponent>(TEXT("Sprite"));
if (SpriteComponent)
{
SpriteComponent->bHiddenInGame = true;
#if WITH_EDITORONLY_DATA
if (!IsRunningCommandlet())
{
struct FConstructorStatics
{
ConstructorHelpers::FObjectFinderOptional<UTexture2D> Texture;
FName ID_FTests;
FText NAME_FTests;
FConstructorStatics()
: Texture(TEXT("/Engine/EditorResources/S_FTest"))
, ID_FTests(TEXT("FTests"))
, NAME_FTests(NSLOCTEXT( "SpriteCategory", "FTests", "FTests" ))
{
}
};
static FConstructorStatics ConstructorStatics;
SpriteComponent->Sprite = ConstructorStatics.Texture.Get();
SpriteComponent->SpriteInfo.Category = ConstructorStatics.ID_FTests;
SpriteComponent->SpriteInfo.DisplayName = ConstructorStatics.NAME_FTests;
}
#endif
RootComponent = SpriteComponent;
}
#if WITH_EDITORONLY_DATA
RenderComp = CreateDefaultSubobject<UFuncTestRenderingComponent>(TEXT("RenderComp"));
RenderComp->PostPhysicsComponentTick.bCanEverTick = false;
RenderComp->SetupAttachment(RootComponent);
#endif // WITH_EDITORONLY_DATA
#if WITH_EDITOR
static bool bSelectionHandlerSetUp = false;
if (HasAnyFlags(RF_ClassDefaultObject) && !HasAnyFlags(RF_TagGarbageTemp) && bSelectionHandlerSetUp == false)
{
USelection::SelectObjectEvent.AddStatic(&AFunctionalTest::OnSelectObject);
bSelectionHandlerSetUp = true;
}
#endif // WITH_EDITOR
#if WITH_EDITORONLY_DATA
TestName = CreateEditorOnlyDefaultSubobject<UTextRenderComponent>(TEXT("TestName"));
if ( TestName )
{
TestName->bHiddenInGame = true;
TestName->SetHorizontalAlignment(EHTA_Center);
TestName->SetRelativeLocation(FVector(0, 0, 80));
TestName->SetRelativeRotation(FRotator(0, 0, 0));
TestName->PostPhysicsComponentTick.bCanEverTick = false;
TestName->SetupAttachment(RootComponent);
}
#endif
}
void AFunctionalTest::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
#if WITH_EDITOR
if ( TestName )
{
if ( bIsEnabled )
{
TestName->SetTextRenderColor(FColor(11, 255, 0));
TestName->SetText(FText::FromString(GetActorLabel()));
}
else
{
TestName->SetTextRenderColor(FColor(55, 55, 55));
TestName->SetText(FText::FromString(GetActorLabel() + TEXT("\n") + TEXT("# Disabled #")));
}
}
#endif
}
bool AFunctionalTest::RunTest(const TArray<FString>& Params)
{
FAutomationTestFramework::Get().SetTreatWarningsAsErrors(bWarningsAsErrors);
//Scalability::FQualityLevels Quality;
//Quality.SetDefaults();
//Scalability::SetQualityLevels(Quality);
FailureMessage = TEXT("");
//Do not collect garbage during the test. We force GC at the end.
GEngine->DelayGarbageCollection();
RunFrame = GFrameNumber;
RunTime = GetWorld()->GetTimeSeconds();
TotalTime = 0.f;
if (TimeLimit >= 0)
{
SetActorTickEnabled(true);
}
bIsReady = false;
bIsRunning = true;
GoToObservationPoint();
PrepareTest();
OnTestPrepare.Broadcast();
return true;
}
void AFunctionalTest::PrepareTest()
{
ReceivePrepareTest();
}
void AFunctionalTest::StartTest()
{
TotalTime = 0.f;
StartFrame = GFrameNumber;
StartTime = GetWorld()->GetTimeSeconds();
ReceiveStartTest();
OnTestStart.Broadcast();
}
void AFunctionalTest::OnTimeout()
{
FinishTest(TimesUpResult, TimesUpMessage.ToString());
}
void AFunctionalTest::Tick(float DeltaSeconds)
{
// already requested not to tick.
if ( bIsRunning == false )
{
return;
}
//Do not collect garbage during the test. We force GC at the end.
GEngine->DelayGarbageCollection();
if ( !bIsReady )
{
bIsReady = IsReady();
// Once we're finally ready to begin the test, then execute the Start event.
if ( bIsReady )
{
StartTest();
}
}
if ( bIsReady )
{
TotalTime += DeltaSeconds;
if ( TimeLimit > 0.f && TotalTime > TimeLimit )
{
OnTimeout();
}
else
{
Super::Tick(DeltaSeconds);
}
}
else
{
TotalTime += DeltaSeconds;
if ( PreparationTimeLimit > 0.f && TotalTime > PreparationTimeLimit )
{
OnTimeout();
}
}
}
bool AFunctionalTest::IsReady_Implementation()
{
return true;
}
void AFunctionalTest::FinishTest(EFunctionalTestResult TestResult, const FString& Message)
{
const static UEnum* FTestResultTypeEnum = FindObject<UEnum>( nullptr, TEXT("FunctionalTesting.EFunctionalTestResult") );
if (bIsRunning == false)
{
// ignore
return;
}
//Force GC at the end of every test.
GEngine->ForceGarbageCollection();
Result = TestResult;
bIsRunning = false;
SetActorTickEnabled(false);
OnTestFinished.Broadcast();
AActor** ActorToDestroy = AutoDestroyActors.GetData();
for (int32 ActorIndex = 0; ActorIndex < AutoDestroyActors.Num(); ++ActorIndex, ++ActorToDestroy)
{
if (*ActorToDestroy != NULL)
{
// will be removed next frame
(*ActorToDestroy)->SetLifeSpan( 0.01f );
}
}
const FText ResultText = FTestResultTypeEnum->GetDisplayNameTextByValue( (int64)TestResult );
const FString OutMessage = FString::Printf(TEXT("%s %s: \"%s\"")
, *GetName()
, *ResultText.ToString()
, Message.IsEmpty() == false ? *Message : TEXT("Test finished") );
AutoDestroyActors.Reset();
switch (TestResult)
{
case EFunctionalTestResult::Invalid:
case EFunctionalTestResult::Error:
case EFunctionalTestResult::Failed:
UE_VLOG(this, LogFunctionalTest, Error, TEXT("%s"), *OutMessage);
UE_LOG(LogFunctionalTest, Error, TEXT("%s"), *OutMessage);
break;
case EFunctionalTestResult::Running:
UE_VLOG(this, LogFunctionalTest, Warning, TEXT("%s"), *OutMessage);
UE_LOG(LogFunctionalTest, Warning, TEXT("%s"), *OutMessage);
break;
default:
UE_VLOG(this, LogFunctionalTest, Log, TEXT("%s"), *OutMessage);
UE_LOG(LogFunctionalTest, Log, TEXT("%s"), *OutMessage);
break;
}
//if (AdditionalDetails.IsEmpty() == false)
//{
// const FString AdditionalDetails = FString::Printf(TEXT("%s %s, time %.2fs"), *GetAdditionalTestFinishedMessage(TestResult), *OnAdditionalTestFinishedMessageRequest(TestResult), TotalTime);
// UE_LOG(LogFunctionalTest, Log, TEXT("%s"), *AdditionalDetails);
//}
TestFinishedObserver.ExecuteIfBound(this);
FAutomationTestFramework::Get().SetTreatWarningsAsErrors(TOptional<bool>());
}
void AFunctionalTest::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
TestFinishedObserver.Unbind();
Super::EndPlay(EndPlayReason);
}
void AFunctionalTest::CleanUp()
{
FailureMessage = TEXT("");
}
bool AFunctionalTest::IsRunning() const
{
return bIsRunning;
}
bool AFunctionalTest::IsEnabled() const
{
return bIsEnabled;
}
//@todo add "warning" level here
void AFunctionalTest::LogMessage(const FString& Message)
{
UE_LOG(LogFunctionalTest, Log, TEXT("%s"), *Message);
UE_VLOG(this, LogFunctionalTest, Log
, TEXT("%s> %s")
, *GetName(), *Message);
}
void AFunctionalTest::SetTimeLimit(float InTimeLimit, EFunctionalTestResult InResult)
{
if (InTimeLimit < 0.f)
{
UE_VLOG(this, LogFunctionalTest, Warning
, TEXT("%s> Trying to set TimeLimit to less than 0. Falling back to 0 (infinite).")
, *GetName());
InTimeLimit = 0.f;
}
TimeLimit = InTimeLimit;
if (InResult == EFunctionalTestResult::Invalid)
{
UE_VLOG(this, LogFunctionalTest, Warning
, TEXT("%s> Trying to set test Result to \'Invalid\'. Falling back to \'Failed\'")
, *GetName());
InResult = EFunctionalTestResult::Failed;
}
TimesUpResult = InResult;
}
void AFunctionalTest::GatherRelevantActors(TArray<AActor*>& OutActors) const
{
if (ObservationPoint)
{
OutActors.AddUnique(ObservationPoint);
}
for (auto Actor : AutoDestroyActors)
{
if (Actor)
{
OutActors.AddUnique(Actor);
}
}
OutActors.Append(DebugGatherRelevantActors());
}
void AFunctionalTest::AddRerun(FName Reason)
{
RerunCauses.Add(Reason);
}
FName AFunctionalTest::GetCurrentRerunReason()const
{
return CurrentRerunCause;
}
void AFunctionalTest::RegisterAutoDestroyActor(AActor* ActorToAutoDestroy)
{
AutoDestroyActors.AddUnique(ActorToAutoDestroy);
}
#if WITH_EDITOR
void AFunctionalTest::PostEditChangeProperty( struct FPropertyChangedEvent& PropertyChangedEvent)
{
static const FName NAME_FunctionalTesting = FName(TEXT("FunctionalTesting"));
static const FName NAME_TimeLimit = FName(TEXT("TimeLimit"));
static const FName NAME_TimesUpResult = FName(TEXT("TimesUpResult"));
Super::PostEditChangeProperty(PropertyChangedEvent);
if (PropertyChangedEvent.Property != NULL)
{
if (FObjectEditorUtils::GetCategoryFName(PropertyChangedEvent.Property) == NAME_FunctionalTesting)
{
// first validate new data since there are some dependencies
if (PropertyChangedEvent.Property->GetFName() == NAME_TimeLimit)
{
if (TimeLimit < 0.f)
{
TimeLimit = 0.f;
}
}
else if (PropertyChangedEvent.Property->GetFName() == NAME_TimesUpResult)
{
if (TimesUpResult == EFunctionalTestResult::Invalid)
{
TimesUpResult = EFunctionalTestResult::Failed;
}
}
}
}
}
void AFunctionalTest::OnSelectObject(UObject* NewSelection)
{
AFunctionalTest* AsFTest = Cast<AFunctionalTest>(NewSelection);
if (AsFTest)
{
AsFTest->MarkComponentsRenderStateDirty();
}
}
#endif // WITH_EDITOR
void AFunctionalTest::GoToObservationPoint()
{
if (ObservationPoint == nullptr)
{
return;
}
UWorld* World = GetWorld();
if (World && World->GetGameInstance())
{
APlayerController* TargetPC = nullptr;
for (FConstPlayerControllerIterator PCIterator = World->GetPlayerControllerIterator(); PCIterator; ++PCIterator)
{
APlayerController* PC = PCIterator->Get();
// Don't use debug camera player controllers.
// While it's tempting to teleport the camera if the user is debugging something then moving the camera around will them.
if (PC && !PC->IsA(ADebugCameraController::StaticClass()))
{
TargetPC = PC;
break;
}
}
if (TargetPC)
{
if (TargetPC->GetPawn())
{
TargetPC->GetPawn()->TeleportTo(ObservationPoint->GetActorLocation(), ObservationPoint->GetActorRotation(), /*bIsATest=*/false, /*bNoCheck=*/true);
TargetPC->SetControlRotation(ObservationPoint->GetActorRotation());
}
else
{
TargetPC->SetViewTarget(ObservationPoint);
}
}
}
}
/** Returns SpriteComponent subobject **/
UBillboardComponent* AFunctionalTest::GetSpriteComponent()
{
return SpriteComponent;
}
//////////////////////////////////////////////////////////////////////////
bool AFunctionalTest::AssertTrue(bool Condition, FString Message, const UObject* ContextObject)
{
if ( !Condition )
{
LogStep(ELogVerbosity::Error, FString::Printf(TEXT("Assertion failed: '%s' for context '%s'"), *Message, ContextObject ? *ContextObject->GetName() : TEXT("")));
return false;
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Assertion passed (%s)"), *Message));
return true;
}
}
bool AFunctionalTest::AssertFalse(bool Condition, FString Message, const UObject* ContextObject)
{
return AssertTrue(!Condition, Message, ContextObject);
}
bool AFunctionalTest::AssertIsValid(UObject* Object, FString Message, const UObject* ContextObject)
{
if ( !IsValid(Object) )
{
LogStep(ELogVerbosity::Error, FString::Printf(TEXT("Invalid object: '%s' for context '%s'"), *Message, ContextObject ? *ContextObject->GetName() : TEXT("")));
return false;
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Valid object: (%s)"), *Message));
return true;
}
}
bool AFunctionalTest::AssertValue_Int(int32 Actual, EComparisonMethod ShouldBe, int32 Expected, const FString& What, const UObject* ContextObject)
{
if ( !PerformComparison(Actual, Expected, ShouldBe) )
{
LogStep(ELogVerbosity::Error, FString::Printf(TEXT("%s: expected {%d} to be %s {%d} for context '%s'"), *What, Actual, *GetComparisonAsString(ShouldBe), Expected, ContextObject ? *ContextObject->GetName() : TEXT("")));
return false;
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("%s: expected {%d} to be %s {%d} for context '%s'"), *What, Actual, *GetComparisonAsString(ShouldBe), Expected, ContextObject ? *ContextObject->GetName() : TEXT("")));
return true;
}
}
bool AFunctionalTest::AssertValue_Float(float Actual, EComparisonMethod ShouldBe, float Expected, const FString& What, const UObject* ContextObject)
{
if ( !PerformComparison(Actual, Expected, ShouldBe) )
{
LogStep(ELogVerbosity::Error, FString::Printf(TEXT("%s: expected {%f} to be %s {%f} for context '%s'"), *What, Actual, *GetComparisonAsString(ShouldBe), Expected, ContextObject ? *ContextObject->GetName() : TEXT("")));
return false;
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("%s: expected {%f} to be %s {%f} for context '%s'"), *What, Actual, *GetComparisonAsString(ShouldBe), Expected, ContextObject ? *ContextObject->GetName() : TEXT("")));
return true;
}
}
bool AFunctionalTest::AssertValue_DateTime(FDateTime Actual, EComparisonMethod ShouldBe, FDateTime Expected, const FString& What, const UObject* ContextObject)
{
if ( !PerformComparison(Actual, Expected, ShouldBe) )
{
LogStep(ELogVerbosity::Error, FString::Printf(TEXT("%s: expected {%s} to be %s {%s} for context '%s'"), *What, *Actual.ToString(), *GetComparisonAsString(ShouldBe), *Expected.ToString(), ContextObject ? *ContextObject->GetName() : TEXT("")));
return false;
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("DateTime assertion passed (%s)"), *What));
return true;
}
}
bool AFunctionalTest::AssertEqual_Float(const float Actual, const float Expected, const FString& What, const float Tolerance, const UObject* ContextObject)
{
if ( !FMath::IsNearlyEqual(Actual, Expected, Tolerance) )
{
LogStep(ELogVerbosity::Error, FString::Printf(TEXT("Expected '%s' to be {%f}, but it was {%f} within tolerance {%f} for context '%s'"), *What, Expected, Actual, Tolerance, ContextObject ? *ContextObject->GetName() : TEXT("")));
return false;
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Float assertion passed (%s)"), *What));
return true;
}
}
bool AFunctionalTest::AssertEqual_Bool(const bool Actual, const bool Expected, const FString& What, const UObject* ContextObject)
{
if (Actual != Expected)
{
LogStep(ELogVerbosity::Error, FString::Printf(TEXT("Expected '%s' to be {%d}, but it was {%d} for context '%s'"), *What, Expected, Actual, ContextObject ? *ContextObject->GetName() : TEXT("")));
return false;
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Bool assertion passed (%s)"), *What));
return true;
}
}
bool AFunctionalTest::AssertEqual_Int(const int32 Actual, const int32 Expected, const FString& What, const UObject* ContextObject)
{
if (Actual != Expected)
{
LogStep(ELogVerbosity::Error, FString::Printf(TEXT("Expected '%s' to be {%d}, but it was {%d} for context '%s'"), *What, Expected, Actual, ContextObject ? *ContextObject->GetName() : TEXT("")));
return false;
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Bool assertion passed (%s)"), *What));
return true;
}
}
bool AFunctionalTest::AssertEqual_Name(const FName Actual, const FName Expected, const FString& What, const UObject* ContextObject)
{
if (Actual != Expected)
{
LogStep(ELogVerbosity::Error, FString::Printf(TEXT("Expected '%s' to be {%s}, but it was {%s} for context '%s'"), *What, *Expected.ToString(), *Actual.ToString(), ContextObject ? *ContextObject->GetName() : TEXT("")));
return false;
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("FName assertion passed (%s)"), *What));
return true;
}
}
bool AFunctionalTest::AssertEqual_Transform(const FTransform& Actual, const FTransform& Expected, const FString& What, const UObject* ContextObject)
{
if ( !Expected.Equals(Actual) )
{
LogStep(ELogVerbosity::Error, FString::Printf(TEXT("Expected '%s' to be {%s}, but it was {%s} for context '%s'"), *What, *TransformToString(Expected), *TransformToString(Actual), ContextObject ? *ContextObject->GetName() : TEXT("")));
return false;
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Transform assertion passed (%s)"), *What));
return true;
}
}
bool AFunctionalTest::AssertNotEqual_Transform(const FTransform& Actual, const FTransform& NotExpected, const FString& What, const UObject* ContextObject)
{
if ( NotExpected.Equals(Actual) )
{
LogStep(ELogVerbosity::Error, FString::Printf(TEXT("Expected '%s' not to be {%s} for context '%s'"), *What, *TransformToString(NotExpected), ContextObject ? *ContextObject->GetName() : TEXT("")));
return false;
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Transform assertion passed (%s)"), *What));
return true;
}
}
bool AFunctionalTest::AssertEqual_Rotator(const FRotator Actual, const FRotator Expected, const FString& What, const UObject* ContextObject)
{
if ( !Expected.Equals(Actual) )
{
LogStep(ELogVerbosity::Error, FString::Printf(TEXT("Expected '%s' to be {%s} but it was {%s} for context '%s'"), *What, *Expected.ToString(), *Actual.ToString(), ContextObject ? *ContextObject->GetName() : TEXT("")));
return false;
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Rotator assertion passed (%s)"), *What));
return true;
}
}
bool AFunctionalTest::AssertNotEqual_Rotator(const FRotator Actual, const FRotator NotExpected, const FString& What, const UObject* ContextObject)
{
if ( NotExpected.Equals(Actual) )
{
LogStep(ELogVerbosity::Error, FString::Printf(TEXT("Expected '%s' not to be {%s} for context '%s'"), *What, *NotExpected.ToString(), ContextObject ? *ContextObject->GetName() : TEXT("")));
return false;
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Rotator assertion passed (%s)"), *What));
return true;
}
}
bool AFunctionalTest::AssertEqual_Vector(const FVector Actual, const FVector Expected, const FString& What, const float Tolerance, const UObject* ContextObject)
{
if ( !Expected.Equals(Actual, Tolerance) )
{
LogStep(ELogVerbosity::Error, FString::Printf(TEXT("Expected '%s' to be {%s} but it was {%s} within tolerance {%f} for context '%s'"), *What, *Expected.ToString(), *Actual.ToString(), Tolerance, ContextObject ? *ContextObject->GetName() : TEXT("")));
return false;
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Vector assertion passed (%s)"), *What));
return true;
}
}
bool AFunctionalTest::AssertNotEqual_Vector(const FVector Actual, const FVector NotExpected, const FString& What, const UObject* ContextObject)
{
if ( NotExpected.Equals(Actual) )
{
LogStep(ELogVerbosity::Error, FString::Printf(TEXT("Expected '%s' not to be {%s} for context '%s'"), *What, *NotExpected.ToString(), ContextObject ? *ContextObject->GetName() : TEXT("")));
return false;
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Vector assertion passed (%s)"), *What));
return true;
}
}
bool AFunctionalTest::AssertEqual_String(const FString Actual, const FString Expected, const FString& What, const UObject* ContextObject)
{
if ( !Expected.Equals(Actual) )
{
LogStep(ELogVerbosity::Error, FString::Printf(TEXT("Expected '%s' to be {%s} but it was {%s} for context '%s'"), *What, *Expected, *Actual, ContextObject ? *ContextObject->GetName() : TEXT("")));
return false;
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("String assertion passed (%s)"), *What));
return true;
}
}
bool AFunctionalTest::AssertNotEqual_String(const FString Actual, const FString NotExpected, const FString& What, const UObject* ContextObject)
{
if ( NotExpected.Equals(Actual) )
{
LogStep(ELogVerbosity::Error, FString::Printf(TEXT("Expected '%s' not to be {%s} for context '%s'"), *What, *NotExpected, ContextObject ? *ContextObject->GetName() : TEXT("")));
return false;
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("String assertion passed (%s)"), *What));
return true;
}
}
bool AFunctionalTest::AssertEqual_TraceQueryResults(const UTraceQueryTestResults* Actual, const UTraceQueryTestResults* Expected, const FString& What, const UObject* ContextObject)
{
return Actual->AssertEqual(Expected, What, ContextObject, *this);
}
void AFunctionalTest::AddWarning(const FString Message)
{
LogStep(ELogVerbosity::Warning, Message);
}
void AFunctionalTest::AddError(const FString Message)
{
LogStep(ELogVerbosity::Error, Message);
}
void AFunctionalTest::LogStep(ELogVerbosity::Type Verbosity, const FString& Message)
{
FString FullMessage(Message);
if ( IsInStep() )
{
FullMessage.Append(TEXT(" in step: "));
FString StepName = TEXT("");
if ( StepName.IsEmpty() )
{
StepName = TEXT("<UN-NAMED STEP>");
}
FullMessage.Append(StepName);
}
switch ( Verbosity )
{
case ELogVerbosity::Display:
case ELogVerbosity::Log:
UE_VLOG(this, LogFunctionalTest, Display, TEXT("%s"), *FullMessage);
UE_LOG(LogFunctionalTest, Display, TEXT("%s"), *FullMessage);
break;
case ELogVerbosity::Warning:
UE_VLOG(this, LogFunctionalTest, Warning, TEXT("%s"), *FullMessage);
UE_LOG(LogFunctionalTest, Warning, TEXT("%s"), *FullMessage);
break;
case ELogVerbosity::Error:
UE_VLOG(this, LogFunctionalTest, Error, TEXT("%s"), *FullMessage);
UE_LOG(LogFunctionalTest, Error, TEXT("%s"), *FullMessage);
break;
}
}
FString AFunctionalTest::GetCurrentStepName() const
{
return IsInStep() ? Steps.Top() : FString();
}
void AFunctionalTest::StartStep(const FString& StepName)
{
Steps.Push(StepName);
}
void AFunctionalTest::FinishStep()
{
if ( Steps.Num() > 0 )
{
Steps.Pop();
}
else
{
AddWarning(TEXT("FinishStep was called when no steps were currently in progress."));
}
}
bool AFunctionalTest::IsInStep() const
{
return Steps.Num() > 0;
}
//////////////////////////////////////////////////////////////////////////
FPerfStatsRecord::FPerfStatsRecord(FString InName)
: Name(InName)
, GPUBudget(0.0f)
, RenderThreadBudget(0.0f)
, GameThreadBudget(0.0f)
{
}
void FPerfStatsRecord::SetBudgets(float InGPUBudget, float InRenderThreadBudget, float InGameThreadBudget)
{
GPUBudget = InGPUBudget;
RenderThreadBudget = InRenderThreadBudget;
GameThreadBudget = InGameThreadBudget;
}
FString FPerfStatsRecord::GetReportString() const
{
return FString::Printf(TEXT("%s,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f"),
*Name,
Record.FrameTimeTracker.GetMinValue() - Baseline.FrameTimeTracker.GetMinValue(),
Record.FrameTimeTracker.GetAvgValue() - Baseline.FrameTimeTracker.GetAvgValue(),
Record.FrameTimeTracker.GetMaxValue() - Baseline.FrameTimeTracker.GetMaxValue(),
Record.RenderThreadTimeTracker.GetMinValue() - Baseline.RenderThreadTimeTracker.GetMinValue(),
Record.RenderThreadTimeTracker.GetAvgValue() - Baseline.RenderThreadTimeTracker.GetAvgValue(),
Record.RenderThreadTimeTracker.GetMaxValue() - Baseline.RenderThreadTimeTracker.GetMaxValue(),
Record.GameThreadTimeTracker.GetMinValue() - Baseline.GameThreadTimeTracker.GetMinValue(),
Record.GameThreadTimeTracker.GetAvgValue() - Baseline.GameThreadTimeTracker.GetAvgValue(),
Record.GameThreadTimeTracker.GetMaxValue() - Baseline.GameThreadTimeTracker.GetMaxValue(),
Record.GPUTimeTracker.GetMinValue() - Baseline.GPUTimeTracker.GetMinValue(),
Record.GPUTimeTracker.GetAvgValue() - Baseline.GPUTimeTracker.GetAvgValue(),
Record.GPUTimeTracker.GetMaxValue() - Baseline.GPUTimeTracker.GetMaxValue());
}
FString FPerfStatsRecord::GetBaselineString() const
{
return FString::Printf(TEXT("%s,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f"),
*Name,
Baseline.FrameTimeTracker.GetMinValue(),
Baseline.FrameTimeTracker.GetAvgValue(),
Baseline.FrameTimeTracker.GetMaxValue(),
Baseline.RenderThreadTimeTracker.GetMinValue(),
Baseline.RenderThreadTimeTracker.GetAvgValue(),
Baseline.RenderThreadTimeTracker.GetMaxValue(),
Baseline.GameThreadTimeTracker.GetMinValue(),
Baseline.GameThreadTimeTracker.GetAvgValue(),
Baseline.GameThreadTimeTracker.GetMaxValue(),
Baseline.GPUTimeTracker.GetMinValue(),
Baseline.GPUTimeTracker.GetAvgValue(),
Baseline.GPUTimeTracker.GetMaxValue());
}
FString FPerfStatsRecord::GetRecordString() const
{
return FString::Printf(TEXT("%s,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f"),
*Name,
Record.FrameTimeTracker.GetMinValue(),
Record.FrameTimeTracker.GetAvgValue(),
Record.FrameTimeTracker.GetMaxValue(),
Record.RenderThreadTimeTracker.GetMinValue(),
Record.RenderThreadTimeTracker.GetAvgValue(),
Record.RenderThreadTimeTracker.GetMaxValue(),
Record.GameThreadTimeTracker.GetMinValue(),
Record.GameThreadTimeTracker.GetAvgValue(),
Record.GameThreadTimeTracker.GetMaxValue(),
Record.GPUTimeTracker.GetMinValue(),
Record.GPUTimeTracker.GetAvgValue(),
Record.GPUTimeTracker.GetMaxValue());
}
FString FPerfStatsRecord::GetOverBudgetString() const
{
double Min, Max, Avg;
GetRenderThreadTimes(Min, Max, Avg);
float RTMax = Max;
float RTBudgetFrac = Max / RenderThreadBudget;
GetGameThreadTimes(Min, Max, Avg);
float GTMax = Max;
float GTBudgetFrac = Max / GameThreadBudget;
GetGPUTimes(Min, Max, Avg);
float GPUMax = Max;
float GPUBudgetFrac = Max / GPUBudget;
return FString::Printf(TEXT("%s,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f"),
*Name,
RTMax,
RenderThreadBudget,
RTBudgetFrac,
GTMax,
GameThreadBudget,
GTBudgetFrac,
GPUMax,
GPUBudget,
GPUBudgetFrac
);
}
bool FPerfStatsRecord::IsWithinGPUBudget()const
{
double Min, Max, Avg;
GetGPUTimes(Min, Max, Avg);
return Max <= GPUBudget;
}
bool FPerfStatsRecord::IsWithinGameThreadBudget()const
{
double Min, Max, Avg;
GetGameThreadTimes(Min, Max, Avg);
return Max <= GameThreadBudget;
}
bool FPerfStatsRecord::IsWithinRenderThreadBudget()const
{
double Min, Max, Avg;
GetRenderThreadTimes(Min, Max, Avg);
return Max <= RenderThreadBudget;
}
void FPerfStatsRecord::GetGPUTimes(double& OutMin, double& OutMax, double& OutAvg)const
{
OutMin = Record.GPUTimeTracker.GetMinValue() - Baseline.GPUTimeTracker.GetMinValue();
OutMax = Record.GPUTimeTracker.GetMaxValue() - Baseline.GPUTimeTracker.GetMaxValue();
OutAvg = Record.GPUTimeTracker.GetAvgValue() - Baseline.GPUTimeTracker.GetAvgValue();
}
void FPerfStatsRecord::GetGameThreadTimes(double& OutMin, double& OutMax, double& OutAvg)const
{
OutMin = Record.GameThreadTimeTracker.GetMinValue() - Baseline.GameThreadTimeTracker.GetMinValue();
OutMax = Record.GameThreadTimeTracker.GetMaxValue() - Baseline.GameThreadTimeTracker.GetMaxValue();
OutAvg = Record.GameThreadTimeTracker.GetAvgValue() - Baseline.GameThreadTimeTracker.GetAvgValue();
}
void FPerfStatsRecord::GetRenderThreadTimes(double& OutMin, double& OutMax, double& OutAvg)const
{
OutMin = Record.RenderThreadTimeTracker.GetMinValue() - Baseline.RenderThreadTimeTracker.GetMinValue();
OutMax = Record.RenderThreadTimeTracker.GetMaxValue() - Baseline.RenderThreadTimeTracker.GetMaxValue();
OutAvg = Record.RenderThreadTimeTracker.GetAvgValue() - Baseline.RenderThreadTimeTracker.GetAvgValue();
}
void FPerfStatsRecord::Sample(UWorld* World, float DeltaSeconds, bool bBaseline)
{
check(World);
const FStatUnitData* StatUnitData = World->GetGameViewport()->GetStatUnitData();
check(StatUnitData);
if (bBaseline)
{
Baseline.FrameTimeTracker.AddSample(StatUnitData->RawFrameTime);
Baseline.GameThreadTimeTracker.AddSample(FPlatformTime::ToMilliseconds(GGameThreadTime));
Baseline.RenderThreadTimeTracker.AddSample(FPlatformTime::ToMilliseconds(GRenderThreadTime));
Baseline.GPUTimeTracker.AddSample(FPlatformTime::ToMilliseconds(GGPUFrameTime));
Baseline.NumFrames++;
Baseline.SumTimeSeconds += DeltaSeconds;
}
else
{
Record.FrameTimeTracker.AddSample(StatUnitData->RawFrameTime);
Record.GameThreadTimeTracker.AddSample(FPlatformTime::ToMilliseconds(GGameThreadTime));
Record.RenderThreadTimeTracker.AddSample(FPlatformTime::ToMilliseconds(GRenderThreadTime));
Record.GPUTimeTracker.AddSample(FPlatformTime::ToMilliseconds(GGPUFrameTime));
Record.NumFrames++;
Record.SumTimeSeconds += DeltaSeconds;
}
}
UAutomationPerformaceHelper::UAutomationPerformaceHelper()
: bRecordingBasicStats(false)
, bRecordingBaselineBasicStats(false)
, bRecordingCPUCapture(false)
, bRecordingStatsFile(false)
, bGPUTraceIfBelowBudget(false)
{
}
UWorld* UAutomationPerformaceHelper::GetWorld() const
{
UWorld* OuterWorld = GetOuter()->GetWorld();
ensureAsRuntimeWarning(OuterWorld != nullptr);
return OuterWorld;
}
void UAutomationPerformaceHelper::BeginRecordingBaseline(FString RecordName)
{
if (UWorld* World = GetWorld())
{
bRecordingBasicStats = true;
bRecordingBaselineBasicStats = true;
bGPUTraceIfBelowBudget = false;
Records.Add(FPerfStatsRecord(RecordName));
GEngine->SetEngineStat(World, World->GetGameViewport(), TEXT("Unit"), true);
}
}
void UAutomationPerformaceHelper::EndRecordingBaseline()
{
bRecordingBaselineBasicStats = false;
bRecordingBasicStats = false;
}
void UAutomationPerformaceHelper::BeginRecording(FString RecordName, float InGPUBudget, float InRenderThreadBudget, float InGameThreadBudget)
{
if (UWorld* World = GetWorld())
{
//Ensure we're recording engine stats.
GEngine->SetEngineStat(World, World->GetGameViewport(), TEXT("Unit"), true);
bRecordingBasicStats = true;
bRecordingBaselineBasicStats = false;
bGPUTraceIfBelowBudget = false;
FPerfStatsRecord* CurrRecord = GetCurrentRecord();
if (!CurrRecord || CurrRecord->Name != RecordName)
{
Records.Add(FPerfStatsRecord(RecordName));
CurrRecord = GetCurrentRecord();
}
check(CurrRecord);
CurrRecord->SetBudgets(InGPUBudget, InRenderThreadBudget, InGameThreadBudget);
}
}
void UAutomationPerformaceHelper::EndRecording()
{
if (const FPerfStatsRecord* Record = GetCurrentRecord())
{
UE_LOG(LogFunctionalTest, Log, TEXT("Finished Perf Stats Record:\n%s"), *Record->GetReportString());
}
bRecordingBasicStats = false;
}
void UAutomationPerformaceHelper::Tick(float DeltaSeconds)
{
if (bRecordingBasicStats)
{
Sample(DeltaSeconds);
}
if (bGPUTraceIfBelowBudget)
{
if (!IsCurrentRecordWithinGPUBudget())
{
FString PathName = FPaths::ProfilingDir();
GGPUTraceFileName = PathName / CreateProfileFilename(GetCurrentRecord()->Name, TEXT(".rtt"), true);
UE_LOG(LogFunctionalTest, Log, TEXT("Functional Test has fallen below GPU budget. Performing GPU trace."));
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("Performed GPU Thred Trace!"));
//Only perform one trace per test.
bGPUTraceIfBelowBudget = false;
}
}
//Other stats need ticking?
}
void UAutomationPerformaceHelper::Sample(float DeltaSeconds)
{
if (UWorld* World = GetWorld())
{
int32 Index = Records.Num() - 1;
if (Index >= 0 && bRecordingBasicStats)
{
Records[Index].Sample(World, DeltaSeconds, bRecordingBaselineBasicStats);
}
}
}
void UAutomationPerformaceHelper::WriteLogFile(const FString& CaptureDir, const FString& CaptureExtension)
{
FString PathName = FPaths::ProfilingDir();
if (!CaptureDir.IsEmpty())
{
PathName = PathName + (CaptureDir + TEXT("/"));
IFileManager::Get().MakeDirectory(*PathName);
}
FString Extension = CaptureExtension;
if (Extension.IsEmpty())
{
Extension = TEXT("perf.csv");
}
const FString Filename = CreateProfileFilename(CaptureExtension, true);
const FString FilenameFull = PathName + Filename;
const FString OverBudgetTableHeader = TEXT("TestName, MaxRT, RT Budget, RT Frac, MaxGT, GT Budget, GT Frac, MaxGPU, GPU Budget, GPU Frac\n");
FString OverbudgetTable;
const FString DataTableHeader = TEXT("TestName,MinFrameTime,AvgFrameTime,MaxFrameTime,MinRT,AvgRT,MaxRT,MinGT,AvgGT,MaxGT,MinGPU,AvgGPU,MaxGPU\n");
FString AdjustedTable;
FString RecordTable;
FString BaselineTable;
for (FPerfStatsRecord& Record : Records)
{
AdjustedTable += Record.GetReportString() + FString(TEXT("\n"));
RecordTable += Record.GetRecordString() + FString(TEXT("\n"));
BaselineTable += Record.GetBaselineString() + FString(TEXT("\n"));
if (!Record.IsWithinGPUBudget() || !Record.IsWithinRenderThreadBudget() || !Record.IsWithinGameThreadBudget())
{
OverbudgetTable += Record.GetOverBudgetString() + FString(TEXT("\n"));
}
}
FString FileContents = FString::Printf(TEXT("Over Budget Tests\n%s%s\nAdjusted Results\n%s%s\nRaw Results\n%s%s\nBaseline Results\n%s%s\n"),
*OverBudgetTableHeader, *OverbudgetTable, *DataTableHeader, *AdjustedTable, *DataTableHeader, *RecordTable, *DataTableHeader, *BaselineTable);
FFileHelper::SaveStringToFile(FileContents, *FilenameFull);
UE_LOG(LogTemp, Display, TEXT("Finished test, wrote file to %s"), *FilenameFull);
Records.Empty();
bRecordingBasicStats = false;
bRecordingBaselineBasicStats = false;
}
bool UAutomationPerformaceHelper::IsRecording()const
{
return bRecordingBasicStats;
}
void UAutomationPerformaceHelper::OnBeginTests()
{
OutputFileBase = CreateProfileFilename(TEXT(""), true);
StartOfTestingTime = FDateTime::Now().ToString();
}
void UAutomationPerformaceHelper::OnAllTestsComplete()
{
if (bRecordingBaselineBasicStats)
{
EndRecordingBaseline();
}
if (bRecordingBasicStats)
{
EndRecording();
}
if (bRecordingCPUCapture)
{
StopCPUProfiling();
}
if (bRecordingStatsFile)
{
EndStatsFile();
}
bGPUTraceIfBelowBudget = false;
if (Records.Num() > 0)
{
WriteLogFile(TEXT(""), TEXT("perf.csv"));
}
}
bool UAutomationPerformaceHelper::IsCurrentRecordWithinGPUBudget()const
{
if (const FPerfStatsRecord* Curr = GetCurrentRecord())
{
return Curr->IsWithinGPUBudget();
}
return true;
}
bool UAutomationPerformaceHelper::IsCurrentRecordWithinGameThreadBudget()const
{
if (const FPerfStatsRecord* Curr = GetCurrentRecord())
{
return Curr->IsWithinGameThreadBudget();
}
return true;
}
bool UAutomationPerformaceHelper::IsCurrentRecordWithinRenderThreadBudget()const
{
if (const FPerfStatsRecord* Curr = GetCurrentRecord())
{
return Curr->IsWithinRenderThreadBudget();
}
return true;
}
const FPerfStatsRecord* UAutomationPerformaceHelper::GetCurrentRecord()const
{
int32 Index = Records.Num() - 1;
if (Index >= 0)
{
return &Records[Index];
}
return nullptr;
}
FPerfStatsRecord* UAutomationPerformaceHelper::GetCurrentRecord()
{
int32 Index = Records.Num() - 1;
if (Index >= 0)
{
return &Records[Index];
}
return nullptr;
}
void UAutomationPerformaceHelper::StartCPUProfiling()
{
UE_LOG(LogFunctionalTest, Log, TEXT("START PROFILING..."));
ExternalProfiler.StartProfiler(false);
}
void UAutomationPerformaceHelper::StopCPUProfiling()
{
UE_LOG(LogFunctionalTest, Log, TEXT("STOP PROFILING..."));
ExternalProfiler.StopProfiler();
}
void UAutomationPerformaceHelper::TriggerGPUTraceIfRecordFallsBelowBudget()
{
bGPUTraceIfBelowBudget = true;
}
void UAutomationPerformaceHelper::BeginStatsFile(const FString& RecordName)
{
if (UWorld* World = GetWorld())
{
FString MapName = World->GetMapName();
FString Cmd = FString::Printf(TEXT("Stat StartFile %s-%s/%s.ue4stats"), *MapName, *StartOfTestingTime, *RecordName);
GEngine->Exec(World, *Cmd);
}
}
void UAutomationPerformaceHelper::EndStatsFile()
{
if (UWorld* World = GetWorld())
{
GEngine->Exec(World, TEXT("Stat StopFile"));
}
}