Mapz's Blog

可以递归的函数指针

UE5:简述Gauntlet的原理和使用

什么是 Gauntlet

Gauntlet 官方文档

简而言之,Gauntlet 提供支持来启动多个实例来进行测试,例如常见的 Dedicated Server 测试,需要一个服务器和多个客户端

但是 Gauntlet 并不关心测试的内容,只提供一个需要开发者实现的 GauntletTestController 来控制测试的流程

所以 Gauntlet 的侧重点在于 组织单个或多个实例 ,而非测试的 内容和形式(包括使用的测试框架,测试的内容等)

具体的测试可以由开发者自己编写的 GauntletTestController 以 任意方式 来测试 任意内容

本文的开发环境

操作系统:windows 11

引擎:UE 5.0 源码版本

IDE:Rider for Unreal

Gauntlet 的原理

两个部分

Gauntlet 分为两个部分

  • 游戏外自动化部分,属于 AutomationTool
  • 游戏内 Runtime 部分,属于 Gauntlet 插件,通过 Gauntlet Test Controller 驱动游戏内逻辑

其中游戏外部分,控制游戏实例的参数和启动

游戏内部分,控制具体的测试逻辑

什么是 AutomationTool

AutomationTool 官方文档

AutomationTool 可简称 UAT

是虚幻引擎中用于自动化流水线的一套工具,主要用于 Build 项目和测试等,这套工具由 C# 编写

执行

1
"%EnginePath%\Engine\Build\BatchFiles\RunUAT"

即可执行 Automation 命令,命令的基类为

1
public abstract class BuildCommand : CommandUtils

开发者也可以自己添加 Automation 项目

在其中添加相关 Automation 中需要的自定义类型,或者继承 BuildCommand 为 UAT 添加自定义命令

Automation 项目都需要以 XXX.Automation 来命名,且放在 [ProjectDir]\Build 或 [EngineDir]\Source\Programs 目录下

Build 时需要使用源码版本的引擎,从而在每次 Build 的时候重新生成自定义 Automation 项目的 dll

Gauntlet.Automation 游戏外自动化部分

Gauntlet 正是 Automation 中的一个项目,也就是上文所说的游戏外自动化部分

RunUnreal 命令

Gauntlet 项目提供了 RunUnreal 命令来进行 Gauntlet 测试

1
public class RunUnreal : BuildCommand

命令入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

public override ExitCode Execute()
{
Globals.Params = new Gauntlet.Params(this.Params);

UnrealTestOptions ContextOptions = new UnrealTestOptions();
// 附加载各种参数到 Context 中
AutoParam.ApplyParamsAndDefaults(ContextOptions, Globals.Params.AllArguments);

if (string.IsNullOrEmpty(ContextOptions.Project))
{
throw new AutomationException("No project specified. Use -project=ShooterGame etc");
}

ContextOptions.Namespaces = "Gauntlet.UnrealTest,UnrealGame,UnrealEditor";
ContextOptions.UsesSharedBuildType = true;
// 开始测试
return RunTests(ContextOptions);
}

测试参数 Context

1
public class UnrealTestOptions : TestExecutorOptions,  IAutoParamNotifiable

其中就包含了上面所说的启动多个实例需要用到的包括 Platform 类型、Build 类型、包文件地址、多个实例的 Build Target 类型、Role 类型(客户端还是服务器)、加载的地图之类的参数

RunTest

1
public virtual ExitCode RunTests(UnrealTestOptions ContextOptions)

包含测试逻辑, ExitCode 中会返回测试是否通过

执行函数时,会先根据我们传入的 Context 来查找存在的符合条件的可执行文件,然后打开一个或多个游戏实例来进行测试

ExecuteTests

1
bool ExecuteTests(UnrealTestOptions Options, IEnumerable<ITestNode> TestList)

具体测试逻辑入口,准备好实例的 Context 信息后,会传入测试列表,执行这个函数开始测试

测试的具体CSharp类型

1
public abstract class BaseTest : ITestNode

这个就是测试的基类,包含了测试所需的基本逻辑链条,在实际使用中,需要使用其子类

1
2
public abstract class UnrealTestNode<TConfigClass> : BaseTest, IDisposable
where TConfigClass : UnrealTestConfiguration, new()

来派生出开发者自己的测试类型,并在其中指定 Gauntlet Controller 和游戏实例配置等信息

这个类型中,还包括一些测试结果相关的功能,例如输入测试报告等

在实际使用的章节我们会继续提到这个类型

我们现在只需要知道,测试的列表,是通过 UAT 的命令行传入的

启动实例

1
protected bool PrepareUnrealApp()

在测试 Node 开始测试的时候,这个函数会被执行,来启动游戏实例

在这个函数中,我们会发现上文所说的 两个部分 连接起来的秘密

1
2
3
// add controllers
SessionRole.CommandLineParams.Add("gauntlet",
TestRole.Controllers.Count > 0 ? string.Join(",", TestRole.Controllers) : null);

这里,把 Gauntlet Test Controller 通过命令行的形式,加入到了游戏实例的启动参数中!

也就是说游戏实例启动后,一定是去获取了 gauntlet=xxx,xxx,xxx 参数,来获得需要执行的 gauntlet test controller 列表

这个我们在后面再继续聊

当 Controller 准备好以后,最终执行了

1
UnrealApp = new UnrealSession(Context.BuildInfo, SessionRoles) { Sandbox = Context.Options.Sandbox };

来启动实例

我们可以看到,这个实例是以 UnrealSession 的形式存在的

UnrealSession

1
public class UnrealSession : IDisposable

Unreal Session 用于启动、监控、关闭游戏实例

Gauntlet 游戏内插件部分

可以看到,插件非常简单,基本就只有一个 Gauntlet Test Controller

并提供了两个示例 BootTest 和 Error Test

加载 Gauntlet 命令行的位置

GauntletModule.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void FGauntletModuleImpl::OnPostEngineInit()
{
FCoreUObjectDelegates::PostLoadMapWithWorld.AddRaw(this, &FGauntletModuleImpl::InnerPostMapChange);
FCoreUObjectDelegates::PreLoadMap.AddRaw(this, &FGauntletModuleImpl::InnerPreMapChange);

FParse::Value(FCommandLine::Get(), TEXT("gauntlet.screenshotperiod="), ScreenshotPeriod);
FParse::Value(FCommandLine::Get(), TEXT("gauntlet.heartbeatperiod="), HeartbeatPeriod);
// 加载 Controllers
LoadControllers();

float kTickRate = 1.0f;
FParse::Value(FCommandLine::Get(), TEXT("gauntlet.tickrate="), kTickRate);


TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda([this, kTickRate](float TimeDelta)
{
// ticker passes in frame-delta, not tick delta...
InnerTick(kTickRate);
return true;
}),
kTickRate);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
void FGauntletModuleImpl::LoadControllers()
{
FString ControllerString;

FParse::Value(FCommandLine::Get(), TEXT("gauntlet="), ControllerString, false);

if (ControllerString.Len())
{
TArray<FString> ControllerNames;

ControllerString.ParseIntoArrayWS(ControllerNames, TEXT(","));

TSet<FString> ControllersToCreate;

for (const FString& Name : ControllerNames)
{
UClass* TestClass = nullptr;

for (TObjectIterator<UClass> It; It; ++It)
{
UClass* Class = *It;

FString FullName = Name;

// Search for SomethingTestController and ControllerSomethingTest. The last is legacy
FString PartialName1 = Name + TEXT("Controller");
FString PartialName2 = FString(TEXT("Controller")) + Name;

if (Class->IsChildOf<UGauntletTestController>())
{
FString ClassName = Class->GetName();
if (ClassName == FullName || ClassName.EndsWith(PartialName1) || ClassName.EndsWith(PartialName2))
{
// Gauntlet has a couple of test classes, so need to differentiate between "GauntletFooTest" and "GameFooTest".
// that will both be launched via -gauntlet=FooTest
bool GauntletDefault = ClassName.StartsWith(TEXT("Gauntlet"));

TestClass = Class;

// If not gauntlet stop searching
if (!GauntletDefault)
{
break;
}
}
}
}

checkf(TestClass, TEXT("Could not find class for controller %s"), *Name);

UGauntletTestController* NewController = NewObject<UGauntletTestController>(GetTransientPackage(), TestClass);

check(NewController);

UE_LOG(LogGauntlet, Display, TEXT("Added Gauntlet controller %s"), *Name);

// Important - add the controller first! Some controllers may trigger GC's which would
// then result in them being collected...
Controllers.Add(NewController);
}
}

for (auto Controller : Controllers)
{
Controller->OnInit();
}
}

可以看到,在 PostEngineInit 后,开始 LoadControllers

然后读取了命令行,并使用其名称,去获得 C++ 的相应类型

然后执行其 OnInit

GauntletTestController 简析

声明

1
2
UCLASS()
class GAUNTLET_API UGauntletTestController : public UObject

里面是一些简单的状态控制,几个处理函数都需要自己去实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
virtual void    OnInit() {}

/**
* Called prior to a map change
*/
virtual void OnPreMapChange() {}

/**
* Called after a map change. GetCurrentMap() will now return the new map
*/
virtual void OnPostMapChange(UWorld* World) {}

/**
* Called periodically to let the controller check and control state
*/
virtual void OnTick(float TimeDelta) {}

/**
* Called when a state change is applied to the module. States are game-driven.
* GetCurrentState() == OldState until this function returns
*/
virtual void OnStateChange(FName OldState, FName NewState) {}

正所谓什么都没写,就有无限可能(狗头。

前面说的可以测试任何内容就是这个意思,你可以通过写一个自己的 TestController 来添加任何逻辑和接入任何测试框架。

从上面的 OnInit 那里就可以简单推测出,整个 TestController 是由 GauntletModuleImpl 来驱动的,事实呢也是如此,上面几个函数的调用都是在 GauntletModuleImpl 中。

从零开始的Gauntlet实战

现在我们已经了解了 Gauntlet 的基本原理,那么现在我们从零开始使用 Gauntlet 搭建一个简单的测试

版本是 UE 5.0 源码编译版本

搭建源码环境

因为编译 Automation 项目需要源码版本的引擎,所以需要搭建源码环境

创建一个新的游戏项目

这里我们选择 First Person 模版,创建一个 C++ 项目,取名 GauntletTutorial

然后把项目依赖的引擎版本设置为源码,编译启动编辑器

打开 Plugin 设置,启用 Gauntlet 插件

因为我们测试的时候需要启动 Server 和 Client 实例,所以我们需要在项目中添加 Server Build Target 以及 Client Build Target

详见 官网文档说明

我们这里只需要正确添加 GauntletTutorialServer.Target.cs 和 GauntletTutorialClient.Target.cs 两个文件即可

制作 Automation 工程

创建项目

官网文档说明

注:5.0 以前的版本和 5.0 以后的版本使用的 .Net 依赖不同,创建方式略有不同

我这里 IDE 使用 Rider ,使用 VS 的同学应该也差不太多

选择 File -> New Solution -> .Net/.Net Core -> Class Library

填入你喜欢的项目名称,记住之前说过的两个规则

  1. 项目以 XXX.Automation 命名
  2. 项目放在 [ProjectDir]\Build 下

我们这里在 [ProjectDir]\Build 目录下创建一个 Gauntlet 目录,把我们的项目放进去

然后 Framework 选择 netcoreapp3.1

勾上 Put Solution and project in the same directory

点击创建生成项目

编写一个测试

原理部分讲到,我们的每个测试都是继承自

1
2
public abstract class UnrealTestNode<TConfigClass> : BaseTest, IDisposable
where TConfigClass : UnrealTestConfiguration, new()

的一个子类

其中,TConfigClass 是我们测试使用的配置类型,继承自 UnrealTestConfiguration

我们现在来创建一个自己的测试

修改默认创建的 Class1 ,改名为 GauntletTutorialTest,并使用 UnrealTestConfig 类型作为配置,类声明如下

1
public class GauntletTutorialTest : UnrealTestNode<UnrealTestConfig>

可以看到 IDE 提示缺少依赖,我们需要引入 Gauntlet.Automation 项目作为依赖

项目目录右键点击 Dependence ,打开 Reference

点击 Add From

选择 [EnginePath]\unrealengine\Engine\Binaries\DotNET\AutomationScripts\netcoreapp3.1\Gauntlet.Automation.dll

然后修改代码,最终如下

1
2
3
4
5
6
7
8
9
10
11
using Gauntlet;
using UnrealGame;
namespace GauntletTutorial.Automation
{
public class GauntletTutorialTest : UnrealTestNode<UnrealTestConfig>
{
public GauntletTutorialTest(UnrealTestContext inContext) : base(inContext)
{
}
}
}

我们现在创建出了一个新的测试,但如何设置测试参数呢?答案是重写函数 GetConfiguration()

1
2
3
4
/// <summary>
/// Returns information about how to configure our Unreal processes. For the most part the majority
/// of Unreal tests should only need to override this function
/// </summary>

此函数的说明:大部分情况下,我们只需要重写这个函数就可以了

现在我们大致写一个测试配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public override UnrealTestConfig GetConfiguration()
{
UnrealTestConfig config = base.GetConfiguration();

// 添加一个客户端和一个服务器
UnrealTestRole clientRole = config.RequireRole(UnrealTargetRole.Client);
UnrealTestRole serverRole = config.RequireRole(UnrealTargetRole.Server);

// 设置一个最长测试时间(单位是秒,时间到了后会结束测试)
config.MaxDuration = 60 * 5;

// 超时后,测试结果是成功还是失败
config.MaxDurationReachedResult = EMaxDurationReachedResult.Success;

// 给每个 Role 设定 GauntletTestController
// 这里使用官方提供的 ErrorTest 来做一个测试
clientRole.Controllers.Add("GauntletTestControllerErrorTest");
serverRole.Controllers.Add("GauntletTestControllerErrorTest");

// 可以通过 Map 来修改测试地图
// config.Map = "TestMap";

// 也可以通过 Role 的 Map Override 来修改地图
// clientRole.MapOverride = "ClientMapOverride";

return config;
}

另外除了这些配置,我们可以看到 UnrealTestConfig 和其父类中还有很多注解为 AutoParam 的变量

这些变量都可以通过 RunUnreal 的命令行参数来自动获取并赋值

在测试中,我们指定每个 Role 都执行系统自带的 GauntletTestControllerErrorTest 来测试

至此一个简单的测试就完成了

运行编写好的测试

通过之前的原理部分,我们知道,Gauntlet Automation 是去查找打好的可执行文件来运行的,所以运行前,应该保证你测试中的 Role 实例已经打好包了,例如 Server 就要打好服务器包

在 [ProjectDir] 下新建一个 Scripts 目录

打包

新建一个批处理 Pack.bat 来进行打包

1
2
3
4
5
6
7
8
@echo on

set ProjectPath=项目目录
set EnginePath=源码版引擎目录

"%EnginePath%\Engine\Build\BatchFiles\RunUAT" BuildCookRun -project=%ProjectPath%\GauntletTutorial.uproject -platform=Win64 -configuration=Development -build -cook -pak -stage -server -client

pause

执行后,会打出 Client 和 Server 包,因为是源码构建,可能会花比较久的时间

执行 RunUnreal

再新建一个批处理 RunGauntlet.bat

1
2
3
4
5
6
7
8
@echo on

set ProjectPath=项目目录
set StagingDir=%ProjectPath%\Saved\StagedBuilds\
set EnginePath=源码版引擎目录

"%EnginePath%\Engine\Build\BatchFiles\RunUAT" RunUnreal -project=%ProjectPath%\GauntletTutorial.uproject -build=%StagingDir% -platform=Win64 -configuration=development -test=GauntletTutorial.Automation.GauntletTutorialTest -scriptdir=%ProjectPath%
pause

其中 platform 和 configuration 一定更要和打包配置一致,否则会找不到目标可执行文件

build 参数选择打出来的包的目录

test 参数填入 Gauntlet Automation 测试的全名

运行后,我们可以看到,按照配置启动了一个客户端和一个服务器,如下

然后测试结果是失败

原因是 GauntletTestControllerErrorTest 默认逻辑会在启动后立刻释放一个 check 让测试失败

传入命令行参数到 Automation 项目和游戏实例中

查看 UGauntletTestControllerErrorTest 的源码,我们可知这个测试会接受命令行参数,从而调用 check , ensure 等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void UGauntletTestControllerErrorTest::OnInit()
{
ErrorDelay = 0;
ErrorType = TEXT("check");

FParse::Value(FCommandLine::Get(), TEXT("errortest.delay="), ErrorDelay);
FParse::Value(FCommandLine::Get(), TEXT("errortest.type="), ErrorType);
}

void UGauntletTestControllerErrorTest::OnTick(float TimeDelta)
{
if (GetTimeInCurrentState() > ErrorDelay)
{
if (ErrorType == TEXT("ensure"))
{
UE_LOG(LogGauntlet, Display, TEXT("Issuing ensure as requested"));
ensureMsgf(false, TEXT("Ensuring false...."));
EndTest(-1);
}
else if (ErrorType == TEXT("check"))
{
UE_LOG(LogGauntlet, Display, TEXT("Issuing failed check as requested"));
checkf(false, TEXT("Asserting as requested"));
}
else if (ErrorType == TEXT("fatal"))
{
UE_LOG(LogGauntlet, Fatal, TEXT("Issuing fatal error as requested"));
}
else if (ErrorType == TEXT("gpf"))
{
#ifndef PVS_STUDIO
UE_LOG(LogGauntlet, Display, TEXT("Issuing GPF as requested"));
int* Ptr = (int*)0;
CA_SUPPRESS(6011);
*Ptr = 42; // -V522
#endif // PVS_STUDIO
}
else
{
UE_LOG(LogGauntlet, Error, TEXT("No recognized error request. Failing test"));
EndTest(-1);
}
}
}

那么我们怎么样才能从 Gauntlet 传入命令行参数到游戏呢?

官方例子 Nullrhi 参数

下面我们参考 UnrealTestConfiguration 是如何把 Nullrhi 这个参数传入游戏中的

我们回到 Automation 项目中,查看类型 我们使用的配置类型 UnrealTestConfig 的基类 Gauntlet.UnrealTestConfiguration

其中有一个函数是

1
public virtual void ApplyToConfig(UnrealAppConfig AppConfig, UnrealSessionRole ConfigRole, IEnumerable<UnrealSessionRole> OtherRoles)

这个函数的作用就是组装测试的配置,其中 UnrealAppConfig 就是实例启动的相关参数

在 ApplyToConfig 函数中有这几行代码

1
2
3
4
if (Nullrhi)
{
AppConfig.CommandLine += " -nullrhi";
}

程序读取了 Nullrhi 这个成员变量,并判断如果为 True 则在 AppConfig 的命令行参数,也就是实例启动的命令行参数中加入 “ -nullrhi” ,从而让客户端不进行渲染

1
2
3
4
5
6
/// <summary>
/// Use a nullrhi for tests
/// </summary>
///
[AutoParam(false)]
protected bool Nullrhi { get; set; }

Nullrhi 成员使用了注解 [AutoParam(false)]

这个注解的作用是直接从 RunUnreal 的命令行获取参数进行初始化,这个命令行传入有两种写法

我们下面来尝试用这两种写法来使用 nullrhi 参数

修改我们的 RunGauntlet.bat

方法一

1
2
3
4
5
6
7
8
9
10
11
12
@echo on

set ProjectPath=项目目录
set StagingDir=%ProjectPath%\Saved\StagedBuilds\
set EnginePath=引擎目录

"%EnginePath%\Engine\Build\BatchFiles\RunUAT" RunUnreal -project=%ProjectPath%\GauntletTutorial.uproject ^
-build=%StagingDir% -platform=Win64 -configuration=development ^
-test=GauntletTutorial.Automation.GauntletTutorialTest ^
-nullrhi ^
-scriptdir=%ProjectPath%
pause

这里面,我们直接把 nullrhi 当做 RunUnreal 的命令行参数传入

方法二

1
2
3
4
5
6
7
8
9
10
11
@echo on

set ProjectPath=项目目录
set StagingDir=%ProjectPath%\Saved\StagedBuilds\
set EnginePath=引擎目录

"%EnginePath%\Engine\Build\BatchFiles\RunUAT" RunUnreal -project=%ProjectPath%\GauntletTutorial.uproject ^
-build=%StagingDir% -platform=Win64 -configuration=development ^
-test=GauntletTutorial.Automation.GauntletTutorialTest(nullrhi) ^
-scriptdir=%ProjectPath%
pause

这里面,我们可以在 test 参数中的的 Gauntlet 测试类型后打一个括号,传入我们的 nullrhi 参数

不管是哪种方式,修改后运行批处理,可以发现客户端没有渲染了,可知是 nullrhi 已经传入了游戏中,其完整流程为

  1. 通过命令行,参数被 AutoParam 传给了测试的配置类中的成员变量
  2. 在配置类的 ApplyToConfig 函数中,通过判断成员变量的值,并在 AppConfig 的 Commandline 中加入到游戏命令行

ApplyToConfig 也不光是可以修改命令行,其他用法也可以根据官方代码学习

尝试传入 ErrorTest 所需参数

我们使用的 Controller 中需要传入命令行参数 errortest.delay 和 errotest.type

于是乎,我们按照上面 nullrhi 的做法,先写一个新的配置类型,继承 UnrealTestConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System;
using System.Collections.Generic;
using Gauntlet;

namespace GauntletTutorial.Automation
{
public class GauntletTutorialConfig : UnrealTestConfiguration
{
[AutoParam(5)]
private float ErrorDelay { get; set; }

[AutoParam]
private String ErrorType { get; set; }

public override void ApplyToConfig(UnrealAppConfig AppConfig, UnrealSessionRole ConfigRole,
IEnumerable<UnrealSessionRole> OtherRoles)
{
base.ApplyToConfig(AppConfig,ConfigRole,OtherRoles);
if (ErrorDelay > 0)
{
AppConfig.CommandLine += " -errortest.delay=" + ErrorDelay;
AppConfig.CommandLine += " -errortest.type=" + ErrorType;
}
}

}
}

然后修改我们的 GauntletTutorialTest 的泛型参数为刚创建的新配置类型

1
2
3
4
5
6
7
8
9
...
public class GauntletTutorialTest : UnrealTestNode<GauntletTutorialConfig>
{
public GauntletTutorialTest(UnrealTestContext inContext) : base(inContext)
{
}

public override GauntletTutorialConfig GetConfiguration()
...

然后修改 RunGauntlet.bat 加入参数 ErrorDelay 和 ErrorType

1
2
3
4
5
6
7
8
9
10
11
@echo on

set ProjectPath=项目目录
set StagingDir=%ProjectPath%\Saved\StagedBuilds\
set EnginePath=引擎目录

"%EnginePath%\Engine\Build\BatchFiles\RunUAT" RunUnreal -project=%ProjectPath%\GauntletTutorial.uproject ^
-build=%StagingDir% -platform=Win64 -configuration=development ^
-test=GauntletTutorial.Automation.GauntletTutorialTest(ErrorDelay=20,ErrorType=ensure) ^
-scriptdir=%ProjectPath%
pause

重新启动 RunGauntlet.bat ,发现 20 秒后,提示 ensure ,返回测试失败,我们的参数成功传入到游戏中

至此,我们的整个 Gauntlet 基本流程就算完成了

实战:实现一个基本的测试需求

需求

我们现在需要测试在 N 个客户端开启后登入一个服务器,确定所有客户端正确登入服务器则测试成功

创建测试配置类型

创建新的测试配置类型,其中指定一个参数 ClientCount,通过 RunUnreal 命令行传入,用于启动 N 个 Client 实例

然后用 AppConfig 通过命令行参数 -ClientCountToCheck 传入游戏中

可以看到我们这里使用的基类 Config 为 EpicGameTestConfig

使用这个的原因是里面包含了让客户端自动连接服务器的逻辑

LoginTestConfig.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System.Collections.Generic;
using Gauntlet;

namespace GauntletTutorial.Automation
{
public class LoginTestConfig : EpicGameTestConfig
{
[AutoParam(1)]
public int ClientCount { get; set; }

public override void ApplyToConfig(UnrealAppConfig AppConfig, UnrealSessionRole ConfigRole, IEnumerable<UnrealSessionRole> OtherRoles)
{
base.ApplyToConfig(AppConfig, ConfigRole, OtherRoles);
AppConfig.CommandLine += " -ClientCountToCheck=" + ClientCount;
}
}
}

创建测试类型

在测试中,我们直接把配置中的 ClientCount 取出来,创建相应数量的客户端

然后服务器设置 TestController 为 GauntletTestLoginController,客户端不需要 Controller 所以不设置

设置 NoMCP 为 true,这样可以跳过 Gauntlet 内部客户端登录服务器的账号验证等步骤

如 60 秒还没测试成功,则判断为失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System.Collections.Generic;
using Gauntlet;

namespace GauntletTutorial.Automation
{
public class LoginTest : UnrealTestNode<LoginTestConfig>
{
public LoginTest(UnrealTestContext inContext) : base(inContext)
{
}

public override LoginTestConfig GetConfiguration()
{
LoginTestConfig config = base.GetConfiguration();
UnrealTestRole server = config.RequireRole(UnrealTargetRole.Server);
server.Controllers.Add("GauntletTestLoginController");
// 直接启动配置数量的客户端
config.RequireRoles(UnrealTargetRole.Client, config.ClientCount);
// 60 秒后超时,判定为测试失败
config.MaxDuration = 60;
config.MaxDurationReachedResult = EMaxDurationReachedResult.Failure;
config.NoMCP = true;
return config;
}
}
}

创建 GauntletController

首先,在游戏项目中的 GauntletTutorial.build.cs 中

PublicDependencyModuleNames 中加入 “Gauntlet”

然后创建新 C++ 类型 UGauntletTestLoginController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GauntletTestController.h"
#include "GauntletTestLoginController.generated.h"

/**
*
*/
UCLASS()
class GAUNTLETTUTORIAL_API UGauntletTestLoginController : public UGauntletTestController
{
GENERATED_BODY()

virtual void OnInit() override;
virtual void OnTick(float TimeDelta) override;

UFUNCTION()
void OnClientLogin(AGameModeBase* GameMode, APlayerController* NewPlayer);

int32 CurrentClientCount;
int32 ClientCountShouldLogin;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Fill out your copyright notice in the Description page of Project Settings.


#include "GauntletTestLoginController.h"
#include "GameFramework/GameModeBase.h"

void UGauntletTestLoginController::OnInit()
{
Super::OnInit();
CurrentClientCount = 0;

FParse::Value(FCommandLine::Get(), TEXT("ClientCountToCheck="), ClientCountShouldLogin);
FGameModeEvents::GameModePostLoginEvent.AddUObject(this,&UGauntletTestLoginController::OnClientLogin);

}

void UGauntletTestLoginController::OnTick(float TimeDelta)
{
Super::OnTick(TimeDelta);
// 全部都登入啦
if (CurrentClientCount == ClientCountShouldLogin)
{
EndTest(0);
}
}

void UGauntletTestLoginController::OnClientLogin(AGameModeBase* GameMode, APlayerController* NewPlayer)
{
CurrentClientCount++;
}

我们注册全局登入事件,然后判断登录的客户端数量是否能达到配置的人数

如果进入的数量和配置的数量相等了,则结束测试,返回代表成功的结果0

编写测试批处理

在 [ProjectDir]\Scripts 目录下创建 RunLoginTest.bat ,test 传入 LogintTest, 并传入参数 ClientCount 5

1
2
3
4
5
6
7
8
9
10
11
@echo on

set ProjectPath=项目目录
set StagingDir=%ProjectPath%\Saved\StagedBuilds\
set EnginePath=引擎目录

"%EnginePath%\Engine\Build\BatchFiles\RunUAT" RunUnreal -project=%ProjectPath%\GauntletTutorial.uproject ^
-build=%StagingDir% -platform=Win64 -configuration=development ^
-test=GauntletTutorial.Automation.LoginTest(ClientCount=5) ^
-scriptdir=%ProjectPath%
pause

打包,运行测试

运行 Pack.bat 打包

然后运行 RunLoginTest.bat 开启测试

可以看到当 6 个 device 都启动后,测试显示成功

然后查看 Server 的日志

日志在 [Engin]\GauntletTemp\DeviceCache\Win64[LocalDevice”N”]\UserDir\Saved\Logs 中

这里的服务器大概率应该是 LocalDevice0,其他的客户端应该是 LocalDevice1 ~ 5

日志中显示 5 个客户端登录成功,我们的需求完成

调试 RunUnreal 和 游戏实例

我们在学习和测试中,经常会有需要调试 Gauntlet Automation 或者在特定实例调试的情况

调试 RunUnreal

这时我们只需在 RunUAT 命令行参数中加入 -waitfordebugger

再运行 Gauntlet 测试,就会在启动时等待调试器接入

然后我们在 Automation 项目代码中打好断点,然后在 Rider 中选择 Run -> AttachToProcess

然后过滤输入 Automation ,在进程中选中 dotnet(AutomationTool.dll) 开始调试即可

调试特定游戏实例

我们知道游戏中传入参数 -waitfordebugger 就可以等待调试器接入

所以我们只需按照之前传参到特定实例的方法传入参数

然后在游戏项目中打好断点,attach 到游戏进程即可

相关学习和参考

现在我们已经了解了 Gauntlet 的基本使用

如果需要了解和学习更多的用法,可以

  • 查看 Gauntlet Automation 项目中相关源代码,学习借鉴其中的用法
  • 查看相关测官网文档
  • 查看一些使用案例

可供参考的项目

DaedalicTestAutomation 插件

Github 地址

这个插件写了一套测试框架,然后也集成了使用 Gauntlet 来启动测试的方式,很有参考价值

ShooterGame 示例项目

官方示例项目中,包含了一些 Gauntlet Automation 和 Gauntlet Test Controller 的使用实例

本文项目的 Github 仓库

Github 仓库地址