Merging from /usr/John_Barrett: Added UE4 UScript/Blueprint Virtual Machine reflection class, new stack tracing and log tracing code, fixed and verified fixes for many broken unit tests; full list of changes:

- Added class for allowing reflection of UObjects/UStructs/UProperties/etc., so that properties can be referenced dynamically by string, instead of statically
     - Allows game code to be referenced without direct linking
     - Allows unit tests to break without blocking compiles, allows easier backwards compatability, and easier archiving of old unit tests
     - Very useful for quick debug code; may tie it into the console with autocomplete at some stage
     - Example:
          FGuid* EntryItemGuidRef = (FGuid*)(void*)((FVMReflection(UnitPC.Get())->*"WorldInventory"->*"Inventory"->*ItemsPropName)["FFortItemEntry"][0]->*"ItemGuid")["FGuid"];

- Added a new stack tracing manager, which can be used by unit tests through GTraceManager, or through the whole engine with the StackTrace console command (see UnitTestManager.cpp for full command list)
     - Allows conditional/filtered stack traces to be inserted anywhere into the code (categorized by name), for debugging purposes
     - Once-off stack trace example:
          GEngine->Exec(NULL, TEXT("StackTrace TraceName"));
     - Multiple/accumulated stack trace examples:
          GEngine->Exec(NULL, TEXT("StackTrace TraceName Add -Log"));
          GEngine->Exec(NULL, TEXT("StackTrace TraceName Dump"));

- Added a log hook for the stack tracing manager, and a LogTrace command, which allows stack traces to be dumped whenever a specific log is encountered (see UnitTestManager.cpp for full command list)
     - Example, for debugging the cause of a disconnect:
          "LogTrace AddPartial UNetConnection::Close: Name: IpConnection_"

- Added -UnitTestCap=x commandline parameter, to limit the maximum number of unit tests that can run at once

- Added AllowedClientActors/AllowedClientRPCs to ClientUnitTest, to allow better whitelisting of accepted actors/RPC's

- Fixed many broken unit tests

- Adjusted actor replication blocking hook, to allow blocking of all instances of actor replication, instead of just actor channels

- Made console command results, auto-focus the Console tab, if the current tab won't display the results

- Fixed blocking automation testing, when the current game project doesn't support unit tests (doesn't have a unit test environment setup)

- Disable recursive killing of unit test child processes, while TerminateProc has a bug that kills all killable Windows processes

- Updated crash callstack detection

[CL 2565239 by John Barrett in Main branch]
This commit is contained in:
John Barrett
2015-05-26 13:35:34 -04:00
committed by Shambler@OldUnreal.com
parent dac8dc638e
commit 5d70d237c5
29 changed files with 5110 additions and 231 deletions

View File

@@ -7,8 +7,8 @@
"CreatedBy" : "Epic Games, Inc.",
"CreatedByURL" : "http://epicgames.com",
"EngineVersion" : "4.2.0",
"Description": "A unit testing framework for testing the UE4 netcode, primarily for bugs and exploits",
"Category": "Networking",
"Description" : "A unit testing framework for testing the UE4 netcode, primarily for bugs and exploits",
"Category" : "Networking",
"Modules" :
[

View File

@@ -63,8 +63,8 @@ enum class EUnitTestFlags : uint32 // NOTE: If you change from uint32, you need
IgnoreDisconnect = 0x00080000, // Whether or not minimal/fake client disconnects, should be treated as a unit test failure
/** Unit test events */
NotifyAllowNetActor = 0x00100000, // Whether or not to trigger 'NotifyAllowNetActor', which whitelist actor channels by class
NotifyNetActors = 0x00200000, // Whether or not to trigger a 'NotifyNetActor' event, AFTER creation of actor channel actor
NotifyProcessEvent = 0x00400000, // Whether or not to trigger 'NotifyScriptProcessEvent' for every executed local function
/** Debugging */
CaptureReceivedRaw = 0x01000000, // Whether or not to capture raw (clientside) packet receives
@@ -72,8 +72,8 @@ enum class EUnitTestFlags : uint32 // NOTE: If you change from uint32, you need
DumpReceivedRaw = 0x04000000, // Whether or not to also hex-dump the raw packet receives to the log/log-window
DumpSendRaw = 0x08000000, // Whether or not to also hex-dump the raw packet sends to the log/log-window
DumpControlMessages = 0x10000000, // Whether or not to dump control channel messages, and their raw hex content
DumpReceivedRPC = 0x20000000, // Whether or not to dump RPC receives
DumpSendRPC = 0x40000000, // Whether or not to dump RPC sends
DumpReceivedRPC = 0x20000000, // Whether or not to dump RPC receives (with LogNetTraffic, detects ProcessEvent RPC fail)
DumpSendRPC = 0x40000000 // Whether or not to dump RPC sends
};
// Required for bitwise operations with the above enum
@@ -107,8 +107,8 @@ inline FString GetUnitTestFlagName(EUnitTestFlags Flag)
EUTF_CASE(IgnoreServerCrash);
EUTF_CASE(IgnoreClientCrash);
EUTF_CASE(IgnoreDisconnect);
EUTF_CASE(NotifyAllowNetActor);
EUTF_CASE(NotifyNetActors);
EUTF_CASE(NotifyProcessEvent);
EUTF_CASE(CaptureReceivedRaw);
EUTF_CASE(CaptureSendRaw);
EUTF_CASE(DumpReceivedRaw);
@@ -300,6 +300,12 @@ protected:
/** The (non-URL) commandline parameters clients should be launched with */
FString BaseClientParameters;
/** Actors the server is allowed replicate to client (requires AllowActors flag). Use NotifyAllowNetActor for conditional allows. */
TArray<UClass*> AllowedClientActors;
/** Clientside RPC's that should be allowed to execute (requires NotifyProcessEvent flag; other flags also allow specific RPC's) */
TArray<FString> AllowedClientRPCs;
/** Runtime variables */
protected:
@@ -413,12 +419,13 @@ public:
virtual void NotifyHandleClientPlayer(APlayerController* PC, UNetConnection* Connection);
/**
* Override this, to receive notification BEFORE an actor channel actor has been created (allowing you to block, based on class)
* Override this, to receive notification BEFORE a replicated actor has been created (allowing you to block, based on class)
*
* @param ActorClass The actor class that is about to be created
* @param bActorChannel Whether or not this actor is being created within an actor channel
* @return Whether or not to allow creation of that actor
*/
virtual bool NotifyAllowNetActor(UClass* ActorClass);
virtual bool NotifyAllowNetActor(UClass* ActorClass, bool bActorChannel);
/**
* Override this, to receive notification AFTER an actor channel actor has been created
@@ -471,10 +478,9 @@ public:
* @param Actor The actor the event is being executed on
* @param Function The script function being executed
* @param Parameters The raw unparsed parameters, being passed into the function
* @param HookOrigin Reference to the unit test that the event is associated with
* @return Whether or not to block the event from executing
*/
virtual bool NotifyScriptProcessEvent(AActor* Actor, UFunction* Function, void* Parameters, void* HookOrigin);
virtual bool NotifyScriptProcessEvent(AActor* Actor, UFunction* Function, void* Parameters);
#endif
/**
@@ -549,12 +555,14 @@ public:
/**
* Sends the specified RPC for the specified actor, and verifies that the RPC was sent (triggering a unit test failure if not)
*
* @param Target The actor which will send the RPC
* @param FunctionName The name of the RPC
* @param Parms The RPC parameters (same as would be specified to ProcessEvent)
* @return Whether or not the RPC was sent successfully
* @param Target The actor which will send the RPC
* @param FunctionName The name of the RPC
* @param Parms The RPC parameters (same as would be specified to ProcessEvent)
* @param ParmsSize The size of the RPC parameters, for verifying binary compatibility
* @param ParmsSizeCorrection Some parameters are compressed to a different size. Verify Parms matches, and use this to correct.
* @return Whether or not the RPC was sent successfully
*/
bool SendRPCChecked(AActor* Target, const TCHAR* FunctionName, void* Parms);
bool SendRPCChecked(AActor* Target, const TCHAR* FunctionName, void* Parms, int16 ParmsSize, int16 ParmsSizeCorrection=0);
/**
* As above, except the RPC is called within a lambda
@@ -608,10 +616,12 @@ protected:
/**
* Whether or not all 'requirements' flag conditions have been met
*
* @return Whether or not all requirements are met
* @param bIgnoreCustom If true, checks all requirements other than custom requirements
* @return Whether or not all requirements are met
*/
bool HasAllRequirements();
bool HasAllRequirements(bool bIgnoreCustom=false);
public:
/**
* Optionally, if the 'RequireCustom' flag is set, this returns whether custom conditions have been met.
*
@@ -633,7 +643,7 @@ protected:
*/
virtual ELogType GetExpectedLogTypes() override;
protected:
virtual bool ExecuteUnitTest() override;
virtual void CleanupUnitTest() override;

View File

@@ -49,6 +49,10 @@ class ANUTActor : public AActor, public FSelfRegisteringExec
{
GENERATED_UCLASS_BODY()
private:
/** The name of the beacon net driver */
FName BeaconDriverName;
public:
/** The value of World.RealTimeSeconds as of the last time the client was marked as still alive */
float LastAliveTime;
@@ -67,7 +71,11 @@ public:
virtual void PostActorCreated() override;
#if TARGET_UE4_CL < CL_CONSTNETCONN
virtual UNetConnection* GetNetConnection() override;
#else
virtual UNetConnection* GetNetConnection() const override;
#endif
bool NotifyControlMessage(UNetConnection* Connection, uint8 MessageType, FInBunch& Bunch);

View File

@@ -46,12 +46,13 @@ class UUnitTestNetConnection : public UIpConnection
DECLARE_DELEGATE_TwoParams(FReceivedRawPacketDel, void* /*Data*/, int32& /*Count*/);
/**
* Delegate for notifying on (and optionally blocking) actor channel creation
* Delegate for notifying on (and optionally blocking) replicated actor creation
*
* @param ActorClass The class of the actor being replicated
* @return Whether or not to allow creation of the actor channel
* @param bActorChannel Whether or not this actor creation is from an actor channel
* @return Whether or not to allow creation of the actor
*/
DECLARE_DELEGATE_RetVal_OneParam(bool, FOnActorChannelSpawn, UClass* /*ActorClass*/);
DECLARE_DELEGATE_RetVal_TwoParams(bool, FOnReplicatedActorSpawn, UClass* /*ActorClass*/, bool /*bActorChannel*/);
/** Delegate for hooking LowLevelSend */
@@ -60,8 +61,8 @@ class UUnitTestNetConnection : public UIpConnection
/** Delegate for hooking ReceivedRawPacket */
FReceivedRawPacketDel ReceivedRawPacketDel;
/** Delegate for notifying on actor channel creation */
FOnActorChannelSpawn ActorChannelSpawnDel;
/** Delegate for notifying on replicated actor creation */
FOnReplicatedActorSpawn ReplicatedActorSpawnDel;
};

View File

@@ -17,6 +17,7 @@ class UUnitTestPackageMap : public UPackageMapClient
TSharedPtr<FNetGUIDCache> InNetGUIDCache)
: UPackageMapClient(ObjectInitializer, InConnection, InNetGUIDCache)
, bWithinSerializeNewActor(false)
, bPendingArchetypeSpawn(false)
{
}
#endif
@@ -29,5 +30,8 @@ class UUnitTestPackageMap : public UPackageMapClient
public:
/** Whether or not we are currently within execution of SerializeNewActor */
bool bWithinSerializeNewActor;
/** Whether or not SerializeNewActor is about to spawn an actor, from an archetype */
bool bPendingArchetypeSpawn;
};

View File

@@ -137,6 +137,9 @@ protected:
/** The unit test environment (not set until the current games unit test module is loaded - not set at all, if no such module) */
static FUnitTestEnvironment* UnitEnv;
/** The null unit test environment - for unit tests which support all games, due to requiring no game-specific features */
static FUnitTestEnvironment* NullUnitEnv;
/** The time of the last NetTick event */
double LastNetTick;
@@ -227,7 +230,14 @@ public:
FString CurGame = FApp::GetGameName();
Result = ExpectedResult[CurGame];
if (ExpectedResult.Contains(CurGame))
{
Result = ExpectedResult[CurGame];
}
else if (ExpectedResult.Contains(TEXT("NullUnitEnv")))
{
Result = ExpectedResult[TEXT("NullUnitEnv")];
}
return Result;
}
@@ -305,7 +315,7 @@ public:
/**
* Executes the main unit test
*
* @return Whether or not the unit test was executed successfully
* @return Whether or not the unit test kicked off execution successfully
*/
virtual bool ExecuteUnitTest() PURE_VIRTUAL(UUnitTest::ExecuteUnitTest, return false;)

View File

@@ -14,7 +14,20 @@
* "UE4Editor.exe shootergame -run=NetcodeUnitTest.UnitTestCommandlet"
*
* Parameters:
* "-UnitTest=UnitTestName" - Launches only the specified unit test
* -UnitTest=UnitTestName
* - Launches only the specified unit test
*
* -UnitTestNoAutoClose
* - Sets the default option for the 'AutoClose' button to False
*
* -UnitTestServerParms="CommandlineParameters"
* - Adds additional commandline parameters to unit test server instances (useful for e.g. unsuppressing specific logs)
*
* -UnitTestClientParms="CommandlineParameters"
* - Adds additional commandline parameters to unit test client instances
*
* -UnitTestCap=x
* - Caps the maximum number of unit tests that can run at the same time
*/
UCLASS()
class UUnitTestCommandlet : public UCommandlet

View File

@@ -262,6 +262,11 @@ public:
* FOutputDevice methods
*/
// We're hiding UObject::Serialize() by declaring this. That's OK, but Clang will warn about it.
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Woverloaded-virtual"
#endif
/**
* Write text to the console
*
@@ -269,10 +274,6 @@ public:
* @param Verbosity The log level verbosity for the message
* @param Category The log category for the message
*/
#if defined(__clang__) // We're hiding UObject::Serialize() by declaring this. That's OK, but Clang will warn about it.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Woverloaded-virtual"
#endif
virtual void Serialize(const TCHAR* Data, ELogVerbosity::Type Verbosity, const class FName& Category) override;
#if defined(__clang__)
#pragma clang diagnostic pop

View File

@@ -0,0 +1,166 @@
// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
#pragma once
#include "UnitTest.h"
#include "VMReflection.generated.h"
/**
* Internal unit test for verifying the functionality of the UScript/BP VM reflection helper
*/
UCLASS()
class UVMReflection : public UUnitTest
{
GENERATED_UCLASS_BODY()
public:
virtual bool ExecuteUnitTest() override;
};
/**
* Test classes for testing different types/combinations of property reflection
*/
UCLASS()
class UVMTestClassA : public UObject
{
GENERATED_UCLASS_BODY()
public:
UPROPERTY()
UObject* AObjectRef;
UPROPERTY()
uint8 ByteProp;
UPROPERTY()
uint16 UInt16Prop;
UPROPERTY()
uint32 UInt32Prop;
UPROPERTY()
uint64 UInt64Prop;
UPROPERTY()
int8 Int8Prop;
UPROPERTY()
int16 Int16Prop;
UPROPERTY()
int32 Int32Prop;
UPROPERTY()
int64 Int64Prop;
UPROPERTY()
float FloatProp;
UPROPERTY()
double DoubleProp;
UPROPERTY()
bool bBoolPropA;
UPROPERTY()
bool bBoolPropB;
UPROPERTY()
bool bBoolPropC;
UPROPERTY()
bool bBoolPropD;
UPROPERTY()
bool bBoolPropE;
UPROPERTY()
FName NameProp;
UPROPERTY()
FString StringProp;
UPROPERTY()
FText TextProp;
UPROPERTY()
uint8 BytePropArray[4];
UPROPERTY()
UObject* ObjectPropArray[2];
UPROPERTY()
TArray<uint8> DynBytePropArray;
UPROPERTY()
TArray<bool> DynBoolPropArray;
UPROPERTY()
TArray<UObject*> DynObjectPropArray;
UPROPERTY()
TArray<FName> DynNamePropArray;
UPROPERTY()
TArray<double> DynDoublePropArray;
UPROPERTY()
TArray<float> DynFloatPropArray;
UPROPERTY()
TArray<int16> DynInt16PropArray;
UPROPERTY()
TArray<int64> DynInt64PropArray;
UPROPERTY()
TArray<int8> DynInt8PropArray;
UPROPERTY()
TArray<int32> DynIntPropArray;
UPROPERTY()
TArray<uint16> DynUInt16PropArray;
UPROPERTY()
TArray<uint32> DynUIntPropArray;
UPROPERTY()
TArray<uint64> DynUInt64PropArray;
UPROPERTY()
TArray<FString> DynStringPropArray;
UPROPERTY()
TArray<FText> DynTextPropArray;
UPROPERTY()
TArray<UClass*> DynClassPropArray;
UPROPERTY()
TArray<APawn*> DynPawnPropArray;
UPROPERTY()
FVector StructProp;
UPROPERTY()
FVector StructPropArray[2];
UPROPERTY()
TArray<FVector> DynStructPropArray;
};
UCLASS()
class UVMTestClassB : public UObject
{
GENERATED_UCLASS_BODY()
public:
UPROPERTY()
UObject* BObjectRef;
};

View File

@@ -14,6 +14,7 @@
#include "UnitTestEnvironment.h"
#include "Net/NUTUtilNet.h"
#include "NUTUtilDebug.h"
#include "NUTUtilReflection.h"
#include "LogWindowManager.h"
#include "SLogWindow.h"
@@ -35,6 +36,8 @@ UClientUnitTest::UClientUnitTest(const FObjectInitializer& ObjectInitializer)
, BaseServerParameters(TEXT(""))
, BaseClientURL(TEXT(""))
, BaseClientParameters(TEXT(""))
, AllowedClientActors()
, AllowedClientRPCs()
, ActiveProcesses()
, ServerHandle(NULL)
, ServerAddress(TEXT(""))
@@ -105,7 +108,7 @@ void UClientUnitTest::NotifyHandleClientPlayer(APlayerController* PC, UNetConnec
}
}
bool UClientUnitTest::NotifyAllowNetActor(UClass* ActorClass)
bool UClientUnitTest::NotifyAllowNetActor(UClass* ActorClass, bool bActorChannel)
{
bool bAllow = false;
@@ -126,6 +129,24 @@ bool UClientUnitTest::NotifyAllowNetActor(UClass* ActorClass)
bAllow = true;
}
if (!!(UnitTestFlags & EUnitTestFlags::RequireBeacon) && ActorClass->IsChildOf(AOnlineBeaconClient::StaticClass()) &&
UnitBeacon == NULL)
{
bAllow = true;
}
if (!bAllow && AllowedClientActors.Num() > 0)
{
const auto CheckIsChildOf =
[&](const UClass* CurEntry)
{
return ActorClass->IsChildOf(CurEntry);
};
// Use 'ContainsByPredicate' as iterator
bAllow = AllowedClientActors.ContainsByPredicate(CheckIsChildOf);
}
return bAllow;
}
@@ -133,14 +154,18 @@ void UClientUnitTest::NotifyNetActor(UActorChannel* ActorChannel, AActor* Actor)
{
if (!UnitNUTActor.IsValid())
{
// Set this even if not required, as it's needed for some UI elements to function
UnitNUTActor = Cast<ANUTActor>(Actor);
ResetTimeout(TEXT("NotifyNetActor - UnitNUTActor"));
if (UnitNUTActor.IsValid() && !!(UnitTestFlags & EUnitTestFlags::RequireNUTActor) && HasAllRequirements())
if (UnitNUTActor.IsValid())
{
ResetTimeout(TEXT("ExecuteClientUnitTest (NotifyNetActor - UnitNUTActor)"));
ExecuteClientUnitTest();
ResetTimeout(TEXT("NotifyNetActor - UnitNUTActor"));
if (!!(UnitTestFlags & EUnitTestFlags::RequireNUTActor) && HasAllRequirements())
{
ResetTimeout(TEXT("ExecuteClientUnitTest (NotifyNetActor - UnitNUTActor)"));
ExecuteClientUnitTest();
}
}
}
@@ -273,26 +298,26 @@ void UClientUnitTest::ReceivedControlBunch(FInBunch& Bunch)
}
#if !UE_BUILD_SHIPPING
bool UClientUnitTest::NotifyScriptProcessEvent(AActor* Actor, UFunction* Function, void* Parameters, void* HookOrigin)
bool UClientUnitTest::NotifyScriptProcessEvent(AActor* Actor, UFunction* Function, void* Parameters)
{
bool bBlockEvent = false;
// Handle UnitTestFlags that require RPC monitoring
if (!(UnitTestFlags & EUnitTestFlags::AcceptRPCs) || !!(UnitTestFlags & EUnitTestFlags::DumpReceivedRPC) ||
if (AllowedClientRPCs.Num() > 0 || !(UnitTestFlags & EUnitTestFlags::AcceptRPCs) || !!(UnitTestFlags & EUnitTestFlags::DumpReceivedRPC) ||
!!(UnitTestFlags & EUnitTestFlags::RequirePawn))
{
bool bNetClientRPC = !!(Function->FunctionFlags & FUNC_Net) && !!(Function->FunctionFlags & FUNC_NetClient);
if (bNetClientRPC)
{
FString FuncName = Function->GetName();
// Whether or not to force acceptance of this RPC
bool bForceAccept = false;
// Handle detection and proper setup of the PlayerController's pawn
if (!!(UnitTestFlags & EUnitTestFlags::RequirePawn) && !bUnitPawnSetup && UnitPC != NULL)
{
FString FuncName = Function->GetName();
if (FuncName == TEXT("ClientRestart"))
{
UNIT_LOG(ELogType::StatusImportant, TEXT("Got ClientRestart"));
@@ -337,17 +362,33 @@ bool UClientUnitTest::NotifyScriptProcessEvent(AActor* Actor, UFunction* Functio
}
bForceAccept = bForceAccept && AllowedClientRPCs.Contains(FuncName);
// Block RPC's, if they are not accepted
if (!(UnitTestFlags & EUnitTestFlags::AcceptRPCs) && !bForceAccept)
{
UNIT_LOG(, TEXT("Blocking receive RPC '%s' for actor '%s'"), *Function->GetName(), *Actor->GetFullName());
FString FuncParms = NUTUtilRefl::FunctionParmsToString(Function, Parameters);
UNIT_LOG(, TEXT("Blocking receive RPC '%s' for actor '%s'"), *FuncName, *Actor->GetFullName());
if (FuncParms.Len() > 0)
{
UNIT_LOG(, TEXT(" '%s' parameters: %s"), *FuncName, *FuncParms);
}
bBlockEvent = true;
}
if (!!(UnitTestFlags & EUnitTestFlags::DumpReceivedRPC) && !bBlockEvent)
{
UNIT_LOG(ELogType::StatusDebug, TEXT("Received RPC '%s' for actor '%s'"), *Function->GetName(), *Actor->GetFullName());
FString FuncParms = NUTUtilRefl::FunctionParmsToString(Function, Parameters);
UNIT_LOG(ELogType::StatusDebug, TEXT("Received RPC '%s' for actor '%s'"), *FuncName, *Actor->GetFullName());
if (FuncParms.Len() > 0)
{
UNIT_LOG(, TEXT(" '%s' parameters: %s"), *FuncName, *FuncParms);
}
}
}
}
@@ -383,36 +424,37 @@ void UClientUnitTest::NotifyProcessLog(TWeakPtr<FUnitTestProcess> InProcess, con
const TArray<FString>* ServerStartProgressLogs = NULL;
const TArray<FString>* ServerReadyLogs = NULL;
const TArray<FString>* ServerTimeoutResetLogs = NULL;
const TArray<FString>* ClientTimeoutResetLogs = NULL;
UnitEnv->GetServerProgressLogs(ServerStartProgressLogs, ServerReadyLogs, ServerTimeoutResetLogs);
UnitEnv->GetClientProgressLogs(ClientTimeoutResetLogs);
// Using 'ContainsByPredicate' as an iterator
FString MatchedLine;
const auto SearchInLogLine =
[&](const FString& ProgressLine)
{
bool bFound = false;
for (auto CurLine : InLogLines)
{
if (CurLine.Contains(ProgressLine))
{
MatchedLine = CurLine;
bFound = true;
break;
}
}
return bFound;
};
// If launching a server, delay joining by the fake client, until the server has fully setup, and reset the unit test timeout,
// each time there is a server log event, that indicates progress in starting up
if (!!(UnitTestFlags & EUnitTestFlags::LaunchServer) && ServerHandle.IsValid() && InProcess.HasSameObject(ServerHandle.Pin().Get()))
{
// Using 'ContainsByPredicate' as an iterator
FString MatchedLine;
const auto SearchInLogLine =
[&](const FString& ProgressLine)
{
bool bFound = false;
for (auto CurLine : InLogLines)
{
if (CurLine.Contains(ProgressLine))
{
MatchedLine = CurLine;
bFound = true;
break;
}
}
return bFound;
};
if (UnitConn == NULL || UnitConn->State == EConnectionState::USOCK_Pending)
{
if (ServerReadyLogs->ContainsByPredicate(SearchInLogLine))
@@ -445,7 +487,16 @@ void UClientUnitTest::NotifyProcessLog(TWeakPtr<FUnitTestProcess> InProcess, con
}
}
// @todo JohnB: Consider adding a progress-checker for launched clients as well
if (!!(UnitTestFlags & EUnitTestFlags::LaunchClient) && ClientHandle.IsValid() && InProcess.HasSameObject(ClientHandle.Pin().Get()))
{
if (ClientTimeoutResetLogs->Num() > 0)
{
if (ClientTimeoutResetLogs->ContainsByPredicate(SearchInLogLine))
{
ResetTimeout(FString(TEXT("ClientTimeoutReset: ")) + MatchedLine, true);
}
}
}
// @todo JohnB: Consider also, adding a way to communicate with launched clients,
// to reset their connection timeout upon server progress, if they fully startup before the server does
@@ -568,12 +619,19 @@ bool UClientUnitTest::NotifyConsoleCommandRequest(FString CommandContext, FStrin
}
else
{
UNIT_LOG(, TEXT("Can't execute command '%s', it's in the 'bad commands' list (i.e. probably crashes)"), *Command);
UNIT_LOG(ELogType::OriginConsole,
TEXT("Can't execute command '%s', it's in the 'bad commands' list (i.e. probably crashes)"), *Command);
}
}
else if (CommandContext == TEXT("Server"))
{
// @todo JohnB: Perhaps add extra checks here, to be sure we're ready to send console commands?
//
// UPDATE: Yes, this is a good idea, because if the client hasn't gotten to the correct login stage
// (NMT_Join or such, need to check when server rejects non-login control commands),
// then it leads to an early disconnect when you try to spam-send a command early.
//
// It's easy to test this, just type in a command before join, and hold down enter on the edit box to spam it.
if (UnitConn != NULL)
{
FOutBunch* ControlChanBunch = NUTNet::CreateChannelBunch(ControlBunchSequence, UnitConn, CHTYPE_Control, 0);
@@ -589,25 +647,25 @@ bool UClientUnitTest::NotifyConsoleCommandRequest(FString CommandContext, FStrin
NUTNet::SendControlBunch(UnitConn, *ControlChanBunch);
UNIT_LOG(, TEXT("Sent command '%s' to server."), *Command);
UNIT_LOG(ELogType::OriginConsole, TEXT("Sent command '%s' to server."), *Command);
bHandled = true;
}
else
{
UNIT_LOG(, TEXT("Failed to send console command '%s', no server connection."), *Command);
UNIT_LOG(ELogType::OriginConsole, TEXT("Failed to send console command '%s', no server connection."), *Command);
}
}
else if (CommandContext == TEXT("Client"))
{
// @todo JohnB
UNIT_LOG(, TEXT("Client console commands not yet implemented"));
UNIT_LOG(ELogType::OriginConsole, TEXT("Client console commands not yet implemented"));
}
else
{
UNIT_LOG(ELogType::StatusFailure, TEXT("Unknown console command context '%s' - this is a bug that should be fixed."),
*CommandContext);
UNIT_LOG(ELogType::StatusFailure | ELogType::OriginConsole,
TEXT("Unknown console command context '%s' - this is a bug that should be fixed."), *CommandContext);
}
return bHandled;
@@ -632,7 +690,8 @@ void UClientUnitTest::GetCommandContextList(TArray<TSharedPtr<FString>>& OutList
}
bool UClientUnitTest::SendRPCChecked(AActor* Target, const TCHAR* FunctionName, void* Parms)
bool UClientUnitTest::SendRPCChecked(AActor* Target, const TCHAR* FunctionName, void* Parms, int16 ParmsSize,
int16 ParmsSizeCorrection/*=0*/)
{
bool bSuccess = false;
UFunction* TargetFunc = Target->FindFunction(FName(FunctionName));
@@ -641,7 +700,15 @@ bool UClientUnitTest::SendRPCChecked(AActor* Target, const TCHAR* FunctionName,
if (TargetFunc != NULL)
{
Target->ProcessEvent(TargetFunc, Parms);
if (TargetFunc->ParmsSize == ParmsSize + ParmsSizeCorrection)
{
Target->ProcessEvent(TargetFunc, Parms);
}
else
{
UNIT_LOG(ELogType::StatusFailure, TEXT("Failed to send RPC '%s', mismatched parameters: '%i' vs '%i' (%i - %i)."),
FunctionName, TargetFunc->ParmsSize, ParmsSize + ParmsSizeCorrection, ParmsSize, -ParmsSizeCorrection);
}
}
bSuccess = PostSendRPC(FunctionName, Target);
@@ -698,6 +765,7 @@ bool UClientUnitTest::PostSendRPC(FString RPCName, AActor* Target/*=NULL*/)
}
// @todo JohnB: This should be changed to work as compile-time conditionals, if possible
bool UClientUnitTest::ValidateUnitTestSettings(bool bCDOCheck/*=false*/)
{
bool bSuccess = Super::ValidateUnitTestSettings();
@@ -712,6 +780,9 @@ bool UClientUnitTest::ValidateUnitTestSettings(bool bCDOCheck/*=false*/)
UNIT_ASSERT(!(UnitTestFlags & EUnitTestFlags::LaunchClient) || BaseClientParameters.Len() > 0);
// You can't specify an allowed actors whitelist, without the AcceptActors flag
UNIT_ASSERT(AllowedClientActors.Num() == 0 || !!(UnitTestFlags & EUnitTestFlags::AcceptActors));
// If you require a player/NUTActor, you need to accept actor channels
UNIT_ASSERT((!(UnitTestFlags & EUnitTestFlags::AcceptPlayerController) && !(UnitTestFlags & EUnitTestFlags::RequireNUTActor)) ||
!!(UnitTestFlags & EUnitTestFlags::AcceptActors));
@@ -723,9 +794,23 @@ bool UClientUnitTest::ValidateUnitTestSettings(bool bCDOCheck/*=false*/)
// If you require a pawn, you must require a PlayerController
UNIT_ASSERT(!(UnitTestFlags & EUnitTestFlags::RequirePawn) || !!(UnitTestFlags & EUnitTestFlags::RequirePlayerController));
// If you require a pawn, you must enable NotifyProcessEvent
UNIT_ASSERT(!(UnitTestFlags & EUnitTestFlags::RequirePawn) || !!(UnitTestFlags & EUnitTestFlags::NotifyProcessEvent));
// If you want to dump received RPC's, you need to hook NotifyProcessEvent
UNIT_ASSERT(!(UnitTestFlags & EUnitTestFlags::DumpReceivedRPC) || !!(UnitTestFlags & EUnitTestFlags::NotifyProcessEvent));
// You can't whitelist client RPC's (i.e. unblock whitelisted RPC's), unless all RPC's are blocked by default
UNIT_ASSERT(!(UnitTestFlags & EUnitTestFlags::AcceptRPCs) || AllowedClientRPCs.Num() == 0);
#if UE_BUILD_SHIPPING
// You can't detect pawns in shipping builds, as you need to hook ProcessEvent for RPC notifications
UNIT_ASSERT(!(UnitTestFlags & EUnitTestFlags::RequirePawn));
// You can't hook ProcessEvent or block RPCs in shipping builds, as the main engine hook is not available in shipping; soft-fail
if (!!(UnitTestFlags & EUnitTestFlags::NotifyProcessEvent) || !(UnitTestFlags & EUnitTestFlags::AcceptRPCs))
{
UNIT_LOG(ELogType::StatusFailure | ELogType::StyleBold, TEXT("Unit tests run in shipping mode, can't hook ProcessEvent."));
bSuccess = false;
}
#endif
// If you require a pawn, validate the existence of certain RPC's that are needed for pawn setup and verification
@@ -768,16 +853,6 @@ bool UClientUnitTest::ValidateUnitTestSettings(bool bCDOCheck/*=false*/)
// Don't require a beacon, if you're not connecting to a beacon
UNIT_ASSERT(!(UnitTestFlags & EUnitTestFlags::RequireBeacon) || !!(UnitTestFlags & EUnitTestFlags::BeaconConnect));
// In shipping builds, you MUST accept RPC's, as there is no way to filter them out (can't hook ProcessEvent); soft-fail
#if UE_BUILD_SHIPPING
if (!(UnitTestFlags & EUnitTestFlags::AcceptRPCs))
{
UNIT_LOG(ELogType::StatusFailure | ELogType::StyleBold, TEXT("Unit tests run in shipping mode, must accept RPC's"));
bSuccess = false;
}
#endif
// Don't specify server-dependent flags, if not auto-launching a server
UNIT_ASSERT(!(UnitTestFlags & EUnitTestFlags::LaunchClient) || !!(UnitTestFlags & EUnitTestFlags::LaunchServer));
@@ -787,9 +862,6 @@ bool UClientUnitTest::ValidateUnitTestSettings(bool bCDOCheck/*=false*/)
// If DumpSendRaw is set, make sure hooking of SendRawPacket is enabled too
UNIT_ASSERT(!(UnitTestFlags & EUnitTestFlags::DumpSendRaw) || !!(UnitTestFlags & EUnitTestFlags::CaptureSendRaw));
// You can't get net actor allow notifications, unless you accept actors
UNIT_ASSERT(!(UnitTestFlags & EUnitTestFlags::NotifyAllowNetActor) || !!(UnitTestFlags & EUnitTestFlags::AcceptActors));
// You can't get net actor notifications, unless you accept actors
UNIT_ASSERT(!(UnitTestFlags & EUnitTestFlags::NotifyNetActors) || !!(UnitTestFlags & EUnitTestFlags::AcceptActors));
@@ -842,7 +914,7 @@ EUnitTestFlags UClientUnitTest::GetMetRequirements()
return ReturnVal;
}
bool UClientUnitTest::HasAllRequirements()
bool UClientUnitTest::HasAllRequirements(bool bIgnoreCustom/*=false*/)
{
bool bReturnVal = true;
@@ -854,6 +926,11 @@ bool UClientUnitTest::HasAllRequirements()
EUnitTestFlags RequiredFlags = (UnitTestFlags & EUnitTestFlags::RequirementsMask);
if (bIgnoreCustom)
{
RequiredFlags &= ~EUnitTestFlags::RequireCustom;
}
if ((RequiredFlags & GetMetRequirements()) != RequiredFlags)
{
bReturnVal = false;
@@ -990,7 +1067,7 @@ bool UClientUnitTest::ConnectFakeClient(FUniqueNetIdRepl* InNetID/*=NULL*/)
bool bFailedScriptHook = false;
if (!(UnitTestFlags & EUnitTestFlags::AcceptRPCs))
if (!(UnitTestFlags & EUnitTestFlags::AcceptRPCs) || !!(UnitTestFlags & EUnitTestFlags::NotifyProcessEvent))
{
#if !UE_BUILD_SHIPPING
AddProcessEventCallback(this, &UClientUnitTest::InternalScriptProcessEvent);
@@ -1049,10 +1126,7 @@ bool UClientUnitTest::ConnectFakeClient(FUniqueNetIdRepl* InNetID/*=NULL*/)
CurUnitConn->LowLevelSendDel.BindUObject(this, &UClientUnitTest::NotifySendRawPacket);
}
if (!!(UnitTestFlags & EUnitTestFlags::NotifyAllowNetActor))
{
CurUnitConn->ActorChannelSpawnDel.BindUObject(this, &UClientUnitTest::NotifyAllowNetActor);
}
CurUnitConn->ReplicatedActorSpawnDel.BindUObject(this, &UClientUnitTest::NotifyAllowNetActor);
// If you don't have to wait for any requirements, execute immediately
if (!(UnitTestFlags & (EUnitTestFlags::RequirementsMask)))
@@ -1296,10 +1370,11 @@ void UClientUnitTest::ShutdownUnitTestProcess(TSharedPtr<FUnitTestProcess> InHan
FString LogMsg = FString::Printf(TEXT("Shutting down process '%s'."), *InHandle->ProcessTag);
UNIT_LOG(ELogType::StatusImportant, TEXT("%s"), *LogMsg);
UNIT_STATUS_LOG(ELogType::StatusVerbose, TEXT("%s"), *LogMsg)
UNIT_STATUS_LOG(ELogType::StatusVerbose, TEXT("%s"), *LogMsg);
FPlatformProcess::TerminateProc(InHandle->ProcessHandle, true);
// @todo JohnB: Restore 'true' here, once the issue where killing child processes sometimes kills all processes, is fixed
FPlatformProcess::TerminateProc(InHandle->ProcessHandle);//, true);
#if TARGET_UE4_CL < CL_CLOSEPROC
InHandle->ProcessHandle.Close();
@@ -1463,8 +1538,15 @@ void UClientUnitTest::CheckOutputForError(TSharedPtr<FUnitTestProcess> InProcess
if (InProcess->ErrorLogStage != EErrorLogStage::ELS_NoError)
{
// Regex pattern for matching callstack logs - matches: " (0x000007fefe22cacd) + 0 bytes ["
const FRegexPattern CallstackPattern(TEXT("\\s\\(0x[0-9,a-f]+\\) \\+ [0-9]+ bytes \\["));
// Regex pattern for matching callstack logs - matches:
// " (0x000007fefe22cacd) + 0 bytes"
// " {0x000007fefe22cacd} + 0 bytes"
const FRegexPattern CallstackPattern(TEXT("\\s[\\(|\\{]0x[0-9,a-f]+[\\)|\\}] \\+ [0-9]+ bytes"));
// Matches:
// "ntldll.dll"
const FRegexPattern AltCallstackPattern(TEXT("^[a-z,A-Z,0-9,\\-,_]+\\.[exe|dll]"));
// Check for the beginning of description logs
if (InProcess->ErrorLogStage == EErrorLogStage::ELS_ErrorStart &&
@@ -1478,8 +1560,9 @@ void UClientUnitTest::CheckOutputForError(TSharedPtr<FUnitTestProcess> InProcess
InProcess->ErrorLogStage == EErrorLogStage::ELS_ErrorCallstack)
{
FRegexMatcher CallstackMatcher(CallstackPattern, CurLine);
FRegexMatcher AltCallstackMatcher(AltCallstackPattern, CurLine);
if (CallstackMatcher.FindNext())
if (CallstackMatcher.FindNext() || AltCallstackMatcher.FindNext())
{
InProcess->ErrorLogStage = EErrorLogStage::ELS_ErrorCallstack;
}
@@ -1539,7 +1622,7 @@ bool UClientUnitTest::InternalScriptProcessEvent(AActor* Actor, UFunction* Funct
{
UNIT_EVENT_BEGIN(OriginUnitTest);
bBlockEvent = OriginUnitTest->NotifyScriptProcessEvent(Actor, Function, Parameters, HookOrigin);
bBlockEvent = OriginUnitTest->NotifyScriptProcessEvent(Actor, Function, Parameters);
UNIT_EVENT_END;
}

View File

@@ -8,13 +8,18 @@
#include "NUTUtilNet.h"
#include "NUTUtil.h"
#if TARGET_UE4_CL < CL_BEACONHOST
const FName NAME_BeaconDriver = FName(TEXT("BeaconDriver"));
#else
const FName NAME_BeaconDriver = FName(TEXT("BeaconNetDriver"));
#endif
IMPLEMENT_CONTROL_CHANNEL_MESSAGE(NUTControl);
ANUTActor::ANUTActor(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
, BeaconDriverName(NAME_None)
, LastAliveTime(0.f)
, bMonitorForBeacon(false)
{
@@ -46,7 +51,11 @@ void ANUTActor::PostActorCreated()
}
}
#if TARGET_UE4_CL < CL_CONSTNETCONN
UNetConnection* ANUTActor::GetNetConnection()
#else
UNetConnection* ANUTActor::GetNetConnection() const
#endif
{
UNetConnection* ReturnVal = Super::GetNetConnection();
@@ -62,7 +71,7 @@ UNetConnection* ANUTActor::GetNetConnection() const
}
// If this is the server, set based on beacon driver client connection.
// Only the server has a net driver of name NAME_BeaconDriver (clientside the name is chosen dynamically)
else if (NetDriver != NULL && NetDriver->NetDriverName == NAME_BeaconDriver && NetDriver->ClientConnections.Num() > 0)
else if (NetDriver != NULL && NetDriver->NetDriverName == BeaconDriverName && NetDriver->ClientConnections.Num() > 0)
{
ReturnVal = NetDriver->ClientConnections[0];
}
@@ -174,7 +183,7 @@ bool ANUTActor::NotifyControlMessage(UNetConnection* Connection, uint8 MessageTy
{
UE_LOG(LogUnitTest, Log, TEXT("Successfully summoned actor of class '%s'"), *SpawnClassName);
if (bForceBeginPlay)
if (bForceBeginPlay && !NewActor->HasActorBegunPlay())
{
UE_LOG(LogUnitTest, Log, TEXT("Forcing call to 'BeginPlay' on newly spawned actor."));
@@ -371,7 +380,26 @@ void ANUTActor::Tick(float DeltaSeconds)
// Monitor for the beacon net driver, so it can be hooked
if (bMonitorForBeacon)
{
#if TARGET_UE4_CL < CL_BEACONHOST
UNetDriver* BeaconDriver = GEngine->FindNamedNetDriver(CurWorld, NAME_BeaconDriver);
#else
// Somehow, the beacon driver name got messed up in a subsequent checkin, so now has to be found manually
UNetDriver* BeaconDriver = NULL;
FWorldContext* CurContext = GEngine->GetWorldContextFromWorld(CurWorld);
if (CurContext != NULL)
{
for (auto CurDriverRef : CurContext->ActiveNetDrivers)
{
if (CurDriverRef.NetDriverDef->DefName == NAME_BeaconDriver)
{
BeaconDriver = CurDriverRef.NetDriver;
break;
}
}
}
#endif
// Only hook when a client is connected
if (BeaconDriver != NULL && BeaconDriver->ClientConnections.Num() > 0)
@@ -385,7 +413,8 @@ void ANUTActor::Tick(float DeltaSeconds)
Role = ROLE_None;
SetReplicates(false);
NetDriverName = NAME_BeaconDriver;
BeaconDriverName = BeaconDriver->NetDriverName;
NetDriverName = BeaconDriverName;
Role = ROLE_Authority;
SetReplicates(true);

View File

@@ -11,6 +11,14 @@
#include "Net/NUTUtilNet.h"
/**
* Globals
*/
FStackTraceManager GTraceManager;
FLogStackTraceManager GLogTraceManager;
/**
* FScopedLog
*/
@@ -124,10 +132,10 @@ FScopedLog::~FScopedLog()
#if !UE_BUILD_SHIPPING
/**
* FScopedProcessEventLog
* FProcessEventHookBase
*/
FScopedProcessEventLog::FScopedProcessEventLog()
FProcessEventHookBase::FProcessEventHookBase()
: OrigEventHook()
{
if (AActor::ProcessEventDelegate.IsBound())
@@ -135,35 +143,242 @@ FScopedProcessEventLog::FScopedProcessEventLog()
OrigEventHook = AActor::ProcessEventDelegate;
}
AActor::ProcessEventDelegate.BindRaw(this, &FScopedProcessEventLog::ProcessEventHook);
AActor::ProcessEventDelegate.BindRaw(this, &FProcessEventHookBase::InternalProcessEventHook);
}
FScopedProcessEventLog::~FScopedProcessEventLog()
FProcessEventHookBase::~FProcessEventHookBase()
{
AActor::ProcessEventDelegate = OrigEventHook;
OrigEventHook.Unbind();
}
bool FScopedProcessEventLog::ProcessEventHook(AActor* Actor, UFunction* Function, void* Parameters)
bool FProcessEventHookBase::InternalProcessEventHook(AActor* Actor, UFunction* Function, void* Parameters)
{
bool bReturnVal = false;
UE_LOG(LogUnitTest, Log, TEXT("FScopedProcessEventLog: Actor: %s, Function: %s"),
(Actor != NULL ? *Actor->GetName() : TEXT("NULL")),
*Function->GetName());
// If there was originally already a ProcessEvent hook in place, transparently pass on the event, so it's not disrupted
if (OrigEventHook.IsBound())
{
bReturnVal = OrigEventHook.Execute(Actor, Function, Parameters);
}
ProcessEventHook(Actor, Function, Parameters);
return bReturnVal;
}
void FScopedProcessEventLog::ProcessEventHook(AActor* Actor, UFunction* Function, void* Parameters)
{
UE_LOG(LogUnitTest, Log, TEXT("FScopedProcessEventLog: Actor: %s, Function: %s"),
(Actor != NULL ? *Actor->GetName() : TEXT("NULL")),
*Function->GetName());
}
#endif
/**
* FNUTStackTrace
*/
FNUTStackTrace::FNUTStackTrace(FString InTraceName)
: TraceName(InTraceName)
, Tracker()
{
Tracker.ResetTracking();
}
FNUTStackTrace::~FNUTStackTrace()
{
Tracker.ResetTracking();
}
void FNUTStackTrace::Enable()
{
if (!IsTrackingEnabled())
{
Tracker.ToggleTracking();
}
}
void FNUTStackTrace::Disable()
{
if (IsTrackingEnabled())
{
Tracker.ToggleTracking();
}
}
void FNUTStackTrace::AddTrace(bool bLogAdd/*=false*/)
{
if (IsTrackingEnabled())
{
if (bLogAdd)
{
UE_LOG(LogUnitTest, Log, TEXT("Adding stack trace for TraceName '%s'."), *TraceName);
}
Tracker.CaptureStackTrace(TRACE_IGNORE_DEPTH);
}
}
void FNUTStackTrace::Dump(bool bKeepTraceHistory/*=false*/)
{
UE_LOG(LogUnitTest, Log, TEXT("Dumping tracked stack traces for TraceName '%s':"), *TraceName);
Tracker.DumpStackTraces(0, *GLog);
if (!bKeepTraceHistory)
{
Tracker.ResetTracking();
}
}
/**
* FStackTraceManager
*/
FStackTraceManager::FStackTraceManager()
: Traces()
{
}
FStackTraceManager::~FStackTraceManager()
{
for (auto It=Traces.CreateConstIterator(); It; ++It)
{
FNUTStackTrace* Trace = It->Value;
if (Trace != NULL)
{
delete Trace;
}
}
Traces.Empty();
}
void FStackTraceManager::Enable(FString TraceName)
{
FNUTStackTrace* Trace = GetOrCreateTrace(TraceName);
Trace->Enable();
}
void FStackTraceManager::Disable(FString TraceName)
{
FNUTStackTrace* Trace = GetTrace(TraceName);
if (Trace != NULL)
{
Trace->Disable();
}
else
{
UE_LOG(LogUnitTest, Log, TEXT("Trace disable: No trace tracking found for TraceName '%s'."), *TraceName);
}
}
void FStackTraceManager::AddTrace(FString TraceName, bool bLogAdd/*=false*/, bool bDump/*=false*/, bool bStartDisabled/*=false*/)
{
bool bIsNewTrace = false;
FNUTStackTrace* Trace = GetOrCreateTrace(TraceName, &bIsNewTrace);
if (bIsNewTrace)
{
if (bStartDisabled)
{
Trace->Disable();
}
else
{
Trace->Enable();
}
}
if (Trace->IsTrackingEnabled())
{
Trace->AddTrace(bLogAdd);
if (bDump)
{
Trace->Dump(true);
}
}
}
void FStackTraceManager::Dump(FString TraceName, bool bKeepTraceHistory/*=false*/, bool bKeepTracking/*=true*/)
{
FNUTStackTrace* Trace = GetTrace(TraceName);
if (Trace != NULL)
{
Trace->Dump(bKeepTraceHistory);
if (!bKeepTracking)
{
delete Trace;
Traces.FindAndRemoveChecked(TraceName);
}
}
else
{
UE_LOG(LogUnitTest, Log, TEXT("No trace tracking found for TraceName '%s'."), *TraceName);
}
}
void FStackTraceManager::Clear(FString TraceName)
{
FNUTStackTrace* Trace = GetTrace(TraceName);
if (Trace != NULL)
{
delete Trace;
Traces.FindAndRemoveChecked(TraceName);
}
else
{
UE_LOG(LogUnitTest, Log, TEXT("No trace tracking found for TraceName '%s'."), *TraceName);
}
}
void FStackTraceManager::DumpAll(bool bKeepTraceHistory/*=false*/, bool bKeepTracking/*=true*/)
{
UE_LOG(LogUnitTest, Log, TEXT("Dumping all tracked stack traces:"));
for (auto It=Traces.CreateIterator(); It; ++It)
{
FNUTStackTrace* Trace = It->Value;
if (Trace != NULL)
{
Trace->Dump(bKeepTraceHistory);
if (!bKeepTracking)
{
delete Trace;
It.RemoveCurrent();
}
}
}
}
void FStackTraceManager::TraceAndDump(FString TraceName)
{
FNUTStackTrace* Trace = GetTrace(TraceName);
if (Trace == NULL || Trace->IsTrackingEnabled())
{
UE_LOG(LogUnitTest, Log, TEXT("Dumping once-off stack trace for TraceName '%s':"), *TraceName);
FStackTracker TempTracker(NULL, NULL, true);
TempTracker.CaptureStackTrace(TRACE_IGNORE_DEPTH);
TempTracker.DumpStackTraces(0, *GLog);
TempTracker.ResetTracking();
}
}
/**
* NUTDebug
*/
@@ -271,3 +486,4 @@ FString NUTDebug::HexDump(const TArray<uint8>& InBytes, bool bDumpASCII/*=true*/
return ReturnValue;
}

View File

@@ -405,7 +405,12 @@ bool NUTNet::CreateFakePlayer(UWorld* InWorld, UNetDriver*& InNetDriver, FString
else
{
// Then send NMT_Login
#if TARGET_UE4_CL < CL_CONSTUNIQUEID
TSharedPtr<FUniqueNetId> DudPtr = MakeShareable(new FUniqueNetIdString(TEXT("Dud")));
#else
TSharedPtr<const FUniqueNetId> DudPtr = MakeShareable(new FUniqueNetIdString(TEXT("Dud")));
#endif
FUniqueNetIdRepl PlayerUID(DudPtr);
FString BlankStr = TEXT("");
FString ConnectURL = UUnitTest::UnitEnv->GetDefaultClientConnectURL();

View File

@@ -16,6 +16,7 @@
UUnitTestPackageMap::UUnitTestPackageMap(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
, bWithinSerializeNewActor(false)
, bPendingArchetypeSpawn(false)
{
}
@@ -25,24 +26,41 @@ bool UUnitTestPackageMap::SerializeObject(FArchive& Ar, UClass* Class, UObject*&
bReturnVal = Super::SerializeObject(Ar, Class, Obj, OutNetGUID);
// This indicates that the new actor channel archetype has just been serialized;
// this is the first place we know what actor type the actor channel is initializing, but BEFORE it is spawned
if (GIsInitializingActorChan && bWithinSerializeNewActor && Class == UObject::StaticClass() && Obj != NULL)
if (bWithinSerializeNewActor)
{
UUnitTestNetConnection* UnitConn = Cast<UUnitTestNetConnection>(GActiveReceiveUnitConnection);
bool bAllowActorChan = true;
if (UnitConn != NULL && UnitConn->ActorChannelSpawnDel.IsBound())
// This indicates that SerializeObject has failed to find an existing instance when trying to serialize an actor,
// so it will be spawned clientside later on (after the archetype is serialized) instead.
// These spawns count as undesired clientside code execution, so filter them through NotifyAllowNetActor.
if (Class == AActor::StaticClass() && Obj == NULL)
{
bAllowActorChan = UnitConn->ActorChannelSpawnDel.Execute(Obj->GetClass());
bPendingArchetypeSpawn = true;
}
if (!bAllowActorChan)
// This indicates that a new actor archetype has just been serialized (which may or may not be during actor channel init);
// this is the first place we know the type of a replicated actor (in an actor channel or otherwise), but BEFORE it is spawned
else if ((GIsInitializingActorChan || bPendingArchetypeSpawn) && Class == UObject::StaticClass() && Obj != NULL)
{
Obj = NULL;
UUnitTestNetConnection* UnitConn = Cast<UUnitTestNetConnection>(GActiveReceiveUnitConnection);
bool bAllowActor = false;
// NULL the control channel, to break code that would disconnect the client (control chan is recovered, in ReceivedBunch)
Connection->Channels[0] = NULL;
if (UnitConn != NULL && UnitConn->ReplicatedActorSpawnDel.IsBound())
{
bAllowActor = UnitConn->ReplicatedActorSpawnDel.Execute(Obj->GetClass(), GIsInitializingActorChan);
}
if (!bAllowActor)
{
UE_LOG(LogUnitTest, Log,
TEXT("Blocking replication/spawning of actor on client (add to NotifyAllowNetActor if required)."));
UE_LOG(LogUnitTest, Log, TEXT(" ActorChannel: %s, Class: %s, Archetype: %s"),
(GIsInitializingActorChan ? TEXT("true") : TEXT("false")), *Obj->GetClass()->GetFullName(),
*Obj->GetFullName());
Obj = NULL;
// NULL the control channel, to break code that would disconnect the client (control chan is recovered, in ReceivedBunch)
Connection->Channels[0] = NULL;
}
}
}
@@ -54,11 +72,21 @@ bool UUnitTestPackageMap::SerializeNewActor(FArchive& Ar, class UActorChannel* C
bool bReturnVal = false;
bWithinSerializeNewActor = true;
bPendingArchetypeSpawn = false;
bReturnVal = Super::SerializeNewActor(Ar, Channel, Actor);
bPendingArchetypeSpawn = false;
bWithinSerializeNewActor = false;
// If we are initializing an actor channel, then make this returns false, to block PostNetInit from being called on the actor,
// which in turn, blocks BeginPlay being called (this can lead to actor component initialization, which can trigger garbage
// collection issues down the line)
if (GIsInitializingActorChan)
{
bReturnVal = false;
}
return bReturnVal;
}

View File

@@ -571,17 +571,17 @@ TSharedRef<FTabManager::FLayout> SLogWidget::InitializeTabLayout(const FArgument
{
// Initialize the LogTabs array (which includes labels/tooltips, for each log type)
LogTabs.Add(MakeShareable(new
FLogTabInfo(TEXT("Summary"), TEXT("Filter for the most notable log entries."), ELogType::StatusImportant)));
FLogTabInfo(TEXT("Summary"), TEXT("Filter for the most notable log entries."), ELogType::StatusImportant, 10)));
if (Args._bStatusWidget)
{
LogTabs.Add(MakeShareable(new
FLogTabInfo(TEXT("Advanced Summary"), TEXT("Filter for the most notable log entries, with extra/advanced information."),
ELogType::StatusImportant | ELogType::StatusVerbose | ELogType::StatusAdvanced)));
ELogType::StatusImportant | ELogType::StatusVerbose | ELogType::StatusAdvanced, 20)));
}
LogTabs.Add(MakeShareable(new
FLogTabInfo(TEXT("All"), TEXT("No filters - all log output it shown."), ELogType::All)));
FLogTabInfo(TEXT("All"), TEXT("No filters - all log output it shown."), ELogType::All, 30)));
if (!Args._bStatusWidget)
{
@@ -605,12 +605,12 @@ TSharedRef<FTabManager::FLayout> SLogWidget::InitializeTabLayout(const FArgument
bool bOpenDebugTab = ((Args._ExpectedFilters & ELogType::StatusDebug) == ELogType::StatusDebug);
LogTabs.Add(MakeShareable(new
FLogTabInfo(TEXT("Debug"), TEXT("Filter for debug log entries."), ELogType::StatusDebug, bOpenDebugTab)));
LogTabs.Add(MakeShareable(new
FLogTabInfo(TEXT("Console"), TEXT("Filter for local console command results."), ELogType::OriginConsole, false)));
FLogTabInfo(TEXT("Debug"), TEXT("Filter for debug log entries."), ELogType::StatusDebug, 5, bOpenDebugTab)));
}
LogTabs.Add(MakeShareable(new
FLogTabInfo(TEXT("Console"), TEXT("Filter for local console command results."), ELogType::OriginConsole, 5, false)));
// Initialize the tab manager, stack and layout
TSharedRef<SDockTab> DudTab = SNew(SLockedTab);
@@ -1005,13 +1005,19 @@ TSharedPtr<FLogTabInfo> SLogWidget::GetActiveTabInfo() const
}
void SLogWidget::AddLine(ELogType LogType, TSharedRef<FString> LogLine, FSlateColor LogColor/*=FSlateColor::UseForeground()*/)
void SLogWidget::AddLine(ELogType LogType, TSharedRef<FString> LogLine, FSlateColor LogColor/*=FSlateColor::UseForeground()*/,
bool bTakeTabFocus/*=false*/)
{
TSharedRef<FLogLine> CurLogEntry = MakeShareable(new FLogLine(LogType, LogLine, LogColor));
// Add the line to the master list
LogLines.Add(CurLogEntry);
TSharedPtr<FLogTabInfo> ActiveTab = GetActiveTabInfo();
bool bLineInTabFocus = ActiveTab.IsValid() && !!(ActiveTab->Filter & LogType);
TSharedPtr<FLogTabInfo> FocusTab = NULL;
// Then add it to each log tab, if it passes that tabs filter
for (auto CurTabInfo : LogTabs)
{
@@ -1020,8 +1026,6 @@ void SLogWidget::AddLine(ELogType LogType, TSharedRef<FString> LogLine, FSlateCo
// If the tab is not presently open, open it now
if (!CurTabInfo->bTabOpen && LogTabManager.IsValid())
{
TSharedPtr<FLogTabInfo> ActiveTab = GetActiveTabInfo();
LogTabManager->InvokeTab(CurTabInfo->TabIdName);
// The new tab has stolen focus, now restore the old tabs focus
@@ -1030,6 +1034,16 @@ void SLogWidget::AddLine(ELogType LogType, TSharedRef<FString> LogLine, FSlateCo
CurTabInfo->bTabOpen = true;
}
// If the line is requesting focus, but is not currently in focus, select a tab for focusing
if (bTakeTabFocus && !bLineInTabFocus)
{
if (CurTabInfo != ActiveTab && (!FocusTab.IsValid() || CurTabInfo->Priority < FocusTab->Priority))
{
FocusTab = CurTabInfo;
}
}
CurTabInfo->TabLogLines.Add(CurLogEntry);
@@ -1043,6 +1057,12 @@ void SLogWidget::AddLine(ELogType LogType, TSharedRef<FString> LogLine, FSlateCo
CurLogListView->RequestListRefresh();
}
}
// If a focus change is required, perform it
if (FocusTab.IsValid() && LogTabManager.IsValid())
{
LogTabManager->InvokeTab(FocusTab->TabIdName);
}
}

View File

@@ -10,6 +10,7 @@
FUnitTestEnvironment* UUnitTest::UnitEnv = NULL;
FUnitTestEnvironment* UUnitTest::NullUnitEnv = NULL;
/**
@@ -221,7 +222,9 @@ void UUnitTest::NotifyLocalLog(ELogType LogType, const TCHAR* Data, ELogVerbosit
CurLogColor = FLinearColor(1.f, 1.f, 0.f);
}
LogWidget->AddLine(LogType, MakeShareable(new FString(LogLine)), CurLogColor);
bool bRequestFocus = !!(LogOrigin & ELogType::FocusMask);
LogWidget->AddLine(LogType, MakeShareable(new FString(LogLine)), CurLogColor, bRequestFocus);
}
}
}

View File

@@ -24,6 +24,10 @@ void FUnitTestEnvironment::AddUnitTestEnvironment(FString Game, FUnitTestEnviron
{
UUnitTest::UnitEnv = Env;
}
else if (Game == TEXT("NullUnitEnv"))
{
UUnitTest::NullUnitEnv = Env;
}
}
}
@@ -32,44 +36,59 @@ FString FUnitTestEnvironment::GetDefaultMap(EUnitTestFlags UnitTestFlags)
return TEXT("");
}
FString FUnitTestEnvironment::GetDefaultServerParameters()
FString FUnitTestEnvironment::GetDefaultServerParameters(FString InLogCmds/*=TEXT("")*/, FString InExecCmds/*=TEXT("")*/)
{
FString ReturnVal = TEXT("");
FString CmdLineServerParms;
FString FullLogCmds = TEXT("");
FString FullLogCmds = InLogCmds;
FString FullExecCmds = InExecCmds;
if (NUTUtil::ParseValue(FCommandLine::Get(), TEXT("UnitTestServerParms="), CmdLineServerParms))
{
// LogCmds needs to be specially merged, since it may be specified in multiple places
FString LogCmds;
if (FParse::Value(*CmdLineServerParms, TEXT("-LogCmds="), LogCmds, false))
{
if (FullLogCmds.Len() > 0)
auto ParseCmds =
[&](const TCHAR* CmdsParm, FString& OutFullCmds)
{
FullLogCmds += TEXT(",");
}
FString Cmds;
FullLogCmds += LogCmds.TrimQuotes();
if (FParse::Value(*CmdLineServerParms, CmdsParm, Cmds, false))
{
if (OutFullCmds.Len() > 0)
{
OutFullCmds += TEXT(",");
}
// Now remove the original LogCmds entry
FString SearchStr = FString::Printf(TEXT("-LogCmds=\"%s\""), *LogCmds.TrimQuotes());
OutFullCmds += Cmds.TrimQuotes();
CmdLineServerParms = CmdLineServerParms.Replace(*SearchStr, TEXT(""), ESearchCase::IgnoreCase);
}
// Now remove the original *Cmds entry
FString SearchStr = FString::Printf(TEXT("%s\"%s\""), CmdsParm, *Cmds.TrimQuotes());
CmdLineServerParms = CmdLineServerParms.Replace(*SearchStr, TEXT(""), ESearchCase::IgnoreCase);
}
};
// LogCmds and ExecCmds need to be specially merged, since they may be specified in muliple places
// (e.g. within unit tests, and using UnitTestServerParms)
ParseCmds(TEXT("-LogCmds="), FullLogCmds);
ParseCmds(TEXT("-ExecCmds="), FullExecCmds);
ReturnVal += TEXT(" ");
ReturnVal += CmdLineServerParms;
}
SetupDefaultServerParameters(ReturnVal, FullLogCmds);
SetupDefaultServerParameters(ReturnVal, FullLogCmds, FullExecCmds);
if (FullLogCmds.Len() > 0)
{
ReturnVal += FString::Printf(TEXT(" -LogCmds=\"%s\""), *FullLogCmds);
}
if (FullExecCmds.Len() > 0)
{
ReturnVal += FString::Printf(TEXT(" -ExecCmds=\"%s\""), *FullExecCmds);
}
return ReturnVal;
}
@@ -118,6 +137,9 @@ void FUnitTestEnvironment::GetServerProgressLogs(const TArray<FString>*& OutStar
// Logs which should trigger a timeout reset for all games
TimeoutResetLogs.Add(TEXT("LogStaticMesh: Building static mesh "));
TimeoutResetLogs.Add(TEXT("LogMaterial: Missing cached shader map for material "));
TimeoutResetLogs.Add(TEXT("Dumping tracked stack traces for TraceName '"));
TimeoutResetLogs.Add(TEXT("Dumping once-off stack trace for TraceName '"));
InitializeServerProgressLogs(StartProgressLogs, ReadyLogs, TimeoutResetLogs);
@@ -129,3 +151,23 @@ void FUnitTestEnvironment::GetServerProgressLogs(const TArray<FString>*& OutStar
OutTimeoutResetLogs = &TimeoutResetLogs;
}
void FUnitTestEnvironment::GetClientProgressLogs(const TArray<FString>*& OutTimeoutResetLogs)
{
static TArray<FString> TimeoutResetLogs;
static bool bSetupLogs = false;
// @todo JohnB: See what logs are common between client/server, and perhaps create a third generic progress log function for them
if (!bSetupLogs)
{
// Logs which should trigger a timeout reset for all games
TimeoutResetLogs.Add(TEXT("LogMaterial: Missing cached shader map for material "));
InitializeClientProgressLogs(TimeoutResetLogs);
bSetupLogs = true;
}
OutTimeoutResetLogs = &TimeoutResetLogs;
}

View File

@@ -12,9 +12,10 @@
#include "SLogWindow.h"
#include "SLogDialog.h"
#include "NUTUtil.h"
#include "NUTUtilDebug.h"
#include "NUTUtilNet.h"
#include "NUTUtilProfiler.h"
#include "NUTUtil.h"
// @todo JohnB: Add an overall-timer, and then start debugging the memory management in more detail
@@ -146,14 +147,15 @@ bool UUnitTestManager::QueueUnitTest(UClass* UnitTestClass, bool bRequeued/*=fal
UnitTestClass != UClientUnitTest::StaticClass();
UUnitTest* UnitTestDefault = (bValidUnitTestClass ? Cast<UUnitTest>(UnitTestClass->GetDefaultObject()) : NULL);
bool bSupportsAllGames = (bValidUnitTestClass ? UnitTestDefault->GetSupportedGames().Contains("NullUnitEnv") : false);
bValidUnitTestClass = UnitTestDefault != NULL;
if (bValidUnitTestClass && UUnitTest::UnitEnv != NULL)
if (bValidUnitTestClass && (UUnitTest::UnitEnv != NULL || bSupportsAllGames))
{
FString UnitTestName = UnitTestDefault->GetUnitTestName();
bool bCurrentGameSupported = UnitTestDefault->GetSupportedGames().Contains(FApp::GetGameName());
bool bCurrentGameSupported = bSupportsAllGames || UnitTestDefault->GetSupportedGames().Contains(FApp::GetGameName());
if (bCurrentGameSupported)
{
@@ -219,8 +221,20 @@ bool UUnitTestManager::QueueUnitTest(UClass* UnitTestClass, bool bRequeued/*=fal
}
else if (UUnitTest::UnitEnv == NULL)
{
STATUS_LOG(ELogType::StatusError | ELogType::StyleBold,
TEXT("No unit test environment found (need to load unit test environment module for this game, or create it)."));
ELogType StatusType = ELogType::StyleBold;
// @todo JohnB: This should be enabled by default instead, so automation testing does give an error when a game doesn't
// have a unit test environment. You should probably setup a whitelist of 'known-unsupported-games' somewhere,
// to keep track of what games need support added, without breaking automation testing (but without breaking the
// flow of setting-up/running automation tests)
if (!GIsAutomationTesting)
{
StatusType |= ELogType::StatusError;
}
STATUS_LOG(StatusType,
TEXT("No unit test environment found (need to load unit test environment module for this game '%s', or create it)."),
FApp::GetGameName());
}
return bSuccess;
@@ -318,6 +332,12 @@ bool UUnitTestManager::WithinUnitTestLimits(UClass* PendingUnitTest/*=NULL*/)
// Check max unit test count
bReturnVal = !bCapUnitTestCount || ActiveUnitTests.Num() < MaxUnitTestCount;
int32 CommandlineCap = 0;
if (bReturnVal && FParse::Value(FCommandLine::Get(), TEXT("UnitTestCap="), CommandlineCap) && CommandlineCap > 0)
{
bReturnVal = ActiveUnitTests.Num() < CommandlineCap;
}
// Limit the number of first-run unit tests (which don't have any stats gathered), to MaxUnitTestCount, even if !bCapUnitTestCount.
// If any first-run unit tests have had to be aborted, this might signify a problem, so make the cap very strict (two at a time)
@@ -1027,7 +1047,7 @@ void UUnitTestManager::OpenStatusWindow()
{
FString LogLine = FOutputDevice::FormatLogLine(Verbosity, Category, V);
STATUS_LOG_BASE(, TEXT("%s"), *LogLine);
STATUS_LOG_BASE(ELogType::OriginConsole, TEXT("%s"), *LogLine);
});
@@ -1293,54 +1313,78 @@ bool UUnitTestManager::Exec(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar
// Debug unit test commands
else if (UnitTestName == TEXT("debug"))
{
if (InWorld != NULL)
{
UUnitTest** TargetUnitTestRef = ActiveUnitTests.FindByPredicate(
[&InWorld](const UUnitTest* InElement)
{
auto CurUnitTest = Cast<UClientUnitTest>(InElement);
return CurUnitTest != NULL && CurUnitTest->UnitWorld == InWorld;
});
UClientUnitTest* TargetUnitTest = (TargetUnitTestRef != NULL ? Cast<UClientUnitTest>(*TargetUnitTestRef) : NULL);
if (TargetUnitTest != NULL)
UUnitTest** TargetUnitTestRef = (InWorld != NULL ? ActiveUnitTests.FindByPredicate(
[&InWorld](const UUnitTest* InElement)
{
if (FParse::Command(&Cmd, TEXT("Requirements")))
auto CurUnitTest = Cast<UClientUnitTest>(InElement);
return CurUnitTest != NULL && CurUnitTest->UnitWorld == InWorld;
})
: NULL);
UClientUnitTest* TargetUnitTest = (TargetUnitTestRef != NULL ? Cast<UClientUnitTest>(*TargetUnitTestRef) : NULL);
// Alternatively, if a unit test has not launched started connecting to a server, its world may not be setup,
// so can detect by checking the active log unit test too
if (TargetUnitTest == NULL && GActiveLogUnitTest != NULL)
{
TargetUnitTest = Cast<UClientUnitTest>(GActiveLogUnitTest);
}
if (TargetUnitTest != NULL)
{
if (FParse::Command(&Cmd, TEXT("Requirements")))
{
EUnitTestFlags RequirementsFlags = (TargetUnitTest->UnitTestFlags & EUnitTestFlags::RequirementsMask);
EUnitTestFlags MetRequirements = TargetUnitTest->GetMetRequirements();
// Iterate over the requirements mask flag bits
FString RequiredBits = TEXT("");
FString MetBits = TEXT("");
FString FailBits = TEXT("");
TArray<FString> FlagResults;
EUnitTestFlags FirstFlag =
(EUnitTestFlags)(1UL << (31 - FMath::CountLeadingZeros((uint32)EUnitTestFlags::RequirementsMask)));
for (EUnitTestFlags CurFlag=FirstFlag; !!(CurFlag & EUnitTestFlags::RequirementsMask);
CurFlag = (EUnitTestFlags)((uint32)CurFlag >> 1))
{
EUnitTestFlags RequirementsFlags = (TargetUnitTest->UnitTestFlags & EUnitTestFlags::RequirementsMask);
EUnitTestFlags MetRequirements = TargetUnitTest->GetMetRequirements();
bool bCurFlagReq = !!(CurFlag & RequirementsFlags);
bool bCurFlagSet = !!(CurFlag & MetRequirements);
// Iterate over the requirements mask flag bits
FString RequiredBits = TEXT("");
FString MetBits = TEXT("");
FString FailBits = TEXT("");
TArray<FString> FlagResults;
EUnitTestFlags FirstFlag =
(EUnitTestFlags)(1UL << (31 - FMath::CountLeadingZeros((uint32)EUnitTestFlags::RequirementsMask)));
RequiredBits += (bCurFlagReq ? TEXT("1") : TEXT("0"));
MetBits += (bCurFlagSet ? TEXT("1") : TEXT("0"));
FailBits += ((bCurFlagReq && !bCurFlagSet) ? TEXT("1") : TEXT("0"));
for (EUnitTestFlags CurFlag=FirstFlag; !!(CurFlag & EUnitTestFlags::RequirementsMask);
CurFlag = (EUnitTestFlags)((uint32)CurFlag >> 1))
{
bool bCurFlagReq = !!(CurFlag & RequirementsFlags);
bool bCurFlagSet = !!(CurFlag & MetRequirements);
FlagResults.Add(FString::Printf(TEXT(" - %s: Required: %i, Set: %i, Failed: %i"), *GetUnitTestFlagName(CurFlag),
(uint32)bCurFlagReq, (uint32)bCurFlagSet, (uint32)(bCurFlagReq && !bCurFlagSet)));
}
RequiredBits += (bCurFlagReq ? TEXT("1") : TEXT("0"));
MetBits += (bCurFlagSet ? TEXT("1") : TEXT("0"));
FailBits += ((bCurFlagReq && !bCurFlagSet) ? TEXT("1") : TEXT("0"));
Ar.Logf(TEXT("Requirements flags for unit test '%s': Required: %s, Set: %s, Failed: %s"),
*TargetUnitTest->GetUnitTestName(), *RequiredBits, *MetBits, *FailBits);
FlagResults.Add(FString::Printf(TEXT(" - %s: Required: %i, Set: %i, Failed: %i"), *GetUnitTestFlagName(CurFlag),
(uint32)bCurFlagReq, (uint32)bCurFlagSet, (uint32)(bCurFlagReq && !bCurFlagSet)));
}
for (auto CurResult : FlagResults)
{
Ar.Logf(*CurResult);
}
}
else if (FParse::Command(&Cmd, TEXT("ForceReady")))
{
if (TargetUnitTest != NULL && !!(TargetUnitTest->UnitTestFlags & EUnitTestFlags::LaunchServer) &&
TargetUnitTest->ServerHandle.IsValid() && TargetUnitTest->UnitPC == NULL)
{
Ar.Logf(TEXT("Forcing unit test '%s' as ready to connect client."), *TargetUnitTest->GetUnitTestName());
Ar.Logf(TEXT("Requirements flags for unit test '%s': Required: %s, Set: %s, Failed: %s"),
*TargetUnitTest->GetUnitTestName(), *RequiredBits, *MetBits, *FailBits);
TargetUnitTest->ConnectFakeClient();
}
}
else if (FParse::Command(&Cmd, TEXT("Disconnect")))
{
if (TargetUnitTest != NULL && TargetUnitTest->UnitConn != NULL)
{
Ar.Logf(TEXT("Forcing unit test '%s' to disconnect."), *TargetUnitTest->GetUnitTestName());
for (auto CurResult : FlagResults)
{
Ar.Logf(*CurResult);
}
TargetUnitTest->UnitConn->Close();
}
}
}
@@ -1490,8 +1534,9 @@ void UUnitTestManager::Serialize(const TCHAR* Data, ELogVerbosity::Type Verbosit
}
TSharedRef<FString> LogLineRef = MakeShareable(LogLine);
bool bRequestFocus = !!(CurLogType & ELogType::FocusMask);
LogWidget->AddLine(CurLogType, LogLineRef, StatusColor);
LogWidget->AddLine(CurLogType, LogLineRef, StatusColor, bRequestFocus);
if (bSetTypeColor)
@@ -1610,6 +1655,141 @@ static bool UnitTestExec(UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar)
return true;
}
/**
* Special 'StackTrace' command, for adding complex arbitrary stack tracing, as a debugging method.
*
* Usage: (NOTE: Replace 'TraceName' as desired, to help identify traces in logs)
*
* Once-off stack trace/dump:
* GEngine->Exec(NULL, TEXT("StackTrace TraceName"));
*
*
* Multiple tracked stack traces: (grouped by TraceName)
* - Add a stack trace to tracking: ('-Log' also logs that a stack trace was added, and '-Dump' immediately dumps it to log,
* '-StartDisabled' only begins tracking once the 'enable' command below is called)
* GEngine->Exec(NULL, TEXT("StackTrace TraceName Add"));
* GEngine->Exec(NULL, TEXT("StackTrace TraceName Add -Log"));
* GEngine->Exec(NULL, TEXT("StackTrace TraceName Add -Log -Dump"));
* GEngine->Exec(NULL, TEXT("StackTrace TraceName Add -StartDisabled"));
*
* - Dump collected/tracked stack traces: (also removes from tracking by default, unless -Keep is added)
* GEngine->Exec(NULL, TEXT("StackTrace TraceName Dump"));
* GEngine->Exec(NULL, TEXT("StackTrace TraceName Dump -Keep"));
*
* - Temporarily disable tracking: (NOTE: Dump must be used with -Keep to use this)
* GEngine->Exec(NULL, TEXT("StackTrace TraceName Disable"));
*
* - Enable/re-enable tracking: (usually not necessary, unless tracking was previously disabled)
* GEngine->Exec(NULL, TEXT("StackTrace TraceName Enable"));
*
*
* Additional commands:
* - Dump all active stack traces (optionally skip resetting built up stack traces, and optionally stop all active traces)
* "StackTrace DumpAll"
* "StackTrace DumpAll -NoReset"
* "StackTrace DumpAll -Stop"
*/
else if (FParse::Command(&Cmd, TEXT("StackTrace")))
{
FString TraceName;
if (FParse::Command(&Cmd, TEXT("DumpAll")))
{
bool bKeepTraceHistory = FParse::Param(Cmd, TEXT("KEEPHISTORY"));
bool bStopTracking = FParse::Param(Cmd, TEXT("STOP"));
GTraceManager.DumpAll(bKeepTraceHistory, !bStopTracking);
}
else if (FParse::Token(Cmd, TraceName, true))
{
if (FParse::Command(&Cmd, TEXT("Enable")))
{
GTraceManager.Enable(TraceName);
}
else if (FParse::Command(&Cmd, TEXT("Disable")))
{
GTraceManager.Disable(TraceName);
}
else if (FParse::Command(&Cmd, TEXT("Add")))
{
bool bLogAdd = FParse::Param(Cmd, TEXT("LOG"));
bool bDump = FParse::Param(Cmd, TEXT("DUMP"));
bool bStartDisabled = FParse::Param(Cmd, TEXT("STARTDISABLED"));
GTraceManager.AddTrace(TraceName, bLogAdd, bDump, bStartDisabled);
}
else if (FParse::Command(&Cmd, TEXT("Dump")))
{
GTraceManager.Dump(TraceName);
}
// If no subcommands above are specified, assume this is a once-off stack trace dump
// @todo JohnB: This will also capture mistyped commands, so find a way to detect that
else
{
GTraceManager.TraceAndDump(TraceName);
}
}
else
{
Ar.Logf(TEXT("Need to specify TraceName, i.e. 'StackTrace TraceName'"));
}
}
/**
* Special 'LogTrace' command, which ties into the stack tracking code as used by the 'StackTrace' command.
* Every time a matching log entry is encountered, a stack trace is dumped.
*
* NOTE: Does not track the category or verbosity of log entries
*
* Usage: (NOTE: Replace 'LogLine' with the log text to be tracked)
*
* - Add an exact log line to tracking (case sensitive, and must match length too):
* GEngine->Exec(NULL, TEXT("LogTrace Add LogLine"));
*
* - Add a partial log line to tracking (case insensitive, and can match substrings):
* GEngine->Exec(NULL, TEXT("LogTrace AddPartial LogLine"));
*
* - Dump accumulated log entries, for a specified log line, and clears it from tracing:
* GEngine->Exec(NULL, TEXT("LogTrace Dump LogLine"));
*
* - Clear the specified log line from tracing:
* GEngine->Exec(NULL, TEXT("LogTrace Clear LogLine"));
*
* - Clear all log lines from tracing:
* GEngine->Exec(NULL, TEXT("LogTrace ClearAll"));
* GEngine->Exec(NULL, TEXT("LogTrace ClearAll -Dump"));
*/
else if (FParse::Command(&Cmd, TEXT("LogTrace")))
{
FString LogLine = TEXT("NotSet");
if (FParse::Command(&Cmd, TEXT("Add")) && (LogLine = Cmd).Len() > 0)
{
GLogTraceManager.AddLogTrace(LogLine, false);
}
else if (FParse::Command(&Cmd, TEXT("AddPartial")) && (LogLine = Cmd).Len() > 0)
{
GLogTraceManager.AddLogTrace(LogLine, true);
}
else if (FParse::Command(&Cmd, TEXT("Dump")) && (LogLine = Cmd).Len() > 0)
{
GLogTraceManager.ClearLogTrace(LogLine, true);
}
else if (FParse::Command(&Cmd, TEXT("Clear")) && (LogLine = Cmd).Len() > 0)
{
GLogTraceManager.ClearLogTrace(LogLine, false);
}
else if (FParse::Command(&Cmd, TEXT("ClearAll")))
{
bool bDump = FParse::Param(Cmd, TEXT("DUMP"));
GLogTraceManager.ClearAll(bDump);
}
// If LogLine is now zero-length instead of 'NotSet', that means a valid command was encountered, but no LogLine specified
else if (LogLine.Len() == 0)
{
Ar.Logf(TEXT("Need to specify a log line for tracing."));
}
}
return bReturnVal;
}

Some files were not shown because too many files have changed in this diff Show More