[UE4] Gauntlet Automation Framework

#1. はじめに
　**Gauntlet Automation Framework**は4.21で追加された機能の1つで、**自動テストを行うことを目的としたフレームワーク**です。[4.21のリリースノート](https://www.unrealengine.com/ja/blog/unreal-engine-4-21-released)にその概要が記載されていますが、要約すると、プロジェクトに即したテストスクリプト(.cs, .cpp)を作成して実行することで様々なテストを実行することができます。アーリーアクセスの機能であるため、以降のバージョンで構成や使い方が変わることがあるということにご留意頂いた上でご利用下さい。
　本記事では4.21.1にて確認しています。
#2. 機能
　Gauntlet Automation Frameworkは自動テストを実行する機能ということを説明しましたが、これはRunUAT(AutomationTool)を使用したテストを実行します。つまりJenkinsなどと連動することで定期的にテストを実行することも可能です。
以下の図は**Frameworkを使用したテスト(Gauntlet Test)**の流れについて説明したものです。
![2018-12-20_02h05_53.png](https://qiita-image-store.s3.amazonaws.com/0/101019/8144d2d6-f9df-d023-3ad8-9471bba11fde.png)
　Gauntlet Testは主に以下の流れで実行します。

**① テストの起動**
RunUATからテストコマンドを実行してテストを開始します。CIとしてJenkinsが用いられることがしばしばありますが、同じようにテスト用の.batファイルなどを予め作成しておき、実行することと同じようなイメージです。

**② アプリケーションの実行**
テストを実行する前にStagedBuildにあるコンテンツ(Cook済みコンテンツ)を起動します。もし実行するデバイスの指定がある場合は、StagedBuildを元にアプリケーションを実行してデバイスを起動します。

**③ テスト実行**
予め作成したテストスクリプトを元にテストを開始します。アプリケーションはテストに基づいて動作します。

**④ テスト完了**
アプリケーションからテスト結果を返却された場合はテストを終了します。結果が返却されない場合においてもテストスクリプトが自身で結果を返却してテストを完了します。

**⑤ レポート**
テスト結果を元にLogや.csvファイルにレポートを出力します。

　簡単な説明ですが、以上がテスト実行時の一連の流れとなります。図中の青色/緑色が主なGauntlet Frameworkが提供する機能となりますが、この部分については後の章にて詳細を説明します。

#3. 使い方
　この章では、主にGauntlet Testを実行する方法を説明しています。ここでは、簡単なテストスクリプトを実行する方法や、自身でバッチファイルを作成するといった、基礎的な手順について説明します。
##3.1. サンプルテストを実行する
　まずはじめにGauntletTestを実行するための手順について記載します。Gauntlet Testには簡単なテストスクリプト(ElementalDemoTest.cs)が予め用意されていますので、Engineをダウンロードした直後からすぐに試すことができます。以下にサンプルを実行するための手順を示します。

**[実行手順]**
① Staging Buildの作成
② テスト実行
###3.1.1. Staging Buildの作成
　Staging Buildはパッケージ作成などを行うと作成されるもので、[PROJECT]/Saved/StagedBuildsに作成されます。ここではプロジェクトをパッケージすることを行います。もし自動化する場合など、コマンドで実行する場合は以下のようなコマンドで作成することができます。

```:StagingBuildの作成のコマンド例
Build\BatchFiles\RunUAT.bat BuildCookRun -project=ElementalDemo -platform=Win64 -configuration=Development -build -cook -pak -stage
```
###3.1.2. テスト実行
　Staging Buildを作成したら後はテストを実行するだけです。ここでは予め作成したテストスクリプトをRunUATから実行します。以下はコマンドで実行する場合などは以下のようなコマンドでテストを実行することができます。以下はWindowsでElementalDemoのプロジェクトを実行する例です。

```:テスト実行のコマンド例
Engine\Build\BatchFiles\RunUAT.bat RunUnreal -project=ElementalDemo -platform=Win64 -configuration=Development -test=ElementalDemoTest -build=Samples\Showcases\ElementalDemo\Saved\StagedBuilds
```

　もしテストの実行にうまくいけば、アプリケーションを起動してテストを開始します。テストスクリプトのサンプルは**「単にアプリケーションを起動し50分経過でタイムアウト」**を行うスクリプトです。よって、アプリケーションを起動したらそのままの状態を維持します。もしこのアプリケーションを終了した場合(Alt+F4など)にはテストを終了して以下のような結果が得られます。もしここまでの動作が確認できれば、Gauntlet Testを実施するための手順は一通り完了です。
![2018-12-20_00h18_21.png](https://qiita-image-store.s3.amazonaws.com/0/101019/dae46608-709e-5928-0d55-228e99ca4cf6.png)
テストのレポートはデフォルトでは、[ENGINE_ROOT]\GauntletTemp\Logs\に出力されます。
##3.2. テスト実施のためのバッチファイルを作成する
　次に自身のプロジェクトで自動テストを実行できるようにするために、テスト実施のためのバッチファイルを作成する方法について説明します。以下はテストを実行するための基本的なバッチファイルです。これは3.1.2.で説明した「テスト実行のコマンド例」を編集したものですが、このコマンドを元にバッチファイルを作成することができます。

　このバッチファイルでは**Githubから取得したEngineを利用**して、独自のプロジェクト**GauntletTest**に対してテストサンプル**ElementDemoTest**のテストを**Win64:Development**の構成で実行するようなケースの例です。パスやプロジェクト名を書き換えることで、基本的にそのまま使用することができますが、プロジェクトによって独自にカスタマイズすることもできます。

```:MyGauntletTest.bat
rem // RunUAT.batへのパス
set UAT_PATH=G:\GitHub\UnrealEngine-4.21\Engine\Build\BatchFiles\RunUAT.bat
rem // プロジェクト名 
set PRJ_NAME=GauntletTest
rem // ステージングディレクトリのパス
set STAGING_DIR=G:\Projects\GauntletTest\Saved\StagedBuilds
rem // テスト実行コマンド
set TEST_CMD=RunUnreal
rem // テスト名
set TEST_NAME=ElementalDemoTest
rem // プラットフォーム
set PLATFORM=Win64
rem // ビルドコンフィグレーション
set CONFIG=Development

rem ********************* Start Gauntlet Test *********************
%UAT_PATH% %TEST_CMD% -project=%PRJ_NAME% -platform=%PLATFORM% -configuration=%CONFIG% -test=%TEST_NAME% -build=%STAGING_DIR% 
rem ********************* End   Gauntlet Test *********************
pause
```

| 変数		|機能					|
|:--------------|:--------------------------------	|
| UAT_PATH	|RunUAT.batを実行できればどういった指定でも構いません			|
| PRJ_NAME	|プロジェクト(.uprojectは不要)を指定します					|
| TEST_CMD	|テストコマンド、BuildCommandの継承クラス(RunUnreal)を指定できます		|
| TEST_NAME	|DefaultNodeの継承クラス							|
| PLATFORM	|プラットフォーム(Win64/Andorid/PS4など)を指定します			|
| CONFIG	|ビルドコンフィグ(Shipping/Developmentなど)を指定します			|

#4. フレームワーク
　「Gauntlet Testの流れ」で説明した図中にあるフレームワーク部分について説明します。

![2018-12-20_02h05_53.png](https://qiita-image-store.s3.amazonaws.com/0/101019/213eef6a-8a03-c2d4-b2f8-1a582fde3aed.png)

**起動スクリプト**
\Engine\Source\Programs\AutomationTool\Gauntlet\Unreal\RunUnreal.cs
GauntletTest用の実行クラスです。テスト開始をTest Executerに通知したり、テストを実行するデバイスのセットアップを行う機能を所有しています。必要に応じて継承することで、テスト実行前のデバイス起動やアカウント認証などのステップを追加することができます。

**Test実行/管理**
\Engine\Source\Programs\AutomationTool\Gauntlet\Framework\Gauntlet.TestExecutor.cs
主にテストの実行管理を行うクラスです。基本的には起動スクリプトからテストの開始を受け取ってテストスクリプトを実行したり、テストの実行結果を管理する機能を所有します。

**テストスクリプト**
\Engine\Source\Programs\AutomationTool\Gauntlet\Unreal\Base\Gauntlet.UnrealTestNode.cs
テスト用スクリプトの基底クラスです。TestStartやTickのコールバック関数が用意されているので、テストに必要なコードを書くことでテスト実行中の動作を制御することができます。

**テスト制御**
\Engine\Plugins\Experimental\Gauntlet\Source\Gauntlet\Public\GauntletTestController
GauntletTestを行う上での基底クラスです。Pluginを有効にすることで機能します。基本的にはアプリケーションに組み込まれる内容であり、ゲームコードに直接アクセスしてゲームを制御する際に使用します。各プロジェクトで自動テストを追加する場合は、このクラスを基底クラスとしたサブクラスを作成してテストコードを追加します。基底クラスでは基本モジュールのみを提供するクラスで、派生クラスで開始および監視するようなロジックを実装することもできます。TestControllerを指定して実行する場合はコマンドラインパラメータ(-gauntlet=MyControllerName)を使用します。

#5. 自動テストのカスタマイズ
　この章ではプロジェクトで独自のテストを作成するために必要な知識やサンプルについて説明します。

##5.1. サンプルとソースコードのカスタマイズ
 もしプロジェクトで自動テストを実施するような場合、Engineが提供するクラスを派生したりすることで独自のテストを作成することができます。「Gauntlet Testの流れ」で説明した図において、主に拡張するようなクラスは以下の赤塗のクラスになります。あくまでこれは一例ですが、他のクラスも拡張したり改造することで、より効率的なテストやプロジェクトに沿ったテストを提供することも可能です。
![2018-12-20_02h05_43.png](https://qiita-image-store.s3.amazonaws.com/0/101019/4413a5d4-2b09-1c78-8965-e904cbed1349.png)

　以下はデフォルトで提供されているスクリプトやクラスのサンプルになります。もし独自のテスト環境やスクリプトを作成するにあたって参考にすることができます。

**GauntletTestサンプルテスト1**
\Engine\Source\Programs\AutomationTool\Gauntlet\Unreal\Game\Samples\ElementalDemoTest.cs
UnrealTestNodeの継承クラスで、50分のタイムアウトのみを持ちタイムアウト発生でテスト結果を失敗とします。

**GauntletTestサンプルテスト2**
\Engine\Source\Programs\AutomationTool\Gauntlet\Unreal\Game\Samples\FortGPUTestbedPerfTest.cs
UnrealTestNodeの継承クラスで、5分のタイムアウトか特定の入力を検出してテストを終了します。

**起動テスト用コントローラ**
\Engine\Plugins\Experimental\Gauntlet\Source\Gauntlet\Private\GauntletTestControllerBootTest.cpp
UGauntletTestControllerのサブクラスでアプリケーションの起動チェックを行うテストのサンプルです。

**エラーテスト用コントローラ**
\Engine\Plugins\Experimental\Gauntlet\Source\Gauntlet\Private\GauntletTestControllerErrorTest.cpp
UGauntletTestControllerのサブクラスでアプリケーションのエラー検出を行うテストのサンプルです。

**FGauntletModuleImpl**
GauntletTestの実行モジュールです。主にGauntletTestControllerの制御を行います。PluginのTypeがDevelopmentのため、デフォルトではランタイムで実行できません。これを有効にするにはTypeを”Runtime”に変更する必要があります。~~（おそらくこれはバグかもしれない！）~~ 
※追記：UE4.22以降? の直近のバージョンではRuntimeに修正されています。

##5.2. サンプルプロジェクトの詳細
　ここでは上記で提示したサンプルテストやコントローラクラスの動作について説明しています。以下のコードはサンプルを改変してコメントを追記したものになります。ここに記載されてる内容を参考にすることで、サンプルがどのように動作するかを把握して頂き、サンプルから拡張するにあたっての目安となるかと思います。

**テストのサンプル**

```cs:ElementalDemoTest.cs

namespace UE4Game
{
	/// テストクラス(テスト実行時に引数として渡す名称)を定義します
	/// テストコードはDefaultNodeのサブクラスとして定義します
	public class ElementalDemoTest : DefaultNode
	{
		public ElementalDemoTest(Gauntlet.UnrealTestContext InContext)
			: base(InContext)
		{
		}

		/// テスト用のコンフィグを定義します
		/// 例えば、MaxDurationはテストのタイムアウト時間を指定します
		/// ※ "5min" とコメントがありますが実際には5*600(秒)=50(分)でタイムアウトが発生します 
		public override UE4TestConfig GetConfiguration()
		{
			// just need a single client
			UE4TestConfig Config = base.GetConfiguration();
			UnrealTestRole ClientRole = Config.RequireRole(UnrealTargetRole.Client);
			ClientRole.CommandLine += " -unattended";
			Config.MaxDuration = 5 * 600;		// 5min should be plenty
			return Config;
		}

		/// テスト結果のレポートを作成するモジュールで、テスト終了時に呼ばれます
		/// 本モジュールにはHealth Snapshot(詳細は次章参照)の結果を出力する処理が記載されています
		public override void CreateReport(TestResult Result, UnrealTestContext Contex, UnrealBuildSource Build, IEnumerable<UnrealRoleArtifacts> Artifacts, string ArtifactPath)
		{
			UnrealRoleArtifacts ClientArtifacts = Artifacts.Where(A => A.SessionRole.RoleType == UnrealTargetRole.Client).FirstOrDefault();
			var SnapshotSummary = new UnrealSnapshotSummary<UnrealHealthSnapshot>(ClientArtifacts.AppInstance.StdOut);
			Log.Info("Elemental Performance Report");
            Log.Info(SnapshotSummary.ToString());
			base.CreateReport(Result, Contex, Build, Artifacts, ArtifactPath);
		}

		/// テストの結果を保存するモジュールで、テスト終了時に呼ばれます
		/// 本モジュールではFPSChartの結果を指定のDirectoryに.zipで保存する処理を行います
		/// テスト実行時に引数として"-uploadfolder=[FULLPATH]"で出力先を指定することで.zipファイルを作成します
		/// FPSChartの結果を保存するため、テスト中にStartFPSChart/StopFPSChartを実行することで機能します
		public override void SaveArtifacts_DEPRECATED(string OutputPath)
		{
			string UploadFolder = Globals.Params.ParseValue("uploadfolder", "");
			if (UploadFolder.Count() > 0 && Directory.CreateDirectory(UploadFolder).Exists)
			{
				string PlatformString = TestInstance.ClientApps[0].Device.Platform.ToString();
				string ArtifactDir = TestInstance.ClientApps[0].ArtifactPath;
				string ProfilingDir = Path.Combine(ArtifactDir, "Profiling");
				string FPSChartsDir = Path.Combine(ProfilingDir, "FPSChartStats").ToLower();
				string FpsChartsZipPath = Path.Combine(TestInstance.ClientApps[0].ArtifactPath, "FPSCharts.zip").ToLower();
				if (Directory.Exists(FPSChartsDir))
				{
					ZipFile.CreateFromDirectory(FPSChartsDir, FpsChartsZipPath);
					string DestFileName = "ElementalDemoTest-" + PlatformString + ".zip";
					string DestZipFile = Path.Combine(UploadFolder, DestFileName);
					File.Copy(FpsChartsZipPath, DestZipFile);
				}
			}
			else
			{
				Log.Info("Not uploading CSV Result UploadFolder: '" + UploadFolder + "'");
			}
		}
	}
}

```

**コントローラのサンプル**

```cpp:GauntletTestControllerBootTest.cpp
#include "GauntletTestControllerBootTest.h"

/// テスト実行時に毎フレーム呼び出されます
void UGauntletTestControllerBootTest::OnTick(float TimeDelta)
{
	/// 起動プロセスを完了した場合「テスト結果：正常」としてテストを終了します
	/// 実際にはIsBootProcessComplete()は常にfalseを返すため通過しません
	if (IsBootProcessComplete())
	{
		EndTest(0);
	}
	else
	{
		/// テスト開始から300s(秒)を経過した場合「テスト結果：異常」としてテストを終了します
		/// 実際にはGetTimeInCurrentState()は常に0を返すため通過しません
		if (GetTimeInCurrentState() > 300)
		{
			UE_LOG(LogGauntlet, Error, TEXT("Failing boot test after 300 secs!"));
			EndTest(-1);
		}
	}
}
```

#6. パフォーマンスレポート
　ReleaseNoteに記載されているように、4.21から以下のようなパフォーマンスレポートを出力する機能が追加されました。これはブループリントでも利用可能ですが、テストに組み込むことも可能です。
![2018-12-20_01h03_10.png](https://qiita-image-store.s3.amazonaws.com/0/101019/13e1e1c4-0553-0594-7d99-28a2451b5c32.png)
　以下はBlueprintでレポートを取得する際の例です。
![2018-12-05_02h05_28.png](https://qiita-image-store.s3.amazonaws.com/0/101019/dd31aa01-9362-a6f7-5190-902edad1ed97.png)
　このパフォーマンスを取得する機能は**Health Snapshot**と呼ばれているもので、特定の2点間のパフォーマンスログをキャプチャすることができます。以下に主要な3つのイベントについて説明します。

**・Start Performance Snapshots**
Health Snapshotの取得を開始します。

**・Log Performance Snapshot**
2点間のRecordを記録します。"Reset Stats"のフラグによってRecordのキャプチャ方式を変更します。

**・Stop Performance Snapshots**
Health Snapshotの取得を終了して出力します。

　もしサンプルのElementalDemoTest.csでこれらの出力を確認する際には、予めSnapshotとSummlyの**Title**を一致させておく必要があり、テストの実行中にHealthSnapshotをキャプチャします。以下は"LogPerf"というTitleで一致させてからSnapshotを取得するための例となります。

・Bluprintで"LogPerf"でSnapshotを取得します
![2018-12-20_01h02_05.png](https://qiita-image-store.s3.amazonaws.com/0/101019/8b2dd5ce-8931-0ef1-1dd7-27f84733f852.png)
・Reportを作成する際に"LogPerf"のSnapshotを取得します

```cpp:ElementalDemoTest.cs
var SnapshotSummary = new UnrealSnapshotSummary<UnrealHealthSnapshot>(ClientArtifacts.AppInstance.StdOut, "LogPerf");
```

#7. まとめ
　開発では最終的に人間がテストを実施し、そのパフォーマンスやコンテンツの品質を確認する必要がありますが、SmokeTestやBlackBoxTestなど単純なテストや定期的に実行するようなものは出来るだけ自動化しておくのが効率的です。これらの機能を利用することで、より効率的にエラーを検出したり早期に問題を検出することができるため、是非プロジェクトに沿ったテストを作成してご活用ください。