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

==========================
MAJOR FEATURES + CHANGES
==========================

Change 3354003 on 2017/03/20 by Thomas.Sarkanen

	Back out changelist 3353914

Change 3355932 on 2017/03/21 by Thomas.Sarkanen

	Back out changelist 3354003

	Reinstating merge from Main:

	Merging //UE4/Dev-Main to Dev-AnimPhys (//UE4/Dev-AnimPhys) @ CL 3353839

Change 3385512 on 2017/04/07 by Aaron.McLeran

	Bringing changes over from FN that fix audio streaming on PC/Mac/XboxOne/PS4

	CL#3318457 - Fix crash when recycling PS4 sound sources.
	CL#3313213 - Allowing XboxOne to cook streaming audio
	CL#3313719 - GetWaveFormat now returns OPUS for streaming audio waves
	CL#3320066 - Added libopus for XboxOne
	CL#3320070 - libopus is now properly linked in XboxOne

	CL#3313219 - Allowing Mac to cook streaming audio
	CL#3315332 - Fixed audio streaming on Mac
	CL#3315335 - (additional file missed in previous CL)

	CL#3313207 - Sounds now register themselves with the audio streaming manager even if they are loaded before the audio device manager is created.
	CL#3313294 - Removed some accidental debugging code that was mistakenly added in CL#3313207
	CL#3318530 - Fix threading issues in FAudioStreamingManager

	CL#3340718 - Fix for crash with audio streaming
	CL#3340844 - Fix for more thread safety in audio streaming manager
	CL#3343794 - Added a check in destructor of loaded chunk
	CL#3343794 - Removing check in stopping a source
	CL#3355393 - Moving audio streaming callbacks to use indices rather than ptrs to elements in dynamic array
	CL#3369020 - bumping up size of compressed chunks for AT9 files when doing stream chunk compression
	CL#3369131 - bumping up AT9 version number to get new AT9 cooks for larger chunks

	CL#3373626 - Fixing ps4 streaming
	CL#3375110 - Reverting some changes from 3373626
	CL#3382078 - Making audio streaming decoding async to audio thread for xaudio2
	CL#3383214 - Fixing buffer order issue for audio streaming

Change 3386321 on 2017/04/10 by Lina.Halper

	#ANIM : preview
	- Attache preview mesh to use copy mesh pose

	#jira: UE-43114, UEAP-186
	#rb: Thomas.Sarkanen

Change 3386415 on 2017/04/10 by Ori.Cohen

	Improve the cost of UpdateKinematicBodies - added the ability to defer non simulating bodies.

	#JIRA UEAP-79

Change 3386418 on 2017/04/10 by Ori.Cohen

	Fix physx memory leak when a commandlet loads many assets without ticking scene

	#JIRA UE-43378

Change 3386569 on 2017/04/10 by dan.reynolds

	Updated dummy platform generated by standalone AEOverview tests to distinguish floor materials between the platform and the test zone.

Change 3386714 on 2017/04/10 by Ori.Cohen

	Improve stats extensibility and expose it to the automation framework.

Change 3386805 on 2017/04/10 by Lina.Halper

	Fix build error for editor

	#rb: none

Change 3386854 on 2017/04/10 by Lina.Halper

	build fix for clang

	#rb:none

Change 3387198 on 2017/04/10 by Aaron.McLeran

	#jira UE-43699 Deleting unused velocity variable.

	OpenAL's velocity is not supported in WebAudio.

	Removing dead code in AndroidAudioSource.cpp

Change 3387346 on 2017/04/10 by Ori.Cohen

	Added performance regression map for physics (update kinematic bones and postBroadPhase)

	#JIRA UEAP-79

Change 3387409 on 2017/04/10 by Ori.Cohen

	Fix build, forgot to update this code

Change 3387536 on 2017/04/10 by Lina.Halper

	Merging using AnimPhys-Fortnite-Main
	- fix preview mesh selection/animation

	#code review: Thomas.Sarkanen
	#rb: none

	#i_need_autocorrect

Change 3387995 on 2017/04/11 by Martin.Wilson

	Live link updates
	- Refactor of provider api (separate update of hierarchy and transforms)
	- multi connection streaming from provider
	- provider maintains internal state so that new connections can be updated without interaction with streaming source.
	- Lifetime changes (connection timeout)

Change 3388241 on 2017/04/11 by Lina.Halper

	Merging using AnimPhys-Fortnite-Main

	- merge CL of 3388238
	#rb: Thomas.Sarkanen

Change 3388294 on 2017/04/11 by Lina.Halper

	build fix

	#rb: none

Change 3388341 on 2017/04/11 by Ori.Cohen

	Turn off vs2013 for physx

Change 3389115 on 2017/04/11 by Ori.Cohen

	Forgot missing blueprint for perf test

Change 3389224 on 2017/04/11 by Ori.Cohen

	Added sweep multi tests to perf regression

	#JIRA UEAP-79

Change 3389984 on 2017/04/12 by Martin.Wilson

	CIS Fix

Change 3390315 on 2017/04/12 by Lina.Halper

	- fix on crash of component array when shutting down anim blueprint

	#jira: UE-43868
	#rb: Thomas.Sarkanen

Change 3390402 on 2017/04/12 by Martin.Wilson

	Fix update not being called on post process instances when the main anim instance does not do a parallel update

	#jira UE-43906

Change 3390772 on 2017/04/12 by Lina.Halper

	Fix crash on importing LOD with lesser # of joints

	#rb: Benn.Gallagher

Change 3394850 on 2017/04/14 by Aaron.McLeran

	Adjusting how wavetable generation works for custom wavetables.

	- Changed wavetable creation to use a TSharedPtr vs a raw ptr.

Change 3394853 on 2017/04/14 by Aaron.McLeran

	Bringing from Odin the ability to set the lowpass filter frequency on an audio component from BP

Change 3395684 on 2017/04/17 by Ori.Cohen

	Make debugdraw for line traces const correct.

Change 3396680 on 2017/04/17 by Ori.Cohen

	Added a total scene query stat and the ability to trace all scene queries

Change 3397564 on 2017/04/18 by Benn.Gallagher

	Added clothing functional and performance test map + assets.

Change 3397769 on 2017/04/18 by Thomas.Sarkanen

	CIS fix

	Fixup incorrect AudioStreaming.cpp merge when bringing Main into Dev-AnimPhys

Change 3398518 on 2017/04/18 by Lina.Halper

	Mirroring fix on set world rotation

	#rb: Zak.Middleton
	#jira: UE-43830

Change 3400400 on 2017/04/19 by Chad.Garyet

	adding switch physx build to anim-phys

Change 3400416 on 2017/04/19 by Chad.Garyet

	updated email targets to include switch

Change 3402005 on 2017/04/20 by Ori.Cohen

	Pass stats into scene queries. Not all call sites are updated yet, waiting on Jon for uber search/replace script.

Change 3402264 on 2017/04/20 by Ori.Cohen

	CIS fix

Change 3402344 on 2017/04/20 by Ori.Cohen

	Turn off find unknown (was on by mistake)

Change 3403311 on 2017/04/21 by Benn.Gallagher

	Clothing changes from Dev-General. Fixed LOD pops, mesh swap crashes and convex collision locations

Change 3403399 on 2017/04/21 by Benn.Gallagher

	Lighting build, content cleanup and reorganization for clothing test map

Change 3403401 on 2017/04/21 by Benn.Gallagher

	Clothing test ground truth updates after lighting build.

Change 3403813 on 2017/04/21 by danny.bouimad

	Adding everything needed for our multiplat map TM-AnimPhys

Change 3403900 on 2017/04/21 by mason.seay

	Added WIP text to tests that need fixup

Change 3405383 on 2017/04/24 by Ori.Cohen

	Fix typo where complex flag was not being passed in  to constructor.

	#JIRA UE-44278, UE-44279

Change 3405389 on 2017/04/24 by Martin.Wilson

	Live link:
	- Added support for sending curve data across live link and applying it via the Live Link node
	- Added pose snapshots which are built in the live link clients tick and read by the rest of the engine, save reading live data.

Change 3405569 on 2017/04/24 by Martin.Wilson

	Missed file from CL 3405389

Change 3405810 on 2017/04/24 by Chad.Garyet

	fixing busted target for dev-animphys stream

Change 3406566 on 2017/04/24 by Aaron.McLeran

	#jira UE-44272 Fixing granular synth with packaged builds

	- Changed the way granular synth component and wave table component get PCM data from USoundWave assets. No duplication, just precache directly.

Change 3406694 on 2017/04/24 by Aaron.McLeran

	Update to phonon/steam audio plugin from valve

Change 3407794 on 2017/04/25 by Aaron.McLeran

	#jira UE-44357 Fix for attenuation settings in sequencer

Change 3407848 on 2017/04/25 by Jon.Nabozny

	Add stats to FCollisionQueryParams (continued from CL-3402005).

Change 3407857 on 2017/04/25 by Jon.Nabozny

	Disable FIND_UNKNOWN_SCENE_QUERIES.

Change 3407915 on 2017/04/25 by Lina.Halper

	Animation Automation Test for curve and simple notify

Change 3408164 on 2017/04/25 by Ori.Cohen

	Expose the physx tree rebuild rate.

Change 3408174 on 2017/04/25 by Lina.Halper

	- Changed 1, 2, 3, 4 for ordering of timing
	- Made sure the notify test takes more time between shots.

Change 3408359 on 2017/04/25 by Jon.Nabozny

	Fix FConfigFile::Write for arrays of different sizes.
	(Looks like it is still broke for arrays of the same same, with different values).

Change 3408633 on 2017/04/25 by Aaron.McLeran

	#jira UE-44297 Fix for animating sound cue graph when editor "non-realtime" button is checked

	- Fix is to explicitely register an active timer lambda that queries the preview audio component while the sound cue is playing

Change 3408768 on 2017/04/25 by Aaron.McLeran

	Fixing UHT crash

Change 3409225 on 2017/04/26 by Lina.Halper

	Increase tolerance for the shot test. It's very sensitive otherwise.

Change 3409313 on 2017/04/26 by Benn.Gallagher

	Refactor of clothing paint tool framework to create a more extensible editor and get rid of some GDC techdebt

Change 3409478 on 2017/04/26 by danny.bouimad

	Moved Text Actor forwards as it was causing zFighting

Change 3409572 on 2017/04/26 by Benn.Gallagher

	CIS fix after cloth painter changes

Change 3409585 on 2017/04/26 by danny.bouimad

	Updated Tm-AnimPhys to utilize the AEOverview maps, also found a crash with viewing shader complexity that only occurs on this map.

Change 3410948 on 2017/04/27 by Martin.Wilson

	Live Link:
	- Add subject clearing support to client / message bus protocol
	- Update ref skeleton when subject changes.
	- Remove old classes

Change 3413305 on 2017/04/28 by Danny.Bouimad

	Disabled audio tests on AnimPhys Testmap to hopefuly stop the lighting crashes during launch on (content problem)

Change 3413408 on 2017/04/28 by mason.seay

	Resaving to clear empty engine version warnings

Change 3413418 on 2017/04/28 by Benn.Gallagher

	CIS fix, #pragma once in wrong place (after an include)

Change 3413737 on 2017/04/28 by Martin.Wilson

	Rename Live Link Message Bus messages to contain the word message to avoid future name clashes

Change 3414121 on 2017/04/28 by Ori.Cohen

	Added task batching for physx tasks. Set fortnite to 8 as we already have a lot of thread contention during that time and it's best to just do it all in a single task.

Change 3417833 on 2017/05/02 by Thomas.Sarkanen

	Fix bad merge in SynthComponentGranulator.cpp

Change 3418174 on 2017/05/02 by Jon.Nabozny

	Fix memory leak in UDestructibleComponent::SetSkeletalMesh

Change 3418907 on 2017/05/02 by Aaron.McLeran

	#jira UE-44599 Fixing active sound un-pause issue.

	- While paused, active sounds were updating their active playbacktime.

Change 3419001 on 2017/05/02 by Ori.Cohen

	Added GetNumSimulatedAndAwake so that we can easily test for jitter.

Change 3419079 on 2017/05/02 by Ori.Cohen

	Added a jitter automated test.

Change 3419213 on 2017/05/02 by mason.seay

	Reaving content to remove empty engine version warnings

Change 3419351 on 2017/05/02 by Ori.Cohen

	Added automated test for raycasting against landscape from underneath (JIRA UE-39819)
	It looks like this is currently broken

Change 3419356 on 2017/05/02 by Ori.Cohen

	Updated test with associated JIRA where we first saw this

Change 3419478 on 2017/05/02 by Ori.Cohen

	Added automated test for origin shift regression crash when using aggregates.

Change 3420736 on 2017/05/03 by Ori.Cohen

	Added automated test for moving objects during an overlap callback for UE-41450
	#rnx

Change 3420803 on 2017/05/03 by Ori.Cohen

	Added automated test for JIRA UE-18019
	#rnx

Change 3420835 on 2017/05/03 by Jurre.deBaare

	Anim modifier BP for release notes

Change 3421185 on 2017/05/03 by Ori.Cohen

	Missing file

Change 3422673 on 2017/05/04 by danny.bouimad

	Fixed the cooked/uncooked lighting issue with AEO_StageFloor. The lights should no longer repeatidly spawn when loading in as sub levels.

Change 3422898 on 2017/05/04 by Danny.Bouimad

	Updating QA Audio Content

Change 3422908 on 2017/05/04 by Danny.Bouimad

	Fixing Automation CIS error 'Can't find file for asset. /Game/Tests/Physics/ISMCStaticSweep_BuiltData'

Change 3423508 on 2017/05/04 by Danny.Bouimad

	Replacing ground truth and adding build data for nonissue Automation CIS failure OverlapCallback

Change 3423634 on 2017/05/04 by danny.bouimad

	Made updates to TM-AnimPhys testmap

Change 3423870 on 2017/05/04 by Ori.Cohen

	Fix wheels separating from vehicle due to world kinematic refactor. Added temp variable for now

	#jira UE-44624

Change 3423917 on 2017/05/04 by Ori.Cohen

	Assert_Equal for int returns a bool

Change 3425267 on 2017/05/05 by Martin.Wilson

	Live Link

	- Add interpolation to subjects
	- Add connection settings that can be modified in client panel. All subjects modified by a connection use its connection settings
	- Give live link sources their client Guid so that they can send it with subject data

Change 3425303 on 2017/05/05 by Martin.Wilson

	Missed file from CL 3425267

Change 3430351 on 2017/05/09 by Martin.Wilson

	Crash fix for live link interpolation

Change 3430601 on 2017/05/09 by Benn.Gallagher

	Disabled clothing perf test temporarily due to stats issues

Change 3432669 on 2017/05/10 by Ori.Cohen

	Temporarily turn off line trace under heightfield test. This is a known bug which won't be fixed until 4.17

Change 3432679 on 2017/05/10 by Ori.Cohen

	Temporarily turn off check during TLS release on Switch.

Change 3434960 on 2017/05/11 by danny.bouimad

	Disabled content on TM-AnimPhys that was casuing a out of memory when drawing debug lines on switch.

Change 3436639 on 2017/05/12 by Danny.Bouimad

	Updating ground truths and map for OverlapCallBack to fix CIS error.

[CL 3437043 by Thomas Sarkanen in Main branch]
2017-05-12 11:21:11 -04:00

1201 lines
35 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"
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))
{
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)
, StartFrame(0)
, StartTime(0.0f)
, bIsReady(false)
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = false;
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.
GetWorld()->DelayGarbageCollection();
RunFrame = GFrameNumber;
TotalTime = 0.f;
if (TimeLimit >= 0)
{
SetActorTickEnabled(true);
}
bIsReady = false;
bIsRunning = true;
GoToObservationPoint();
PrepareTest();
return true;
}
void AFunctionalTest::PrepareTest()
{
ReceivePrepareTest();
}
void AFunctionalTest::StartTest()
{
TotalTime = 0.f;
StartFrame = GFrameNumber;
StartTime = GetWorld()->GetTimeSeconds();
ReceiveStartTest();
OnTestStart.Broadcast();
}
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.
GetWorld()->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 )
{
FinishTest(TimesUpResult, TimesUpMessage.ToString());
}
else
{
Super::Tick(DeltaSeconds);
}
}
else
{
TotalTime += DeltaSeconds;
if ( PreparationTimeLimit > 0.f && TotalTime > PreparationTimeLimit )
{
FinishTest(TimesUpResult, TimesUpMessage.ToString());
}
}
}
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.
GetWorld()->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;
}
//////////////////////////////////////////////////////////////////////////
void 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("")));
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Assertion passed (%s)"), *Message));
}
}
void AFunctionalTest::AssertFalse(bool Condition, FString Message, const UObject* ContextObject)
{
AssertTrue(!Condition, Message, ContextObject);
}
void 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("")));
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Valid object: (%s)"), *Message));
}
}
void 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("")));
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Int assertion passed (%s)"), *What));
}
}
void 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("")));
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Float assertion passed (%s)"), *What));
}
}
void 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("")));
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("DateTime assertion passed (%s)"), *What));
}
}
void 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("")));
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Float assertion passed (%s)"), *What));
}
}
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("Int assertion passed (%s)"), *What));
return true;
}
}
void 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("")));
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Transform assertion passed (%s)"), *What));
}
}
void 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("")));
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Transform assertion passed (%s)"), *What));
}
}
void 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("")));
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Rotator assertion passed (%s)"), *What));
}
}
void 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("")));
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Rotator assertion passed (%s)"), *What));
}
}
void 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("")));
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Vector assertion passed (%s)"), *What));
}
}
void 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("")));
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("Vector assertion passed (%s)"), *What));
}
}
void 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("")));
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("String assertion passed (%s)"), *What));
}
}
void 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("")));
}
else
{
LogStep(ELogVerbosity::Log, FString::Printf(TEXT("String assertion passed (%s)"), *What));
}
}
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::Log:
UE_VLOG(this, LogFunctionalTest, Log, TEXT("%s"), *FullMessage);
UE_LOG(LogFunctionalTest, Log, 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)
{
}
void UAutomationPerformaceHelper::BeginRecordingBaseline(FString RecordName)
{
bRecordingBasicStats = true;
bRecordingBaselineBasicStats = true;
bGPUTraceIfBelowBudget = false;
Records.Add(FPerfStatsRecord(RecordName));
GEngine->SetEngineStat(GetOuter()->GetWorld(), GetOuter()->GetWorld()->GetGameViewport(), TEXT("Unit"), true);
}
void UAutomationPerformaceHelper::EndRecordingBaseline()
{
bRecordingBaselineBasicStats = false;
bRecordingBasicStats = false;
}
void UAutomationPerformaceHelper::BeginRecording(FString RecordName, float InGPUBudget, float InRenderThreadBudget, float InGameThreadBudget)
{
//Ensure we're recording engine stats.
GEngine->SetEngineStat(GetOuter()->GetWorld(), GetOuter()->GetWorld()->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)
{
int32 Index = Records.Num() - 1;
if (Index >= 0 && bRecordingBasicStats)
{
Records[Index].Sample(GetOuter()->GetWorld(), 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)
{
FString MapName = GetOuter()->GetWorld()->GetMapName();
FString Cmd = FString::Printf(TEXT("Stat StartFile %s-%s/%s.ue4stats"), *MapName, *StartOfTestingTime, *RecordName);
GEngine->Exec(GetOuter()->GetWorld(), *Cmd);
}
void UAutomationPerformaceHelper::EndStatsFile()
{
GEngine->Exec(GetOuter()->GetWorld(), TEXT("Stat StopFile"));
}