diff --git a/.gitignore b/.gitignore index 6582eaf..ccf07d9 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,6 @@ Plugins/*/Intermediate/* # Cache files for the editor to use DerivedDataCache/* +DaedalicTestAutomationPlugin.Automation/obj/ +DaedalicTestAutomationPlugin/Binaries/ +DaedalicTestAutomationPlugin/Intermediate/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8925126 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,47 @@ +# Contributing + +You'd like to help make Daedalic Test Automation Plugin even more awesome? Seems like today's our lucky day! In order to maintain stability of the plugin and its code base, please adhere to the following steps, and we'll be pleased to include your additions in our next release. + +Note that Daedalic Test Automation Plugin is distributed under the [MIT License](https://github.com/DaedalicEntertainment/ue4-test-automation/blob/develop/LICENSE). So will be your code. + +## How to contribute + +### Step 1: Choose what to do + +If you've got no idea how to help, head over to our [issue tracker](https://github.com/DaedalicEntertainment/ue4-test-automation/issues) and see what you'd like to do most. You can basically pick anything you want to, as long as it's not already assigned to anyone. + +If you know exactly what you're missing, [open a new issue](https://github.com/DaedalicEntertainment/ue4-test-automation/issues/new) to begin a short discussion about your idea and how it fits the project. If we all agree, you're good to go! + +### Step 2: Fork the project and check out the code + +Daedalic Test Automation Plugin is developed using the [GitFlow branching model](https://nvie.com/posts/a-successful-git-branching-model). In order to contribute, you should check out the latest `develop` branch, and create a new feature or hotfix branch to be merged back. + +### Step 3: Implement your feature or bugfix + +Clearly, everybody's got their own approach here. However, we'd still like you to keep a few things in mind, to ensure the stability and consistency of the plugin for everyone: + +* We're using our own [Coding Conventions](https://github.com/DaedalicEntertainment/unreal-coding-conventions), which are largely based on the official [Coding Standard](https://docs.unrealengine.com/latest/INT/Programming/Development/CodingStandard/index.html) provided by Epic Games. If you're used to working with that one, in general, you should be fine. +* When you're adding support for a newer engine version, make sure that you don't break support for previously supported engine versions. We must not force our users to upgrade their engine just because of updating the plugin. +* Note that renaming or reordering parameters is a breaking change. This includes the blueprint macro libraries shipped with the plugin. You should avoid that, if possible. + +### Step 4: Open a pull request + +Finally, [open a pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request) so we can review your changes together, and finally integrate it into the next release. + + +## Release Checklist + +Internally, we're using the following checklist when preparing for a new release: + +* Check pending pull requests +* Create release branch +* Add examples for new features where appropriate +* Run all tests +* Update documentation (README, images, spelling, table of contents) +* Increase version number (and engine version, if necessary) +* Create plugin package +* Check plugin package in another project +* Merge release branch with tag +* Add a new GitHub release with release notes +* Update GitHub issues and milestones +* Notify community (e.g. forums) diff --git a/DaedalicTestAutomationPlugin.Automation/DaeGauntletTest.cs b/DaedalicTestAutomationPlugin.Automation/DaeGauntletTest.cs new file mode 100644 index 0000000..c9715a1 --- /dev/null +++ b/DaedalicTestAutomationPlugin.Automation/DaeGauntletTest.cs @@ -0,0 +1,25 @@ +using Gauntlet; + +namespace DaedalicTestAutomationPlugin.Automation +{ + public class DaeGauntletTest : UnrealTestNode + { + public DaeGauntletTest(UnrealTestContext InContext) : base(InContext) + { + } + + public override DaeTestConfig GetConfiguration() + { + DaeTestConfig Config = base.GetConfiguration(); + + // Start a single instance of the game. + UnrealTestRole ClientRole = Config.RequireRole(UnrealTargetRole.Client); + ClientRole.Controllers.Add("DaeGauntletTestController"); + + // Ignore user account management. + Config.NoMCP = true; + + return Config; + } + } +} diff --git a/DaedalicTestAutomationPlugin.Automation/DaeTestConfig.cs b/DaedalicTestAutomationPlugin.Automation/DaeTestConfig.cs new file mode 100644 index 0000000..baef753 --- /dev/null +++ b/DaedalicTestAutomationPlugin.Automation/DaeTestConfig.cs @@ -0,0 +1,35 @@ +using Gauntlet; +using System.Collections.Generic; + +namespace DaedalicTestAutomationPlugin.Automation +{ + public class DaeTestConfig : EpicGame.EpicGameTestConfig + { + /// + /// Where to write a JUnit XML report to. + /// + [AutoParam] + public string JUnitReportPath; + + /// + /// Which single test to run, instead of all available tests. + /// + [AutoParam] + public string TestName; + + public override void ApplyToConfig(UnrealAppConfig AppConfig, UnrealSessionRole ConfigRole, IEnumerable OtherRoles) + { + base.ApplyToConfig(AppConfig, ConfigRole, OtherRoles); + + if (!string.IsNullOrEmpty(JUnitReportPath)) + { + AppConfig.CommandLine += string.Format(" JUnitReportPath=\"{0}\"", JUnitReportPath); + } + + if (!string.IsNullOrEmpty(TestName)) + { + AppConfig.CommandLine += string.Format(" TestName=\"{0}\"", TestName); + } + } + } +} diff --git a/DaedalicTestAutomationPlugin.Automation/DaedalicTestAutomationPlugin.Automation.csproj b/DaedalicTestAutomationPlugin.Automation/DaedalicTestAutomationPlugin.Automation.csproj new file mode 100644 index 0000000..5a0515f --- /dev/null +++ b/DaedalicTestAutomationPlugin.Automation/DaedalicTestAutomationPlugin.Automation.csproj @@ -0,0 +1,55 @@ + + + + + Debug + AnyCPU + {C008207C-E5DF-41F3-B0ED-6A1A80F7234B} + Library + Properties + DaedalicTestAutomationPlugin.Automation + DaedalicTestAutomationPlugin.Automation + v4.6.2 + 512 + true + + + true + full + false + $(UNREAL_ENGINE_4_PATH)\Engine\Binaries\DotNET\AutomationScripts\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + false + $(UNREAL_ENGINE_4_PATH)\Engine\Binaries\DotNET\AutomationScripts\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + {767B4F85-AB56-4B00-A033-04C7600ACC3D} + Gauntlet.Automation + + + + \ No newline at end of file diff --git a/DaedalicTestAutomationPlugin.Automation/Properties/AssemblyInfo.cs b/DaedalicTestAutomationPlugin.Automation/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..05e2b02 --- /dev/null +++ b/DaedalicTestAutomationPlugin.Automation/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("DaedalicTestAutomationPlugin.Automation")] +[assembly: AssemblyDescription("Tells the Unreal Automation Tool to use our custom Gauntlet controllers.")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Daedalic Entertainment GmbH")] +[assembly: AssemblyProduct("DaedalicTestAutomationPlugin.Automation")] +[assembly: AssemblyCopyright("Copyright 2020 Daedalic Entertainment GmbH")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c008207c-e5df-41f3-b0ed-6a1a80f7234b")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/DaedalicTestAutomationPlugin/Content/BP_DaeTestBlueprintMacroLibrary.uasset b/DaedalicTestAutomationPlugin/Content/BP_DaeTestBlueprintMacroLibrary.uasset new file mode 100644 index 0000000..fbd96ff Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/BP_DaeTestBlueprintMacroLibrary.uasset differ diff --git a/DaedalicTestAutomationPlugin/Content/TestAlwaysFail/BP_TestAlwaysFail.uasset b/DaedalicTestAutomationPlugin/Content/TestAlwaysFail/BP_TestAlwaysFail.uasset new file mode 100644 index 0000000..9bea002 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestAlwaysFail/BP_TestAlwaysFail.uasset differ diff --git a/DaedalicTestAutomationPlugin/Content/TestAlwaysFail/TestAlwaysFail.umap b/DaedalicTestAutomationPlugin/Content/TestAlwaysFail/TestAlwaysFail.umap new file mode 100644 index 0000000..2beaf7c Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestAlwaysFail/TestAlwaysFail.umap differ diff --git a/DaedalicTestAutomationPlugin/Content/TestAlwaysSkip/BP_TestAlwaysSkip.uasset b/DaedalicTestAutomationPlugin/Content/TestAlwaysSkip/BP_TestAlwaysSkip.uasset new file mode 100644 index 0000000..f3668d7 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestAlwaysSkip/BP_TestAlwaysSkip.uasset differ diff --git a/DaedalicTestAutomationPlugin/Content/TestAlwaysSkip/TestAlwaysSkip.umap b/DaedalicTestAutomationPlugin/Content/TestAlwaysSkip/TestAlwaysSkip.umap new file mode 100644 index 0000000..b87f74f Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestAlwaysSkip/TestAlwaysSkip.umap differ diff --git a/DaedalicTestAutomationPlugin/Content/TestAlwaysSucceed/BP_TestAlwaysSucceed.uasset b/DaedalicTestAutomationPlugin/Content/TestAlwaysSucceed/BP_TestAlwaysSucceed.uasset new file mode 100644 index 0000000..650d779 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestAlwaysSucceed/BP_TestAlwaysSucceed.uasset differ diff --git a/DaedalicTestAutomationPlugin/Content/TestAlwaysSucceed/TestAlwaysSucceed.umap b/DaedalicTestAutomationPlugin/Content/TestAlwaysSucceed/TestAlwaysSucceed.umap new file mode 100644 index 0000000..ba948a5 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestAlwaysSucceed/TestAlwaysSucceed.umap differ diff --git a/DaedalicTestAutomationPlugin/Content/TestAlwaysSucceedWithDelay/BP_TestAlwaysSucceedWithDelay.uasset b/DaedalicTestAutomationPlugin/Content/TestAlwaysSucceedWithDelay/BP_TestAlwaysSucceedWithDelay.uasset new file mode 100644 index 0000000..e802892 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestAlwaysSucceedWithDelay/BP_TestAlwaysSucceedWithDelay.uasset differ diff --git a/DaedalicTestAutomationPlugin/Content/TestAlwaysSucceedWithDelay/TestAlwaysSucceedWithDelay.umap b/DaedalicTestAutomationPlugin/Content/TestAlwaysSucceedWithDelay/TestAlwaysSucceedWithDelay.umap new file mode 100644 index 0000000..5dfd359 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestAlwaysSucceedWithDelay/TestAlwaysSucceedWithDelay.umap differ diff --git a/DaedalicTestAutomationPlugin/Content/TestAlwaysTimeout/BP_TestAlwaysTimeout.uasset b/DaedalicTestAutomationPlugin/Content/TestAlwaysTimeout/BP_TestAlwaysTimeout.uasset new file mode 100644 index 0000000..bc70630 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestAlwaysTimeout/BP_TestAlwaysTimeout.uasset differ diff --git a/DaedalicTestAutomationPlugin/Content/TestAlwaysTimeout/TestAlwaysTimeout.umap b/DaedalicTestAutomationPlugin/Content/TestAlwaysTimeout/TestAlwaysTimeout.umap new file mode 100644 index 0000000..12f8932 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestAlwaysTimeout/TestAlwaysTimeout.umap differ diff --git a/DaedalicTestAutomationPlugin/Content/TestAssume/BP_TestAssume.uasset b/DaedalicTestAutomationPlugin/Content/TestAssume/BP_TestAssume.uasset new file mode 100644 index 0000000..7d871c7 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestAssume/BP_TestAssume.uasset differ diff --git a/DaedalicTestAutomationPlugin/Content/TestAssume/TestAssume.umap b/DaedalicTestAutomationPlugin/Content/TestAssume/TestAssume.umap new file mode 100644 index 0000000..21a42ca Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestAssume/TestAssume.umap differ diff --git a/DaedalicTestAutomationPlugin/Content/TestBeforeAfter/BP_TestBeforeAfter.uasset b/DaedalicTestAutomationPlugin/Content/TestBeforeAfter/BP_TestBeforeAfter.uasset new file mode 100644 index 0000000..6ce279e Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestBeforeAfter/BP_TestBeforeAfter.uasset differ diff --git a/DaedalicTestAutomationPlugin/Content/TestBeforeAfter/BP_TestSuiteBeforeAfter.uasset b/DaedalicTestAutomationPlugin/Content/TestBeforeAfter/BP_TestSuiteBeforeAfter.uasset new file mode 100644 index 0000000..a339fc6 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestBeforeAfter/BP_TestSuiteBeforeAfter.uasset differ diff --git a/DaedalicTestAutomationPlugin/Content/TestBeforeAfter/TestBeforeAfter.umap b/DaedalicTestAutomationPlugin/Content/TestBeforeAfter/TestBeforeAfter.umap new file mode 100644 index 0000000..a769530 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestBeforeAfter/TestBeforeAfter.umap differ diff --git a/DaedalicTestAutomationPlugin/Content/TestCalculator/BP_TestCalculator.uasset b/DaedalicTestAutomationPlugin/Content/TestCalculator/BP_TestCalculator.uasset new file mode 100644 index 0000000..7d23411 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestCalculator/BP_TestCalculator.uasset differ diff --git a/DaedalicTestAutomationPlugin/Content/TestCalculator/BP_TestCalculatorAddsNumbers.uasset b/DaedalicTestAutomationPlugin/Content/TestCalculator/BP_TestCalculatorAddsNumbers.uasset new file mode 100644 index 0000000..f2fb397 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestCalculator/BP_TestCalculatorAddsNumbers.uasset differ diff --git a/DaedalicTestAutomationPlugin/Content/TestCalculator/TestCalculator.umap b/DaedalicTestAutomationPlugin/Content/TestCalculator/TestCalculator.umap new file mode 100644 index 0000000..8c8760f Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestCalculator/TestCalculator.umap differ diff --git a/DaedalicTestAutomationPlugin/Content/TestCompare/BP_TestCompare.uasset b/DaedalicTestAutomationPlugin/Content/TestCompare/BP_TestCompare.uasset new file mode 100644 index 0000000..c1b7632 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestCompare/BP_TestCompare.uasset differ diff --git a/DaedalicTestAutomationPlugin/Content/TestCompare/TestCompare.umap b/DaedalicTestAutomationPlugin/Content/TestCompare/TestCompare.umap new file mode 100644 index 0000000..be88e59 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestCompare/TestCompare.umap differ diff --git a/DaedalicTestAutomationPlugin/Content/TestEquals/BP_TestEquals.uasset b/DaedalicTestAutomationPlugin/Content/TestEquals/BP_TestEquals.uasset new file mode 100644 index 0000000..2480fbd Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestEquals/BP_TestEquals.uasset differ diff --git a/DaedalicTestAutomationPlugin/Content/TestEquals/TestEquals.umap b/DaedalicTestAutomationPlugin/Content/TestEquals/TestEquals.umap new file mode 100644 index 0000000..b010136 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestEquals/TestEquals.umap differ diff --git a/DaedalicTestAutomationPlugin/Content/TestParameterized/BP_TestParameter1.uasset b/DaedalicTestAutomationPlugin/Content/TestParameterized/BP_TestParameter1.uasset new file mode 100644 index 0000000..2025c30 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestParameterized/BP_TestParameter1.uasset differ diff --git a/DaedalicTestAutomationPlugin/Content/TestParameterized/BP_TestParameter2.uasset b/DaedalicTestAutomationPlugin/Content/TestParameterized/BP_TestParameter2.uasset new file mode 100644 index 0000000..4c00114 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestParameterized/BP_TestParameter2.uasset differ diff --git a/DaedalicTestAutomationPlugin/Content/TestParameterized/BP_TestParameterProvider.uasset b/DaedalicTestAutomationPlugin/Content/TestParameterized/BP_TestParameterProvider.uasset new file mode 100644 index 0000000..10805bb Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestParameterized/BP_TestParameterProvider.uasset differ diff --git a/DaedalicTestAutomationPlugin/Content/TestParameterized/BP_TestParameterized.uasset b/DaedalicTestAutomationPlugin/Content/TestParameterized/BP_TestParameterized.uasset new file mode 100644 index 0000000..e52f82e Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestParameterized/BP_TestParameterized.uasset differ diff --git a/DaedalicTestAutomationPlugin/Content/TestParameterized/TestParameterized.umap b/DaedalicTestAutomationPlugin/Content/TestParameterized/TestParameterized.umap new file mode 100644 index 0000000..1276588 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestParameterized/TestParameterized.umap differ diff --git a/DaedalicTestAutomationPlugin/Content/TestParameterized/TestParameterized_Streaming.umap b/DaedalicTestAutomationPlugin/Content/TestParameterized/TestParameterized_Streaming.umap new file mode 100644 index 0000000..311a4a8 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Content/TestParameterized/TestParameterized_Streaming.umap differ diff --git a/DaedalicTestAutomationPlugin/DaedalicTestAutomationPlugin.uplugin b/DaedalicTestAutomationPlugin/DaedalicTestAutomationPlugin.uplugin new file mode 100644 index 0000000..920bf92 --- /dev/null +++ b/DaedalicTestAutomationPlugin/DaedalicTestAutomationPlugin.uplugin @@ -0,0 +1,36 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.0.0", + "FriendlyName": "Daedalic Test Automation Plugin", + "Description": "Facilitates setting up integration test suits with Gauntlet.", + "Category": "Daedalic Entertainment", + "CreatedBy": "Daedalic Entertainment GmbH", + "CreatedByURL": "https://www.daedalic.com/", + "DocsURL": "https://github.com/DaedalicEntertainment/ue4-test-automation", + "EngineVersion": "4.23", + "MarketplaceURL": "", + "SupportURL": "https://github.com/DaedalicEntertainment/ue4-test-automation/issues", + "EnabledByDefault": false, + "CanContainContent": true, + "IsBetaVersion": false, + "Installed": false, + "Modules": [ + { + "Name": "DaedalicTestAutomationPlugin", + "Type": "Developer", + "LoadingPhase": "Default" + }, + { + "Name": "DaedalicTestAutomationPluginEditor", + "Type": "Developer", + "LoadingPhase": "PreDefault" + } + ], + "Plugins": [ + { + "Name": "Gauntlet", + "Enabled": true + } + ] +} diff --git a/DaedalicTestAutomationPlugin/Resources/Icon128.png b/DaedalicTestAutomationPlugin/Resources/Icon128.png new file mode 100644 index 0000000..3033ae0 Binary files /dev/null and b/DaedalicTestAutomationPlugin/Resources/Icon128.png differ diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/DaedalicTestAutomationPlugin.Build.cs b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/DaedalicTestAutomationPlugin.Build.cs new file mode 100644 index 0000000..64ddcb1 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/DaedalicTestAutomationPlugin.Build.cs @@ -0,0 +1,51 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +namespace UnrealBuildTool.Rules +{ + public class DaedalicTestAutomationPlugin : ModuleRules + { + public DaedalicTestAutomationPlugin(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new string[] { + // ... add public include paths required here ... + } + ); + + PrivateIncludePaths.AddRange( + new string[] { + // ... add other private include paths required here ... + } + ); + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreUObject", + "Engine", + "InputCore", + "Gauntlet", + "UMG", + "SlateCore" + } + ); + + PrivateDependencyModuleNames.AddRange( + new string[] + { + // ... add private dependencies that you statically link with here ... + } + ); + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } + } +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeDelayFramesAction.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeDelayFramesAction.cpp new file mode 100644 index 0000000..b568b3f --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeDelayFramesAction.cpp @@ -0,0 +1,24 @@ +#include "DaeDelayFramesAction.h" + +FDaeDelayFramesAction::FDaeDelayFramesAction(const FLatentActionInfo& LatentInfo, int32 NumFrames) + : FramesRemaining(NumFrames) + , ExecutionFunction(LatentInfo.ExecutionFunction) + , OutputLink(LatentInfo.Linkage) + , CallbackTarget(LatentInfo.CallbackTarget) +{ +} + +void FDaeDelayFramesAction::UpdateOperation(FLatentResponse& Response) + +{ + --FramesRemaining; + Response.FinishAndTriggerIf(FramesRemaining <= 0, ExecutionFunction, OutputLink, + CallbackTarget); +} + +#if WITH_EDITOR +FString FDaeDelayFramesAction::GetDescription() const +{ + return FString::Printf(TEXT("Delay ({0} frames left)"), FramesRemaining); +} +#endif diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeDelayUntilTriggeredAction.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeDelayUntilTriggeredAction.cpp new file mode 100644 index 0000000..a76d399 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeDelayUntilTriggeredAction.cpp @@ -0,0 +1,26 @@ +#include "DaeDelayUntilTriggeredAction.h" +#include "DaeTestTriggerBox.h" + +FDaeDelayUntilTriggeredAction::FDaeDelayUntilTriggeredAction(const FLatentActionInfo& LatentInfo, + ADaeTestTriggerBox* InTestTriggerBox) + : TestTriggerBox(InTestTriggerBox) + , ExecutionFunction(LatentInfo.ExecutionFunction) + , OutputLink(LatentInfo.Linkage) + , CallbackTarget(LatentInfo.CallbackTarget) +{ +} + +void FDaeDelayUntilTriggeredAction::UpdateOperation(FLatentResponse& Response) + +{ + bool bWasTriggered = !IsValid(TestTriggerBox) || TestTriggerBox->WasTriggered(); + Response.FinishAndTriggerIf(bWasTriggered, ExecutionFunction, OutputLink, CallbackTarget); +} + +#if WITH_EDITOR +FString FDaeDelayUntilTriggeredAction::GetDescription() const +{ + FString TriggerBoxName = IsValid(TestTriggerBox) ? TestTriggerBox->GetName() : TEXT("nullptr"); + return FString::Printf(TEXT("Delay (until {0} was triggered)"), *TriggerBoxName); +} +#endif diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeGauntletStates.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeGauntletStates.cpp new file mode 100644 index 0000000..dcd51bc --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeGauntletStates.cpp @@ -0,0 +1,6 @@ +#include "DaeGauntletStates.h" + +FName FDaeGauntletStates::LoadingNextMap = TEXT("Gauntlet_LoadingNextMap"); +FName FDaeGauntletStates::DiscoveringTests = TEXT("Gauntlet_DiscoveringTests"); +FName FDaeGauntletStates::Running = TEXT("Gauntlet_Running"); +FName FDaeGauntletStates::Finished = TEXT("Gauntlet_Finished"); diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeGauntletTestController.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeGauntletTestController.cpp new file mode 100644 index 0000000..3bf5ff3 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeGauntletTestController.cpp @@ -0,0 +1,194 @@ +#include "DaeGauntletTestController.h" +#include "Kismet/GameplayStatics.h" +#include "DaeGauntletStates.h" +#include "DaeJUnitReportWriter.h" +#include "DaeTestAutomationPluginSettings.h" +#include "DaeTestLogCategory.h" +#include "DaeTestSuiteActor.h" +#include +#include +#include +#include + +void UDaeGauntletTestController::OnInit() +{ + Super::OnInit(); + + // Get tests path. + const UDaeTestAutomationPluginSettings* TestAutomationPluginSettings = + GetDefault(); + + UE_LOG(LogDaeTest, Display, TEXT("Discovering tests from: %s"), + *TestAutomationPluginSettings->TestMapPath); + + // Build list of tests (based on FAutomationEditorCommonUtils::CollectTestsByClass). + FAssetRegistryModule& AssetRegistryModule = + FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); + TArray AssetDataArray; + + AssetRegistryModule.Get().SearchAllAssets(true); + AssetRegistryModule.Get().GetAssetsByClass(UWorld::StaticClass()->GetFName(), AssetDataArray); + + const FString PatternToCheck = + FString::Printf(TEXT("/%s/"), *TestAutomationPluginSettings->TestMapPath); + + for (auto ObjIter = AssetDataArray.CreateConstIterator(); ObjIter; ++ObjIter) + { + const FAssetData& AssetData = *ObjIter; + + FString Filename = FPackageName::LongPackageNameToFilename(AssetData.ObjectPath.ToString()); + + if (Filename.Contains(*PatternToCheck)) + { + FName MapName = AssetData.AssetName; + MapNames.Add(MapName); + + UE_LOG(LogDaeTest, Display, TEXT("Discovered test: %s"), *MapName.ToString()); + } + } + + GetGauntlet()->BroadcastStateChange(FDaeGauntletStates::Initialized); +} + +void UDaeGauntletTestController::OnPostMapChange(UWorld* World) +{ + Super::OnPostMapChange(World); + + UE_LOG(LogDaeTest, Log, TEXT("UDaeGauntletTestController::OnPostMapChange - World: %s"), + *World->GetName()); + + if (GetCurrentState() != FDaeGauntletStates::LoadingNextMap) + { + return; + } + + GetGauntlet()->BroadcastStateChange(FDaeGauntletStates::DiscoveringTests); +} + +void UDaeGauntletTestController::OnTick(float TimeDelta) +{ + if (GetCurrentState() == FDaeGauntletStates::Initialized) + { + // If this isn't a test map (e.g. immediately after startup), load first test map now. + if (!MapNames.Contains(FName(*GetCurrentMap()))) + { + UE_LOG(LogDaeTest, Log, + TEXT("FDaeGauntletStates::Initialized - World is not a test world, " + "loading first test world.")); + + MapIndex = -1; + LoadNextTestMap(); + return; + } + else + { + GetGauntlet()->BroadcastStateChange(FDaeGauntletStates::DiscoveringTests); + } + } + else if (GetCurrentState() == FDaeGauntletStates::LoadingNextMap) + { + UE_LOG(LogDaeTest, Display, TEXT("FDaeGauntletStates::LoadingNextMap - Loading map: %s"), + *MapNames[MapIndex].ToString()); + + UGameplayStatics::OpenLevel(this, MapNames[MapIndex]); + } + else if (GetCurrentState() == FDaeGauntletStates::DiscoveringTests) + { + // Find test suite. + ADaeTestSuiteActor* TestSuite = nullptr; + + for (TActorIterator ActorIt(GetWorld()); ActorIt; ++ActorIt) + { + TestSuite = *ActorIt; + } + + if (!IsValid(TestSuite)) + { + UE_LOG(LogDaeTest, Error, + TEXT("FDaeGauntletStates::DiscoveringTests - No " + "DaeGauntletTestSuiteActor " + "found.")); + LoadNextTestMap(); + return; + } + + // Start first test. + GetGauntlet()->BroadcastStateChange(FDaeGauntletStates::Running); + + TestSuite->OnTestSuiteSuccessful.AddDynamic( + this, &UDaeGauntletTestController::OnTestSuiteFinished); + TestSuite->OnTestSuiteFailed.AddDynamic(this, + &UDaeGauntletTestController::OnTestSuiteFinished); + + TestSuite->RunAllTests(); + } +} + +void UDaeGauntletTestController::LoadNextTestMap() +{ + ++MapIndex; + + // Check if we just want to run a single test. + FString SingleTestName = ParseCommandLineOption(TEXT("TestName")); + + if (!SingleTestName.IsEmpty()) + { + while (MapNames.IsValidIndex(MapIndex) && MapNames[MapIndex].ToString() != SingleTestName) + { + ++MapIndex; + } + } + + if (MapNames.IsValidIndex(MapIndex)) + { + // Load next test map in next tick. This is to avoid invocation list changes during OnPostMapChange. + GetGauntlet()->BroadcastStateChange(FDaeGauntletStates::LoadingNextMap); + } + else + { + // All tests finished. + UE_LOG(LogDaeTest, Display, + TEXT("UDaeGauntletTestController::LoadNextTestMap - All tests finished.")); + + // Write final test report. + FDaeJUnitReportWriter JUnitReportWriter; + FString TestReport = + JUnitReportWriter.CreateReport(TEXT("DaeGauntletTestController"), Results); + UE_LOG(LogDaeTest, Log, TEXT("Test report:\r\n%s"), *TestReport); + + FString JUnitReportPath = ParseCommandLineOption(TEXT("JUnitReportPath")); + + if (!JUnitReportPath.IsEmpty()) + { + UE_LOG(LogDaeTest, Display, TEXT("Writing test report to: %s"), *JUnitReportPath); + FFileHelper::SaveStringToFile(TestReport, *JUnitReportPath); + } + + // Finish Gauntlet. + GetGauntlet()->BroadcastStateChange(FDaeGauntletStates::Finished); + + for (const FDaeTestSuiteResult& Result : Results) + { + if (Result.NumFailedTests() > 0) + { + EndTest(1); + return; + } + } + + EndTest(0); + } +} + +void UDaeGauntletTestController::OnTestSuiteFinished(ADaeTestSuiteActor* TestSuite) +{ + Results.Add(TestSuite->GetResult()); + LoadNextTestMap(); +} + +FString UDaeGauntletTestController::ParseCommandLineOption(const FString& Key) +{ + FString Value; + FParse::Value(FCommandLine::Get(), *Key, Value); + return Value.Mid(1); +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeJUnitReportWriter.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeJUnitReportWriter.cpp new file mode 100644 index 0000000..a5e7f0a --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeJUnitReportWriter.cpp @@ -0,0 +1,109 @@ +#include "DaeJUnitReportWriter.h" + +FString FDaeJUnitReportWriter::CreateReport(const FString& Name, + const TArray& TestSuites) const +{ + // Write a JUnit XML report based on FXmlFile::WriteNodeHierarchy. + // Unfortunately, FXmlNode::Tag is private, so we have to do the hard work here ourselves... + FString XmlString; + + XmlString += TEXT("") LINE_TERMINATOR; + XmlString += TEXT("") LINE_TERMINATOR; + + for (const FDaeTestSuiteResult& TestSuiteResult : TestSuites) + { + const FString& TestClassName = FString::Printf(TEXT("%s.%s"), *TestSuiteResult.MapName, + *TestSuiteResult.TestSuiteName); + + for (const FDaeTestResult& TestResult : TestSuiteResult.TestResults) + { + XmlString += TEXT(" ") LINE_TERMINATOR; + + if (TestResult.HasFailed()) + { + XmlString += + FString::Printf(TEXT(" %s"), + *TestResult.FailureMessage) + + LINE_TERMINATOR; + } + else if (TestResult.WasSkipped()) + { + XmlString += + FString::Printf(TEXT(" %s"), *TestResult.SkipReason) + + LINE_TERMINATOR; + } + + XmlString += TEXT(" ") LINE_TERMINATOR; + } + } + + XmlString += TEXT("") LINE_TERMINATOR; + + return XmlString; +} + +int32 FDaeJUnitReportWriter::NumTotalTests(const TArray& TestSuites) const +{ + int32 TotalTests = 0; + + for (const FDaeTestSuiteResult& TestSuite : TestSuites) + { + TotalTests += TestSuite.NumTotalTests(); + } + + return TotalTests; +} + +int32 FDaeJUnitReportWriter::NumFailedTests(const TArray& TestSuites) const +{ + int32 FailedTests = 0; + + for (const FDaeTestSuiteResult& TestSuite : TestSuites) + { + FailedTests += TestSuite.NumFailedTests(); + } + + return FailedTests; +} + +int32 FDaeJUnitReportWriter::NumSkippedTests(const TArray& TestSuites) const +{ + int32 SkippedTests = 0; + + for (const FDaeTestSuiteResult& TestSuite : TestSuites) + { + SkippedTests += TestSuite.NumSkippedTests(); + } + + return SkippedTests; +} + +float FDaeJUnitReportWriter::GetTotalTimeSeconds(const TArray& TestSuites) const +{ + float TimeSeconds = 0; + + for (const FDaeTestSuiteResult& TestSuite : TestSuites) + { + TimeSeconds += TestSuite.GetTotalTimeSeconds(); + } + + return TimeSeconds; +} + +FString FDaeJUnitReportWriter::GetTimestamp(const TArray& TestSuites) const +{ + FDateTime Timestamp = TestSuites.Num() > 0 ? TestSuites[0].Timestamp : FDateTime::UtcNow(); + return Timestamp.ToIso8601(); +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestActor.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestActor.cpp new file mode 100644 index 0000000..ea2c7dc --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestActor.cpp @@ -0,0 +1,156 @@ +#include "DaeTestActor.h" +#include "DaeTestLogCategory.h" +#include "DaeTestParameterProviderActor.h" + +ADaeTestActor::ADaeTestActor( + const FObjectInitializer& ObjectInitializer /*= FObjectInitializer::Get()*/) + : Super(ObjectInitializer) +{ + TimeoutInSeconds = 30.0f; +} + +void ADaeTestActor::ApplyParameterProviders() +{ + for (int32 Index = 0; Index < ParameterProviders.Num(); ++Index) + { + ADaeTestParameterProviderActor* Provider = ParameterProviders[Index]; + + if (!IsValid(Provider)) + { + UE_LOG(LogDaeTest, Error, + TEXT("ADaeTestActor::ApplyParameterProviders - %s has invalid parameter " + "provider at index %i, skipping."), + *GetName(), Index); + + continue; + } + + TArray AdditionalParameters = Provider->GetParameters(); + Parameters.Append(AdditionalParameters); + + UE_LOG(LogDaeTest, Log, + TEXT("ADaeTestActor::ApplyParameterProviders - %s appended %i additional parameters " + "provided by %s."), + *GetName(), AdditionalParameters.Num(), *Provider->GetName()); + } +} + +void ADaeTestActor::RunTest(UObject* TestParameter) +{ + CurrentParameter = TestParameter; + bHasResult = false; + + if (!SkipReason.IsEmpty()) + { + NotifyOnTestSkipped(SkipReason); + return; + } + + NotifyOnAssume(CurrentParameter); + + if (bHasResult) + { + // This can happen with failed assumptions, for instance. + return; + } + + NotifyOnArrange(CurrentParameter); + NotifyOnAct(CurrentParameter); +} + +void ADaeTestActor::FinishAct() +{ + if (bHasResult) + { + UE_LOG(LogDaeTest, Warning, + TEXT("Test %s already has a result. This can happen after a timeout due to delays, " + "but if not, make sure not to call FinishAct more than once."), + *GetName()); + return; + } + + NotifyOnAssert(CurrentParameter); + + if (!bHasResult) + { + NotifyOnTestSuccessful(); + } +} + +float ADaeTestActor::GetTimeoutInSeconds() const +{ + return TimeoutInSeconds; +} + +TArray> ADaeTestActor::GetParameters() const +{ + return Parameters; +} + +UObject* ADaeTestActor::GetCurrentParameter() const +{ + return CurrentParameter; +} + +void ADaeTestActor::NotifyOnTestSuccessful() +{ + if (bHasResult) + { + return; + } + + bHasResult = true; + + OnTestSuccessful.Broadcast(this, CurrentParameter); +} + +void ADaeTestActor::NotifyOnTestFailed(const FString& Message) +{ + if (bHasResult) + { + return; + } + + bHasResult = true; + + UE_LOG(LogDaeTest, Error, TEXT("%s"), *Message); + + OnTestFailed.Broadcast(this, CurrentParameter, Message); +} + +void ADaeTestActor::NotifyOnTestSkipped(const FString& InSkipReason) +{ + if (bHasResult) + { + return; + } + + bHasResult = true; + + OnTestSkipped.Broadcast(this, CurrentParameter, InSkipReason); +} + +void ADaeTestActor::NotifyOnAssume(UObject* Parameter) +{ + ReceiveOnAssume(Parameter); +} + +void ADaeTestActor::NotifyOnArrange(UObject* Parameter) +{ + ReceiveOnArrange(Parameter); +} + +void ADaeTestActor::NotifyOnAct(UObject* Parameter) +{ + ReceiveOnAct(Parameter); +} + +void ADaeTestActor::NotifyOnAssert(UObject* Parameter) +{ + ReceiveOnAssert(Parameter); +} + +void ADaeTestActor::ReceiveOnAct_Implementation(UObject* Parameter) +{ + FinishAct(); +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestActorBlueprint.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestActorBlueprint.cpp new file mode 100644 index 0000000..da81d7e --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestActorBlueprint.cpp @@ -0,0 +1 @@ +#include "DaeTestActorBlueprint.h" diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestAssertBlueprintFunctionLibrary.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestAssertBlueprintFunctionLibrary.cpp new file mode 100644 index 0000000..d36d516 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestAssertBlueprintFunctionLibrary.cpp @@ -0,0 +1,595 @@ +#include "DaeTestAssertBlueprintFunctionLibrary.h" +#include "DaeTestActor.h" +#include "DaeTestLogCategory.h" +#include "DaeTestTriggerBox.h" +#include +#include +#include +#include +#include +#include +#include + +const FString UDaeTestAssertBlueprintFunctionLibrary::ErrorMessageFormatEqual = + TEXT("Assertion failed - {0} - Expected: {1}, but was: {2}"); + +const FString UDaeTestAssertBlueprintFunctionLibrary::ErrorMessageFormatNotEqual = + TEXT("Assertion failed - {0} - Was {1}, but should not be."); + +const FString UDaeTestAssertBlueprintFunctionLibrary::ErrorMessageFormatLessThan = + TEXT("Assertion failed - {0} - Expected: {1} < {2}, but was not."); + +const FString UDaeTestAssertBlueprintFunctionLibrary::ErrorMessageFormatLessThanOrEqualTo = + TEXT("Assertion failed - {0} - Expected: {1} <= {2}, but was not."); + +const FString UDaeTestAssertBlueprintFunctionLibrary::ErrorMessageFormatGreaterThan = + TEXT("Assertion failed - {0} - Expected: {1} > {2}, but was not."); + +const FString UDaeTestAssertBlueprintFunctionLibrary::ErrorMessageFormatGreaterThanOrEqualTo = + TEXT("Assertion failed - {0} - Expected: {1} >= {2}, but was not."); + +const FString UDaeTestAssertBlueprintFunctionLibrary::ErrorMessageFormatInRange = + TEXT("Assertion failed - {0} - Expected: between {1} and {2}, but was: {3}"); + +const FString UDaeTestAssertBlueprintFunctionLibrary::ErrorMessageFormatNotInRange = + TEXT("Assertion failed - {0} - Expected: not between {1} and {2}, but was: {3}"); + +void UDaeTestAssertBlueprintFunctionLibrary::AssertFail(const FString& What, + UObject* Context /*= nullptr*/) +{ + OnTestFailed(Context, What); +} +void UDaeTestAssertBlueprintFunctionLibrary::AssertTrue(bool bValue, const FString& What, + UObject* Context /*= nullptr*/) +{ + if (!bValue) + { + FString Message = + FString::Format(*ErrorMessageFormatEqual, {What, TEXT("True"), TEXT("False")}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertFalse(bool bValue, const FString& What, + UObject* Context /*= nullptr*/) +{ + if (bValue) + { + FString Message = + FString::Format(*ErrorMessageFormatEqual, {What, TEXT("False"), TEXT("True")}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertValid(UObject* Object, const FString& What, + UObject* Context /*= nullptr*/) +{ + if (!IsValid(Object)) + { + FString Message = + FString::Format(TEXT("Assertion failed - {0} - Expected: valid, but was: invalid"), + {What}); + OnTestFailed(Context, Message); + return; + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertInvalid(UObject* Object, const FString& What, + UObject* Context /*= nullptr*/) +{ + if (IsValid(Object)) + { + FString Message = + FString::Format(TEXT("Assertion failed - {0} - Expected: invalid, but was: valid"), + {What}); + OnTestFailed(Context, Message); + return; + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertWasTriggered(ADaeTestTriggerBox* TestTriggerBox, + UObject* Context /*= nullptr*/) +{ + if (!IsValid(TestTriggerBox)) + { + OnTestFailed(Context, TEXT("Invalid test trigger box in assertion")); + return; + } + + if (!TestTriggerBox->WasTriggered()) + { + FString Message = + FString::Format(TEXT("Assertion failed - Trigger box {0} wasn't triggered"), + {TestTriggerBox->GetName()}); + OnTestFailed(Context, Message); + return; + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertWasNotTriggered( + ADaeTestTriggerBox* TestTriggerBox, UObject* Context /*= nullptr*/) +{ + if (!IsValid(TestTriggerBox)) + { + OnTestFailed(Context, TEXT("Invalid test trigger box in assertion")); + return; + } + + if (TestTriggerBox->WasTriggered()) + { + FString Message = FString::Format(TEXT("Assertion failed - Trigger box {0} was triggered"), + {TestTriggerBox->GetName()}); + OnTestFailed(Context, Message); + return; + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertEqualByte(uint8 Actual, uint8 Expected, + const FString& What, + UObject* Context /*= nullptr*/) +{ + AssertEqual(Context, What, Actual, Expected); +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertNotEqualByte(uint8 Actual, uint8 Unexpected, + const FString& What, + UObject* Context /*= nullptr*/) +{ + AssertNotEqual(Context, What, Actual, Unexpected); +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertCompareByte(uint8 First, + EDaeTestComparisonMethod ShouldBe, + uint8 Second, const FString& What, + UObject* Context /*= nullptr*/) +{ + AssertCompare(Context, What, First, ShouldBe, Second); +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertEqualInt32(int32 Actual, int32 Expected, + const FString& What, + UObject* Context /*= nullptr*/) +{ + AssertEqual(Context, What, Actual, Expected); +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertNotEqualInt32(int32 Actual, int32 Unexpected, + const FString& What, + UObject* Context /*= nullptr*/) +{ + AssertNotEqual(Context, What, Actual, Unexpected); +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertCompareInt32(int32 First, + EDaeTestComparisonMethod ShouldBe, + int32 Second, const FString& What, + UObject* Context /*= nullptr*/) +{ + AssertCompare(Context, What, First, ShouldBe, Second); +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertEqualInt64(int64 Actual, int64 Expected, + const FString& What, + UObject* Context /*= nullptr*/) +{ + AssertEqual(Context, What, Actual, Expected); +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertNotEqualInt64(int64 Actual, int64 Unexpected, + const FString& What, + UObject* Context /*= nullptr*/) +{ + AssertNotEqual(Context, What, Actual, Unexpected); +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertCompareInt64(int64 First, + EDaeTestComparisonMethod ShouldBe, + int64 Second, const FString& What, + UObject* Context /*= nullptr*/) +{ + AssertCompare(Context, What, First, ShouldBe, Second); +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertEqualFloat(float Actual, float Expected, + const FString& What, + UObject* Context /*= nullptr*/) +{ + if (!FMath::IsNearlyEqual(Actual, Expected)) + { + FString Message = FString::Format(*ErrorMessageFormatEqual, {What, Expected, Actual}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertNotEqualFloat(float Actual, float Unexpected, + const FString& What, + UObject* Context /*= nullptr*/) +{ + if (FMath::IsNearlyEqual(Actual, Unexpected)) + { + FString Message = FString::Format(*ErrorMessageFormatNotEqual, {What, Unexpected}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertCompareFloat(float First, + EDaeTestComparisonMethod ShouldBe, + float Second, const FString& What, + UObject* Context /*= nullptr*/) +{ + AssertCompare(Context, What, First, ShouldBe, Second); +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertEqualName(const FName& Actual, + const FName& Expected, + bool bIgnoreCase, const FString& What, + UObject* Context /*= nullptr*/) +{ + bool bEquals = bIgnoreCase ? Actual.IsEqual(Expected, ENameCase::IgnoreCase) + : Actual.IsEqual(Expected, ENameCase::CaseSensitive); + + if (!bEquals) + { + FString Message = FString::Format(*ErrorMessageFormatEqual, + {What, Expected.ToString(), Actual.ToString()}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertNotEqualName(const FName& Actual, + const FName& Unexpected, + bool bIgnoreCase, + const FString& What, + UObject* Context /*= nullptr*/) +{ + bool bEquals = bIgnoreCase ? Actual.IsEqual(Unexpected, ENameCase::IgnoreCase) + : Actual.IsEqual(Unexpected, ENameCase::CaseSensitive); + + if (bEquals) + { + FString Message = + FString::Format(*ErrorMessageFormatNotEqual, {What, Unexpected.ToString()}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertEqualString(const FString& Actual, + const FString& Expected, + bool bIgnoreCase, + const FString& What, + UObject* Context /*= nullptr*/) +{ + ESearchCase::Type SearchCase = bIgnoreCase ? ESearchCase::IgnoreCase + : ESearchCase::CaseSensitive; + + if (!Actual.Equals(Expected, SearchCase)) + { + FString Message = FString::Format(*ErrorMessageFormatEqual, {What, Expected, Actual}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertNotEqualString(const FString& Actual, + const FString& Unexpected, + bool bIgnoreCase, + const FString& What, + UObject* Context /*= nullptr*/) +{ + ESearchCase::Type SearchCase = bIgnoreCase ? ESearchCase::IgnoreCase + : ESearchCase::CaseSensitive; + + if (Actual.Equals(Unexpected, SearchCase)) + { + FString Message = FString::Format(*ErrorMessageFormatNotEqual, {What, Unexpected}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertEqualText(const FText& Actual, + const FText& Expected, + bool bIgnoreCase, const FString& What, + UObject* Context /*= nullptr*/) +{ + bool bEquals = bIgnoreCase ? Actual.EqualToCaseIgnored(Expected) : Actual.EqualTo(Expected); + + if (!bEquals) + { + FString Message = FString::Format(*ErrorMessageFormatEqual, + {What, Expected.ToString(), Actual.ToString()}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertNotEqualText(const FText& Actual, + const FText& Unexpected, + bool bIgnoreCase, + const FString& What, + UObject* Context /*= nullptr*/) +{ + bool bEquals = bIgnoreCase ? Actual.EqualToCaseIgnored(Unexpected) : Actual.EqualTo(Unexpected); + + if (bEquals) + { + FString Message = + FString::Format(*ErrorMessageFormatNotEqual, {What, Unexpected.ToString()}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertEqualVector(const FVector& Actual, + const FVector& Expected, + const FString& What, + UObject* Context /*= nullptr*/) +{ + if (!Actual.Equals(Expected)) + { + FString Message = FString::Format(*ErrorMessageFormatEqual, + {What, Expected.ToString(), Actual.ToString()}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertNotEqualVector(const FVector& Actual, + const FVector& Unexpected, + const FString& What, + UObject* Context /*= nullptr*/) +{ + if (Actual.Equals(Unexpected)) + { + FString Message = + FString::Format(*ErrorMessageFormatNotEqual, {What, Unexpected.ToString()}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertEqualRotator(const FRotator& Actual, + const FRotator& Expected, + const FString& What, + UObject* Context /*= nullptr*/) +{ + if (!Actual.Equals(Expected)) + { + FString Message = FString::Format(*ErrorMessageFormatEqual, + {What, Expected.ToString(), Actual.ToString()}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertNotEqualRotator(const FRotator& Actual, + const FRotator& Unexpected, + const FString& What, + UObject* Context /*= nullptr*/) +{ + if (Actual.Equals(Unexpected)) + { + FString Message = + FString::Format(*ErrorMessageFormatNotEqual, {What, Unexpected.ToString()}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertEqualTransform(const FTransform& Actual, + const FTransform& Expected, + const FString& What, + UObject* Context /*= nullptr*/) +{ + if (!Actual.Equals(Expected)) + { + FString Message = FString::Format(*ErrorMessageFormatEqual, + {What, Expected.ToString(), Actual.ToString()}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertNotEqualTransform(const FTransform& Actual, + const FTransform& Unexpected, + const FString& What, + UObject* Context /*= nullptr*/) +{ + if (Actual.Equals(Unexpected)) + { + FString Message = + FString::Format(*ErrorMessageFormatNotEqual, {What, Unexpected.ToString()}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertInRangeByte(uint8 Value, uint8 MinInclusive, + uint8 MaxInclusive, + const FString& What, + UObject* Context /*= nullptr*/) +{ + AssertInRangeInt32(Value, MinInclusive, MaxInclusive, What, Context); +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertNotInRangeByte(uint8 Value, uint8 MinInclusive, + uint8 MaxInclusive, + const FString& What, + UObject* Context /*= nullptr*/) +{ + AssertNotInRangeInt32(Value, MinInclusive, MaxInclusive, What, Context); +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertInRangeInt32(int32 Value, int32 MinInclusive, + int32 MaxInclusive, + const FString& What, + UObject* Context /*= nullptr*/) +{ + if (!UKismetMathLibrary::InRange_IntInt(Value, MinInclusive, MaxInclusive)) + { + FString Message = + FString::Format(*ErrorMessageFormatInRange, {What, MinInclusive, MaxInclusive, Value}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertNotInRangeInt32(int32 Value, int32 MinInclusive, + int32 MaxInclusive, + const FString& What, + UObject* Context /*= nullptr*/) +{ + if (UKismetMathLibrary::InRange_IntInt(Value, MinInclusive, MaxInclusive)) + { + FString Message = FString::Format(*ErrorMessageFormatNotInRange, + {What, MinInclusive, MaxInclusive, Value}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertInRangeInt64(int64 Value, int64 MinInclusive, + int64 MaxInclusive, + const FString& What, + UObject* Context /*= nullptr*/) +{ + if (!UKismetMathLibrary::InRange_Int64Int64(Value, MinInclusive, MaxInclusive)) + { + FString Message = + FString::Format(*ErrorMessageFormatInRange, {What, MinInclusive, MaxInclusive, Value}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertNotInRangeInt64(int64 Value, int64 MinInclusive, + int64 MaxInclusive, + const FString& What, + UObject* Context /*= nullptr*/) +{ + if (UKismetMathLibrary::InRange_Int64Int64(Value, MinInclusive, MaxInclusive)) + { + FString Message = FString::Format(*ErrorMessageFormatNotInRange, + {What, MinInclusive, MaxInclusive, Value}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertInRangeFloat(float Value, float MinInclusive, + float MaxInclusive, + const FString& What, + UObject* Context /*= nullptr*/) +{ + if (!UKismetMathLibrary::InRange_FloatFloat(Value, MinInclusive, MaxInclusive)) + { + FString Message = + FString::Format(*ErrorMessageFormatInRange, {What, MinInclusive, MaxInclusive, Value}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertNotInRangeFloat(float Value, float MinInclusive, + float MaxInclusive, + const FString& What, + UObject* Context /*= nullptr*/) +{ + if (UKismetMathLibrary::InRange_FloatFloat(Value, MinInclusive, MaxInclusive)) + { + FString Message = FString::Format(*ErrorMessageFormatNotInRange, + {What, MinInclusive, MaxInclusive, Value}); + OnTestFailed(Context, Message); + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertWidgetIsVisible(UUserWidget* Widget, + const FString& What, + UObject* Context /*= nullptr*/) +{ + if (!IsValid(Widget)) + { + FString Message = + FString::Format(TEXT("Assertion failed - {0} - Widget is not valid"), {What}); + OnTestFailed(Context, Message); + return; + } + + if (!Widget->IsInViewport() && !IsValid(Widget->GetParent())) + { + FString Message = FString::Format( + TEXT("Assertion failed - {0} - Widget hasn't been added to the viewport"), {What}); + OnTestFailed(Context, Message); + return; + } + + if (!Widget->IsVisible()) + { + FString Message = FString::Format(TEXT("Assertion failed - {0} - Widget isn't visible, hit " + "test visible or self hit test visible"), + {What}); + OnTestFailed(Context, Message); + return; + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertTextIsSet(UTextBlock* TextBlock, + const FString& What, + UObject* Context /*= nullptr*/) +{ + if (!IsValid(TextBlock)) + { + FString Message = + FString::Format(TEXT("Assertion failed - {0} - Text block is not valid"), {What}); + OnTestFailed(Context, Message); + return; + } + + if (TextBlock->GetText().IsEmpty()) + { + FString Message = FString::Format(TEXT("Assertion failed - {0} - Text is empty"), {What}); + OnTestFailed(Context, Message); + return; + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertRichTextIsSet(URichTextBlock* RichTextBlock, + const FString& What, + UObject* Context /*= nullptr*/) +{ + if (!IsValid(RichTextBlock)) + { + FString Message = + FString::Format(TEXT("Assertion failed - {0} - Rich text block is not valid"), {What}); + OnTestFailed(Context, Message); + return; + } + + if (RichTextBlock->GetText().IsEmpty()) + { + FString Message = + FString::Format(TEXT("Assertion failed - {0} - Rich text is empty"), {What}); + OnTestFailed(Context, Message); + return; + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::AssertImageIsSet(UImage* Image, const FString& What, + UObject* Context /*= nullptr*/) +{ + if (!IsValid(Image)) + { + FString Message = + FString::Format(TEXT("Assertion failed - {0} - Image is not valid"), {What}); + OnTestFailed(Context, Message); + return; + } + + if (!IsValid(Image->Brush.GetResourceObject())) + { + FString Message = FString::Format(TEXT("Assertion failed - {0} - Image brush has no " + "resource object (e.g. texture or material)"), + {What}); + OnTestFailed(Context, Message); + return; + } +} + +void UDaeTestAssertBlueprintFunctionLibrary::OnTestFailed(UObject* Context, const FString& Message) +{ + ADaeTestActor* TestActor = Cast(Context); + + if (IsValid(TestActor)) + { + TestActor->NotifyOnTestFailed(Message); + } + else + { + UE_LOG(LogDaeTest, Error, TEXT("%s"), *Message); + } +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestAssumeBlueprintFunctionLibrary.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestAssumeBlueprintFunctionLibrary.cpp new file mode 100644 index 0000000..c55a5a9 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestAssumeBlueprintFunctionLibrary.cpp @@ -0,0 +1,39 @@ +#include "DaeTestAssumeBlueprintFunctionLibrary.h" + +void UDaeTestAssumeBlueprintFunctionLibrary::AssumeTrue(bool bValue, const FString& What, + UObject* Context /*= nullptr*/) +{ + if (!bValue) + { + FString Message = + FString::Format(TEXT("Assumption failed - {0} - Expected: True, but was: False"), + {What}); + OnTestSkipped(Context, Message); + } +} + +void UDaeTestAssumeBlueprintFunctionLibrary::AssumeFalse(bool bValue, const FString& What, + UObject* Context /*= nullptr*/) +{ + if (bValue) + { + FString Message = + FString::Format(TEXT("Assumption failed - {0} - Expected: False, but was: True"), + {What}); + OnTestSkipped(Context, Message); + } +} + +void UDaeTestAssumeBlueprintFunctionLibrary::OnTestSkipped(UObject* Context, const FString& Message) +{ + ADaeTestActor* TestActor = Cast(Context); + + if (IsValid(TestActor)) + { + TestActor->NotifyOnTestSkipped(Message); + } + else + { + UE_LOG(LogDaeTest, Log, TEXT("%s"), *Message); + } +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestAutomationPluginSettings.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestAutomationPluginSettings.cpp new file mode 100644 index 0000000..6b87217 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestAutomationPluginSettings.cpp @@ -0,0 +1,15 @@ +#include "DaeTestAutomationPluginSettings.h" + +UDaeTestAutomationPluginSettings::UDaeTestAutomationPluginSettings() + : TestMapPath(TEXT("Maps/AutomatedTests")) +{ +} + +void UDaeTestAutomationPluginSettings::PostEditChangeProperty( + struct FPropertyChangedEvent& PropertyChangedEvent) +{ + if (PropertyChangedEvent.GetPropertyName() == TEXT("TestMapPath")) + { + OnTestMapPathChanged.Broadcast(TestMapPath); + } +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestBlueprintFunctionLibrary.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestBlueprintFunctionLibrary.cpp new file mode 100644 index 0000000..1e7901f --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestBlueprintFunctionLibrary.cpp @@ -0,0 +1,42 @@ +#include "DaeTestBlueprintFunctionLibrary.h" +#include "DaeDelayFramesAction.h" +#include "DaeDelayUntilTriggeredAction.h" +#include +#include + +void UDaeTestBlueprintFunctionLibrary::DelayFrames(UObject* WorldContextObject, + struct FLatentActionInfo LatentInfo, + int32 NumFrames) +{ + if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, + EGetWorldErrorMode::LogAndReturnNull)) + { + FLatentActionManager& LatentActionManager = World->GetLatentActionManager(); + if (LatentActionManager.FindExistingAction(LatentInfo.CallbackTarget, + LatentInfo.UUID) + == NULL) + { + LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, + new FDaeDelayFramesAction(LatentInfo, NumFrames)); + } + } +} + +void UDaeTestBlueprintFunctionLibrary::DelayUntilTriggered(UObject* WorldContextObject, + struct FLatentActionInfo LatentInfo, + ADaeTestTriggerBox* TestTriggerBox) +{ + if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, + EGetWorldErrorMode::LogAndReturnNull)) + { + FLatentActionManager& LatentActionManager = World->GetLatentActionManager(); + if (LatentActionManager.FindExistingAction( + LatentInfo.CallbackTarget, LatentInfo.UUID) + == NULL) + { + LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, + new FDaeDelayUntilTriggeredAction(LatentInfo, + TestTriggerBox)); + } + } +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestDelayBlueprintFunctionLibrary.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestDelayBlueprintFunctionLibrary.cpp new file mode 100644 index 0000000..533d8a4 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestDelayBlueprintFunctionLibrary.cpp @@ -0,0 +1,42 @@ +#include "DaeTestDelayBlueprintFunctionLibrary.h" +#include "DaeDelayFramesAction.h" +#include "DaeDelayUntilTriggeredAction.h" +#include +#include + +void UDaeTestDelayBlueprintFunctionLibrary::DelayFrames(UObject* WorldContextObject, + struct FLatentActionInfo LatentInfo, + int32 NumFrames) +{ + if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, + EGetWorldErrorMode::LogAndReturnNull)) + { + FLatentActionManager& LatentActionManager = World->GetLatentActionManager(); + if (LatentActionManager.FindExistingAction(LatentInfo.CallbackTarget, + LatentInfo.UUID) + == NULL) + { + LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, + new FDaeDelayFramesAction(LatentInfo, NumFrames)); + } + } +} + +void UDaeTestDelayBlueprintFunctionLibrary::DelayUntilTriggered(UObject* WorldContextObject, + struct FLatentActionInfo LatentInfo, + ADaeTestTriggerBox* TestTriggerBox) +{ + if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, + EGetWorldErrorMode::LogAndReturnNull)) + { + FLatentActionManager& LatentActionManager = World->GetLatentActionManager(); + if (LatentActionManager.FindExistingAction( + LatentInfo.CallbackTarget, LatentInfo.UUID) + == NULL) + { + LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, + new FDaeDelayUntilTriggeredAction(LatentInfo, + TestTriggerBox)); + } + } +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestInputBlueprintFunctionLibrary.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestInputBlueprintFunctionLibrary.cpp new file mode 100644 index 0000000..c80e803 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestInputBlueprintFunctionLibrary.cpp @@ -0,0 +1,47 @@ +#include "DaeTestInputBlueprintFunctionLibrary.h" +#include +#include +#include +#include +#include + +void UDaeTestInputBlueprintFunctionLibrary::ApplyInputAction( + UObject* Context, const FName& ActionName, + EInputEvent InputEventType /*= EInputEvent::IE_Pressed*/) +{ + APlayerController* PlayerController = UGameplayStatics::GetPlayerController(Context, 0); + + const UInputSettings* InputSettings = GetDefault(); + + for (const FInputActionKeyMapping& Mapping : InputSettings->GetActionMappings()) + { + if (Mapping.ActionName == ActionName) + { + PlayerController->InputKey(Mapping.Key, InputEventType, 0.0f, false); + return; + } + } + + UE_LOG(LogDaeTest, Error, TEXT("%s - Input action not found: %s"), + IsValid(Context) ? *Context->GetName() : TEXT(""), *ActionName.ToString()); +} + +void UDaeTestInputBlueprintFunctionLibrary::ApplyInputAxis(UObject* Context, const FName& AxisName, + float AxisValue /*= 1.0f*/) +{ + APlayerController* PlayerController = UGameplayStatics::GetPlayerController(Context, 0); + + const UInputSettings* InputSettings = GetDefault(); + + for (const FInputAxisKeyMapping& Mapping : InputSettings->GetAxisMappings()) + { + if (Mapping.AxisName == AxisName) + { + PlayerController->InputAxis(Mapping.Key, AxisValue, 0.0f, 1, false); + return; + } + } + + UE_LOG(LogDaeTest, Error, TEXT("%s - Input axis not found: %s"), + IsValid(Context) ? *Context->GetName() : TEXT(""), *AxisName.ToString()); +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestLogCategory.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestLogCategory.cpp new file mode 100644 index 0000000..25e33af --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestLogCategory.cpp @@ -0,0 +1,3 @@ +#include "DaeTestLogCategory.h" + +DEFINE_LOG_CATEGORY(LogDaeTest); diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestParameterProviderActor.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestParameterProviderActor.cpp new file mode 100644 index 0000000..8060c91 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestParameterProviderActor.cpp @@ -0,0 +1,6 @@ +#include "DaeTestParameterProviderActor.h" + +TArray ADaeTestParameterProviderActor::GetParameters_Implementation() +{ + return TArray(); +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestParameterProviderActorBlueprint.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestParameterProviderActorBlueprint.cpp new file mode 100644 index 0000000..c485205 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestParameterProviderActorBlueprint.cpp @@ -0,0 +1 @@ +#include "DaeTestParameterProviderActorBlueprint.h" diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestResult.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestResult.cpp new file mode 100644 index 0000000..ba6a4c8 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestResult.cpp @@ -0,0 +1,27 @@ +#include "DaeTestResult.h" + +FDaeTestResult::FDaeTestResult() + : FDaeTestResult(FString(), 0.0f) +{ +} + +FDaeTestResult::FDaeTestResult(FString InTestName, float InTimeSeconds) + : TestName(InTestName) + , TimeSeconds(InTimeSeconds) +{ +} + +bool FDaeTestResult::WasSuccessful() const +{ + return !HasFailed() && !WasSkipped(); +} + +bool FDaeTestResult::HasFailed() const +{ + return !FailureMessage.IsEmpty(); +} + +bool FDaeTestResult::WasSkipped() const +{ + return !SkipReason.IsEmpty(); +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestSuiteActor.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestSuiteActor.cpp new file mode 100644 index 0000000..60e848b --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestSuiteActor.cpp @@ -0,0 +1,306 @@ +#include "DaeTestSuiteActor.h" +#include "DaeTestActor.h" +#include "DaeTestLogCategory.h" + +ADaeTestSuiteActor::ADaeTestSuiteActor( + const FObjectInitializer& ObjectInitializer /*= FObjectInitializer::Get()*/) +{ + bRunInPIE = true; + TestIndex = -1; + + PrimaryActorTick.bCanEverTick = true; + + // We need to be able to time out even while gameplay is paused. + PrimaryActorTick.bTickEvenWhenPaused = true; +} + +void ADaeTestSuiteActor::BeginPlay() +{ + Super::BeginPlay(); + + // Setup result data. + Result.MapName = GetWorld()->GetMapName(); + Result.TestSuiteName = GetName(); + Result.Timestamp = FDateTime::UtcNow(); +} + +void ADaeTestSuiteActor::Tick(float DeltaSeconds) +{ + Super::Tick(DeltaSeconds); + + if (!IsRunning()) + { + // Check if we should run all tests immediately. + // Happening in first Tick to make sure all actors have begun play. + if (bRunInPIE && GetWorld()->IsPlayInEditor() && TestIndex < 0) + { + RunAllTests(); + } + + return; + } + + TestTimeSeconds += DeltaSeconds; + + ADaeTestActor* CurrentTest = GetCurrentTest(); + + if (TestTimeSeconds >= CurrentTest->GetTimeoutInSeconds()) + { + // Enough waiting. Let's see the results. + UE_LOG(LogDaeTest, Warning, TEXT("Timed out after %f seconds"), + CurrentTest->GetTimeoutInSeconds()); + + CurrentTest->FinishAct(); + } +} + +void ADaeTestSuiteActor::RunAllTests() +{ + UE_LOG(LogDaeTest, Display, TEXT("ADaeTestSuiteActor::RunAllTests - Test Suite: %s"), + *GetName()); + + NotifyOnBeforeAll(); + + TestIndex = -1; + TestParameterIndex = -1; + + RunNextTest(); +} + +bool ADaeTestSuiteActor::IsRunning() const +{ + return IsValid(GetCurrentTest()); +} + +ADaeTestActor* ADaeTestSuiteActor::GetCurrentTest() const +{ + return Tests.IsValidIndex(TestIndex) ? Tests[TestIndex] : nullptr; +} + +UObject* ADaeTestSuiteActor::GetCurrentTestParameter() const +{ + ADaeTestActor* Test = GetCurrentTest(); + + if (!IsValid(Test)) + { + return nullptr; + } + + TArray> TestParameters = Test->GetParameters(); + + if (!TestParameters.IsValidIndex(TestParameterIndex)) + { + return nullptr; + } + + TSoftObjectPtr Parameter = TestParameters[TestParameterIndex]; + + if (!Parameter.IsValid()) + { + return nullptr; + } + + return Parameter.LoadSynchronous(); +} + +FString ADaeTestSuiteActor::GetCurrentTestName() const +{ + ADaeTestActor* Test = GetCurrentTest(); + + if (!IsValid(Test)) + { + return FString(); + } + + FString TestName = Test->GetName(); + + UObject* Parameter = GetCurrentTestParameter(); + + if (IsValid(Parameter)) + { + TestName += TEXT(" - ") + Parameter->GetName(); + } + + return TestName; +} + +FDaeTestSuiteResult ADaeTestSuiteActor::GetResult() const +{ + return Result; +} + +void ADaeTestSuiteActor::NotifyOnBeforeAll() +{ + ReceiveOnBeforeAll(); +} + +void ADaeTestSuiteActor::NotifyOnAfterAll() +{ + ReceiveOnAfterAll(); +} + +void ADaeTestSuiteActor::NotifyOnBeforeEach() +{ + ReceiveOnBeforeEach(); +} + +void ADaeTestSuiteActor::NotifyOnAfterEach() +{ + ReceiveOnAfterEach(); +} + +void ADaeTestSuiteActor::RunNextTest() +{ + ADaeTestActor* CurrentTest = GetCurrentTest(); + + // Unregister events. + if (IsValid(CurrentTest)) + { + CurrentTest->OnTestSuccessful.RemoveDynamic(this, &ADaeTestSuiteActor::OnTestSuccessful); + CurrentTest->OnTestFailed.RemoveDynamic(this, &ADaeTestSuiteActor::OnTestFailed); + CurrentTest->OnTestSkipped.RemoveDynamic(this, &ADaeTestSuiteActor::OnTestSkipped); + } + + // Prepare test run with next parameter. + ++TestParameterIndex; + + UObject* CurrentTestParameter = GetCurrentTestParameter(); + + if (!IsValid(CurrentTestParameter)) + { + // Prepare next test. + ++TestIndex; + TestParameterIndex = 0; + + // Apply parameter providers. + ADaeTestActor* NextTest = GetCurrentTest(); + + if (IsValid(NextTest)) + { + NextTest->ApplyParameterProviders(); + } + } + + TestTimeSeconds = 0.0f; + + if (!Tests.IsValidIndex(TestIndex)) + { + // All tests finished. + UE_LOG(LogDaeTest, Display, TEXT("ADaeTestSuiteActor::RunNextTest - All tests finished.")); + + NotifyOnAfterAll(); + + // Check if any test failed. + for (const FDaeTestResult& TestResult : Result.TestResults) + { + if (!TestResult.FailureMessage.IsEmpty()) + { + OnTestSuiteFailed.Broadcast(this); + return; + } + } + + OnTestSuiteSuccessful.Broadcast(this); + return; + } + + ADaeTestActor* Test = GetCurrentTest(); + + if (IsValid(Test)) + { + FString TestName = GetCurrentTestName(); + UE_LOG(LogDaeTest, Display, TEXT("ADaeTestSuiteActor::RunNextTest - Test: %s"), *TestName); + + // Register events. + Test->OnTestSuccessful.AddDynamic(this, &ADaeTestSuiteActor::OnTestSuccessful); + Test->OnTestFailed.AddDynamic(this, &ADaeTestSuiteActor::OnTestFailed); + Test->OnTestSkipped.AddDynamic(this, &ADaeTestSuiteActor::OnTestSkipped); + + // Run test. + NotifyOnBeforeEach(); + + UObject* TestParameter = GetCurrentTestParameter(); + Test->RunTest(TestParameter); + } + else + { + UE_LOG(LogDaeTest, Error, + TEXT("ADaeTestSuiteActor::RunNextTest - %s has invalid test at index %i, skipping."), + *GetName(), TestIndex); + + RunNextTest(); + } +} + +void ADaeTestSuiteActor::OnTestSuccessful(ADaeTestActor* Test, UObject* Parameter) +{ + if (Test != GetCurrentTest()) + { + // Prevent tests from reporting multiple results. + return; + } + + FString CurrentTestName = GetCurrentTestName(); + + UE_LOG(LogDaeTest, Display, TEXT("ADaeTestSuiteActor::OnTestSuccessful - Test: %s"), + *CurrentTestName); + + // Store result. + FDaeTestResult TestResult(CurrentTestName, TestTimeSeconds); + Result.TestResults.Add(TestResult); + + // Run next test. + NotifyOnAfterEach(); + + RunNextTest(); +} + +void ADaeTestSuiteActor::OnTestFailed(ADaeTestActor* Test, UObject* Parameter, + const FString& FailureMessage) +{ + if (Test != GetCurrentTest()) + { + // Prevent tests from reporting multiple results. + return; + } + + FString CurrentTestName = GetCurrentTestName(); + + UE_LOG(LogDaeTest, Error, + TEXT("ADaeTestSuiteActor::OnTestFailed - Test: %s, FailureMessage: %s"), + *CurrentTestName, *FailureMessage); + + // Store result. + FDaeTestResult TestResult(CurrentTestName, TestTimeSeconds); + TestResult.FailureMessage = FailureMessage; + Result.TestResults.Add(TestResult); + + // Run next test. + NotifyOnAfterEach(); + + RunNextTest(); +} + +void ADaeTestSuiteActor::OnTestSkipped(ADaeTestActor* Test, UObject* Parameter, + const FString& SkipReason) +{ + if (Test != GetCurrentTest()) + { + // Prevent tests from reporting multiple results. + return; + } + + FString CurrentTestName = GetCurrentTestName(); + + UE_LOG(LogDaeTest, Display, + TEXT("ADaeTestSuiteActor::OnTestSkipped - Test: %s, SkipReason: %s"), *CurrentTestName, + *SkipReason); + + // Store result. + FDaeTestResult TestResult(CurrentTestName, TestTimeSeconds); + TestResult.SkipReason = SkipReason; + Result.TestResults.Add(TestResult); + + // Run next test. + RunNextTest(); +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestSuiteActorBlueprint.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestSuiteActorBlueprint.cpp new file mode 100644 index 0000000..4356f7c --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestSuiteActorBlueprint.cpp @@ -0,0 +1 @@ +#include "DaeTestSuiteActorBlueprint.h" diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestSuiteResult.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestSuiteResult.cpp new file mode 100644 index 0000000..5ce15b7 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestSuiteResult.cpp @@ -0,0 +1,53 @@ +#include "DaeTestSuiteResult.h" + +int32 FDaeTestSuiteResult::NumTotalTests() const +{ + return TestResults.Num(); +} + +int32 FDaeTestSuiteResult::NumSuccessfulTests() const +{ + return NumTotalTests() - NumFailedTests() - NumSkippedTests(); +} + +int32 FDaeTestSuiteResult::NumFailedTests() const +{ + int32 FailedTests = 0; + + for (const FDaeTestResult& TestResult : TestResults) + { + if (TestResult.HasFailed()) + { + ++FailedTests; + } + } + + return FailedTests; +} + +int32 FDaeTestSuiteResult::NumSkippedTests() const +{ + int32 SkippedTests = 0; + + for (const FDaeTestResult& TestResult : TestResults) + { + if (TestResult.WasSkipped()) + { + ++SkippedTests; + } + } + + return SkippedTests; +} + +float FDaeTestSuiteResult::GetTotalTimeSeconds() const +{ + float TimeSeconds = 0.0f; + + for (const FDaeTestResult& TestResult : TestResults) + { + TimeSeconds += TestResult.TimeSeconds; + } + + return TimeSeconds; +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestTriggerBox.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestTriggerBox.cpp new file mode 100644 index 0000000..cc31290 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaeTestTriggerBox.cpp @@ -0,0 +1,23 @@ +#include "DaeTestTriggerBox.h" +#include "DaeTestLogCategory.h" + +void ADaeTestTriggerBox::BeginPlay() +{ + Super::BeginPlay(); + + bWasTriggered = false; + + OnActorBeginOverlap.AddDynamic(this, &ADaeTestTriggerBox::OnActorBeginOverlapBroadcast); +} + +bool ADaeTestTriggerBox::WasTriggered() const +{ + return bWasTriggered; +} + +void ADaeTestTriggerBox::OnActorBeginOverlapBroadcast(AActor* OverlappedActor, AActor* OtherActor) +{ + bWasTriggered = true; + + UE_LOG(LogDaeTest, Log, TEXT("%s was triggered by %s."), *GetName(), *OtherActor->GetName()); +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaedalicTestAutomationPlugin.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaedalicTestAutomationPlugin.cpp new file mode 100644 index 0000000..1222925 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Private/DaedalicTestAutomationPlugin.cpp @@ -0,0 +1,32 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" +#include "IDaedalicTestAutomationPlugin.h" + + +class FDaedalicTestAutomationPlugin : public IDaedalicTestAutomationPlugin +{ + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; + +IMPLEMENT_MODULE( FDaedalicTestAutomationPlugin, DaedalicTestAutomationPlugin ) + + + +void FDaedalicTestAutomationPlugin::StartupModule() +{ + // This code will execute after your module is loaded into memory (but after global variables are initialized, of course.) +} + + +void FDaedalicTestAutomationPlugin::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +} + + + diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeDelayFramesAction.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeDelayFramesAction.h new file mode 100644 index 0000000..8a1ca5e --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeDelayFramesAction.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +/** Triggers the output link after the specified number of frames. */ +class DAEDALICTESTAUTOMATIONPLUGIN_API FDaeDelayFramesAction : public FPendingLatentAction +{ +public: + int32 FramesRemaining; + FName ExecutionFunction; + int32 OutputLink; + FWeakObjectPtr CallbackTarget; + + FDaeDelayFramesAction(const FLatentActionInfo& LatentInfo, int32 NumFrames); + + virtual void UpdateOperation(FLatentResponse& Response) override; + +#if WITH_EDITOR + virtual FString GetDescription() const override; +#endif +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeDelayUntilTriggeredAction.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeDelayUntilTriggeredAction.h new file mode 100644 index 0000000..b9504d1 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeDelayUntilTriggeredAction.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +class ADaeTestTriggerBox; + +/** Triggers the output link after the specified trigger box has been triggered. */ +class DAEDALICTESTAUTOMATIONPLUGIN_API FDaeDelayUntilTriggeredAction : public FPendingLatentAction +{ +public: + ADaeTestTriggerBox* TestTriggerBox; + FName ExecutionFunction; + int32 OutputLink; + FWeakObjectPtr CallbackTarget; + + FDaeDelayUntilTriggeredAction(const FLatentActionInfo& LatentInfo, + ADaeTestTriggerBox* InTestTriggerBox); + + virtual void UpdateOperation(FLatentResponse& Response) override; + +#if WITH_EDITOR + virtual FString GetDescription() const override; +#endif +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeGauntletStates.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeGauntletStates.h new file mode 100644 index 0000000..3245dd4 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeGauntletStates.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +struct DAEDALICTESTAUTOMATIONPLUGIN_API FDaeGauntletStates : FGauntletStates +{ + static FName LoadingNextMap; + static FName DiscoveringTests; + static FName Running; + static FName Finished; +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeGauntletTestController.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeGauntletTestController.h new file mode 100644 index 0000000..91fc386 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeGauntletTestController.h @@ -0,0 +1,32 @@ +#pragma once + +#include "DaeTestSuiteResult.h" +#include +#include +#include "DaeGauntletTestController.generated.h" + +class ADaeTestSuiteActor; + +/** Controller for automated tests run by Gauntlet. */ +UCLASS() +class DAEDALICTESTAUTOMATIONPLUGIN_API UDaeGauntletTestController : public UGauntletTestController +{ + GENERATED_BODY() + +public: + virtual void OnInit() override; + virtual void OnPostMapChange(UWorld* World) override; + virtual void OnTick(float TimeDelta) override; + +private: + TArray MapNames; + int32 MapIndex; + TArray Results; + + void LoadNextTestMap(); + + UFUNCTION() + void OnTestSuiteFinished(ADaeTestSuiteActor* TestSuite); + + FString ParseCommandLineOption(const FString& Key); +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeJUnitReportWriter.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeJUnitReportWriter.h new file mode 100644 index 0000000..ab0b48a --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeJUnitReportWriter.h @@ -0,0 +1,16 @@ +#pragma once + +/** Writes test reports based on the Apache Ant JUnit report format (based on org.junit.platform.reporting.legacy.xml.XmlReportWriter.writeTestsuite). */ +class DAEDALICTESTAUTOMATIONPLUGIN_API FDaeJUnitReportWriter +{ +public: + /** Returns an XML report based on the Apache Ant JUnit report format. */ + FString CreateReport(const FString& Name, const TArray& TestSuites) const; + +private: + int32 NumTotalTests(const TArray& TestSuites) const; + int32 NumFailedTests(const TArray& TestSuites) const; + int32 NumSkippedTests(const TArray& TestSuites) const; + float GetTotalTimeSeconds(const TArray& TestSuites) const; + FString GetTimestamp(const TArray& TestSuites) const; +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestActor.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestActor.h new file mode 100644 index 0000000..5451c1b --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestActor.h @@ -0,0 +1,116 @@ +#pragma once + +#include +#include +#include "DaeTestActor.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FDaeTestActorTestSuccessfulSignature, ADaeTestActor*, + Test, UObject*, Parameter); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FDaeTestActorTestFailedSignature, ADaeTestActor*, + Test, UObject*, Parameter, const FString&, + FailureMessage); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FDaeTestActorTestSkippedSignature, ADaeTestActor*, + Test, UObject*, Parameter, const FString&, + SkipReason); + +class ADaeTestParameterProviderActor; + +/** Single automated test to be run as part of a test suite. */ +UCLASS() +class DAEDALICTESTAUTOMATIONPLUGIN_API ADaeTestActor : public AActor +{ + GENERATED_BODY() + +public: + ADaeTestActor(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + /** Applies additional providers for appending parameters for this test. */ + void ApplyParameterProviders(); + + /** Starts executing this test. */ + void RunTest(UObject* TestParameter); + + /** Finishes execution of this test, automatically following up with the Assert step. */ + UFUNCTION(BlueprintCallable) + void FinishAct(); + + /** Gets how long this test is allowed to run before it fails automatically, in seconds. */ + float GetTimeoutInSeconds() const; + + /** Gets the parameters to run this test with, one per run. */ + TArray> GetParameters() const; + + /** Gets the parameter for the current test run. */ + UFUNCTION(BlueprintPure) + UObject* GetCurrentParameter() const; + + /** Event when this test has finished successfully. */ + virtual void NotifyOnTestSuccessful(); + + /** Event when this test has failed. */ + virtual void NotifyOnTestFailed(const FString& Message); + + /** Event when this test has been skipped. */ + virtual void NotifyOnTestSkipped(const FString& InSkipReason); + + /** Event when this test should verify its preconditions. */ + virtual void NotifyOnAssume(UObject* Parameter); + + /** Event when this test should set up. */ + virtual void NotifyOnArrange(UObject* Parameter); + + /** Event when this test should execute. */ + virtual void NotifyOnAct(UObject* Parameter); + + /** Event when this test should check the results. */ + virtual void NotifyOnAssert(UObject* Parameter); + + /** Event when this test should verify its preconditions. This is NOT a latent event. */ + UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "Assume")) + void ReceiveOnAssume(UObject* Parameter); + + /** Event when this test should set up. This is NOT a latent event. */ + UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "Arrange")) + void ReceiveOnArrange(UObject* Parameter); + + /** Event when this test should execute. This is a latent event: You need to call FinishAct when you're finished. */ + UFUNCTION(BlueprintNativeEvent, meta = (DisplayName = "Act")) + void ReceiveOnAct(UObject* Parameter); + + /** Event when this test should check the results. This is NOT a latent event. */ + UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "Assert")) + void ReceiveOnAssert(UObject* Parameter); + + /** Event when this test has finished successfully. */ + FDaeTestActorTestSuccessfulSignature OnTestSuccessful; + + /** Event when this test has failed. */ + FDaeTestActorTestFailedSignature OnTestFailed; + + /** Event when this test has been skipped. */ + FDaeTestActorTestSkippedSignature OnTestSkipped; + +private: + /** How long this test is allowed to run before it fails automatically, in seconds. */ + UPROPERTY(EditAnywhere) + float TimeoutInSeconds; + + /** Reason for skipping this test. Test will be skipped if this is not empty. Useful for temporarily disabling unstable tests. */ + UPROPERTY(EditAnywhere) + FString SkipReason; + + /** Parameterizes this test, running it multiple times, once per specified parameter. */ + UPROPERTY(EditAnywhere) + TArray> Parameters; + + /** Additional providers for appending parameters for this test. Applied exactly once before the first test run. */ + UPROPERTY(EditAnywhere) + TArray ParameterProviders; + + /** Parameter for the current test run. */ + UPROPERTY() + UObject* CurrentParameter; + + /** Whether this test has finished executing (either with success or failure). */ + bool bHasResult; +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestActorBlueprint.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestActorBlueprint.h new file mode 100644 index 0000000..1ef5952 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestActorBlueprint.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include "DaeTestActorBlueprint.generated.h" + +/** Single automated test to be run as part of a test suite. */ +UCLASS(BlueprintType) +class DAEDALICTESTAUTOMATIONPLUGIN_API UDaeTestActorBlueprint : public UBlueprint +{ + GENERATED_BODY() + +public: +#if WITH_EDITOR + // UBlueprint interface + virtual bool SupportedByDefaultBlueprintFactory() const override + { + return false; + } + // End of UBlueprint interface +#endif +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestAssertBlueprintFunctionLibrary.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestAssertBlueprintFunctionLibrary.h new file mode 100644 index 0000000..c1872f9 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestAssertBlueprintFunctionLibrary.h @@ -0,0 +1,350 @@ +#pragma once + +#include "DaeTestComparisonMethod.h" +#include +#include +#include "DaeTestAssertBlueprintFunctionLibrary.generated.h" + +class UImage; +class URichTextBlock; +class UTextBlock; +class UUserWidget; + +class ADaeTestTriggerBox; + +/** Utility functions for asserting state in automated tests. */ +UCLASS() +class DAEDALICTESTAUTOMATIONPLUGIN_API UDaeTestAssertBlueprintFunctionLibrary + : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** Finishes the current test as failure. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context")) + static void AssertFail(const FString& What, UObject* Context = nullptr); + + /** Expects the specified value to be true. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context")) + static void AssertTrue(bool bValue, const FString& What, UObject* Context = nullptr); + + /** Expects the specified value to be false. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context")) + static void AssertFalse(bool bValue, const FString& What, UObject* Context = nullptr); + + /** Expects the specified object to be valid. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context")) + static void AssertValid(UObject* Object, const FString& What, UObject* Context = nullptr); + + /** Expects the specified object not to be valid. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context")) + static void AssertInvalid(UObject* Object, const FString& What, UObject* Context = nullptr); + + /** Expects the specified trigger box to be triggered. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context")) + static void AssertWasTriggered(ADaeTestTriggerBox* TestTriggerBox, UObject* Context = nullptr); + + /** Expects the specified trigger box not to be triggered. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context")) + static void AssertWasNotTriggered(ADaeTestTriggerBox* TestTriggerBox, + UObject* Context = nullptr); + + /** Expects the specified bytes to be equal. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Equal (Byte)")) + static void AssertEqualByte(uint8 Actual, uint8 Expected, const FString& What, + UObject* Context = nullptr); + + /** Expects the specified bytes not to be equal. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Not Equal (Byte)")) + static void AssertNotEqualByte(uint8 Actual, uint8 Unexpected, const FString& What, + UObject* Context = nullptr); + + /** Compares the specified bytes for order. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Compare (Byte)")) + static void AssertCompareByte(uint8 First, EDaeTestComparisonMethod ShouldBe, uint8 Second, + const FString& What, UObject* Context = nullptr); + + /** Expects the specified 32-bit integers to be equal. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Equal (Integer)")) + static void AssertEqualInt32(int32 Actual, int32 Expected, const FString& What, + UObject* Context = nullptr); + + /** Expects the specified 32-bit integers not to be equal. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Not Equal (Integer)")) + static void AssertNotEqualInt32(int32 Actual, int32 Unexpected, const FString& What, + UObject* Context = nullptr); + + /** Compares the specified 32-bit integers for order. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Compare (Integer)")) + static void AssertCompareInt32(int32 First, EDaeTestComparisonMethod ShouldBe, int32 Second, + const FString& What, UObject* Context = nullptr); + + /** Expects the specified 64-bit integers to be equal. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Equal (Integer64)")) + static void AssertEqualInt64(int64 Actual, int64 Expected, const FString& What, + UObject* Context = nullptr); + + /** Expects the specified 64-bit integers not to be equal. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Not Equal (Integer64)")) + static void AssertNotEqualInt64(int64 Actual, int64 Unexpected, const FString& What, + UObject* Context = nullptr); + + /** Compares the specified 64-bit integers for order. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Compare (Integer64)")) + static void AssertCompareInt64(int64 First, EDaeTestComparisonMethod ShouldBe, int64 Second, + const FString& What, UObject* Context = nullptr); + + /** Expects the specified floats to be (nearly) equal. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Equal (Float)")) + static void AssertEqualFloat(float Actual, float Expected, const FString& What, + UObject* Context = nullptr); + + /** Expects the specified floats not to be equal. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Not Equal (Float)")) + static void AssertNotEqualFloat(float Actual, float Unexpected, const FString& What, + UObject* Context = nullptr); + + /** Compares the specified floats for order. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Compare (Float)")) + static void AssertCompareFloat(float First, EDaeTestComparisonMethod ShouldBe, float Second, + const FString& What, UObject* Context = nullptr); + + /** Expects the specified names to be equal. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Equal (Name)")) + static void AssertEqualName(const FName& Actual, const FName& Expected, bool bIgnoreCase, + const FString& What, UObject* Context = nullptr); + + /** Expects the specified names not to be equal. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Not Equal (Name)")) + static void AssertNotEqualName(const FName& Actual, const FName& Unexpected, bool bIgnoreCase, + const FString& What, UObject* Context = nullptr); + + /** Expects the specified strings to be equal. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Equal (String)")) + static void AssertEqualString(const FString& Actual, const FString& Expected, bool bIgnoreCase, + const FString& What, UObject* Context = nullptr); + + /** Expects the specified strings not to be equal. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Not Equal (String)")) + static void AssertNotEqualString(const FString& Actual, const FString& Unexpected, + bool bIgnoreCase, const FString& What, + UObject* Context = nullptr); + + /** Expects the specified texts to be equal. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Equal (Text)")) + static void AssertEqualText(const FText& Actual, const FText& Expected, bool bIgnoreCase, + const FString& What, UObject* Context = nullptr); + + /** Expects the specified texts not to be equal. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Not Equal (Text)")) + static void AssertNotEqualText(const FText& Actual, const FText& Unexpected, bool bIgnoreCase, + const FString& What, UObject* Context = nullptr); + + /** Expects the specified vectors to be (nearly) equal. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Equal (Vector)")) + static void AssertEqualVector(const FVector& Actual, const FVector& Expected, + const FString& What, UObject* Context = nullptr); + + /** Expects the specified vectors not to be equal. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Not Equal (Vector)")) + static void AssertNotEqualVector(const FVector& Actual, const FVector& Unexpected, + const FString& What, UObject* Context = nullptr); + + /** Expects the specified rotators to be (nearly) equal. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Equal (Rotator)")) + static void AssertEqualRotator(const FRotator& Actual, const FRotator& Expected, + const FString& What, UObject* Context = nullptr); + + /** Expects the specified rotators not to be equal. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Not Equal (Rotator)")) + static void AssertNotEqualRotator(const FRotator& Actual, const FRotator& Unexpected, + const FString& What, UObject* Context = nullptr); + + /** Expects the specified transforms to be (nearly) equal. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Equal (Transform)")) + static void AssertEqualTransform(const FTransform& Actual, const FTransform& Expected, + const FString& What, UObject* Context = nullptr); + + /** Expects the specified transforms not to be equal. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Not Equal (Transform)")) + static void AssertNotEqualTransform(const FTransform& Actual, const FTransform& Unexpected, + const FString& What, UObject* Context = nullptr); + + /** Expects Value to be between MinInclusive and MaxInclusive. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert In Range (Byte)")) + static void AssertInRangeByte(uint8 Value, uint8 MinInclusive, uint8 MaxInclusive, + const FString& What, UObject* Context = nullptr); + + /** Expects Value not to be between MinInclusive and MaxInclusive. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Not In Range (Byte)")) + static void AssertNotInRangeByte(uint8 Value, uint8 MinInclusive, uint8 MaxInclusive, + const FString& What, UObject* Context = nullptr); + + /** Expects Value to be between MinInclusive and MaxInclusive. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert In Range (Integer)")) + static void AssertInRangeInt32(int32 Value, int32 MinInclusive, int32 MaxInclusive, + const FString& What, UObject* Context = nullptr); + + /** Expects Value not to be between MinInclusive and MaxInclusive. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Not In Range (Integer)")) + static void AssertNotInRangeInt32(int32 Value, int32 MinInclusive, int32 MaxInclusive, + const FString& What, UObject* Context = nullptr); + + /** Expects Value to be between MinInclusive and MaxInclusive. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert In Range (Integer64)")) + static void AssertInRangeInt64(int64 Value, int64 MinInclusive, int64 MaxInclusive, + const FString& What, UObject* Context = nullptr); + + /** Expects Value not to be between MinInclusive and MaxInclusive. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Not In Range (Integer64)")) + static void AssertNotInRangeInt64(int64 Value, int64 MinInclusive, int64 MaxInclusive, + const FString& What, UObject* Context = nullptr); + + /** Expects Value to be between MinInclusive and MaxInclusive. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert In Range (Float)")) + static void AssertInRangeFloat(float Value, float MinInclusive, float MaxInclusive, + const FString& What, UObject* Context = nullptr); + + /** Expects Value not to be between MinInclusive and MaxInclusive. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context", + DisplayName = "Assert Not In Range (Float)")) + static void AssertNotInRangeFloat(float Value, float MinInclusive, float MaxInclusive, + const FString& What, UObject* Context = nullptr); + + /** Expects the specified widget to be valid and visible (e.g. added to viewport, not hidden or collapsed). */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context")) + static void AssertWidgetIsVisible(UUserWidget* Widget, const FString& What, + UObject* Context = nullptr); + + /** Expects the specified text not to be empty. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context")) + static void AssertTextIsSet(UTextBlock* TextBlock, const FString& What, + UObject* Context = nullptr); + + /** Expects the specified rich text not to be empty. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context")) + static void AssertRichTextIsSet(URichTextBlock* RichTextBlock, const FString& What, + UObject* Context = nullptr); + + /** Expects the specified image to be set up to use a texture or material. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context")) + static void AssertImageIsSet(UImage* Image, const FString& What, UObject* Context = nullptr); + +private: + static const FString ErrorMessageFormatEqual; + static const FString ErrorMessageFormatNotEqual; + static const FString ErrorMessageFormatLessThan; + static const FString ErrorMessageFormatLessThanOrEqualTo; + static const FString ErrorMessageFormatGreaterThan; + static const FString ErrorMessageFormatGreaterThanOrEqualTo; + static const FString ErrorMessageFormatInRange; + static const FString ErrorMessageFormatNotInRange; + + static void OnTestFailed(UObject* Context, const FString& Message); + + template + static void AssertEqual(UObject* Context, const FString& What, T Actual, T Expected) + { + if (Actual != Expected) + { + FString Message = FString::Format(*ErrorMessageFormatEqual, {What, Expected, Actual}); + OnTestFailed(Context, Message); + } + } + + template + static void AssertNotEqual(UObject* Context, const FString& What, T Actual, T Unexpected) + { + if (Actual == Unexpected) + { + FString Message = FString::Format(*ErrorMessageFormatNotEqual, {What, Unexpected}); + OnTestFailed(Context, Message); + } + } + + template + static void AssertCompare(UObject* Context, const FString& What, T First, + EDaeTestComparisonMethod ShouldBe, T Second) + { + bool bFulfilled = false; + + switch (ShouldBe) + { + case EDaeTestComparisonMethod::LessThan: + bFulfilled = First < Second; + break; + + case EDaeTestComparisonMethod::LessThanOrEqualTo: + bFulfilled = First <= Second; + break; + + case EDaeTestComparisonMethod::GreaterThanOrEqualTo: + bFulfilled = First >= Second; + break; + + case EDaeTestComparisonMethod::GreaterThan: + bFulfilled = First > Second; + break; + } + + if (bFulfilled) + { + return; + } + + FString FormatString; + + switch (ShouldBe) + { + case EDaeTestComparisonMethod::LessThan: + FormatString = ErrorMessageFormatLessThan; + break; + + case EDaeTestComparisonMethod::LessThanOrEqualTo: + FormatString = ErrorMessageFormatLessThanOrEqualTo; + break; + + case EDaeTestComparisonMethod::GreaterThanOrEqualTo: + FormatString = ErrorMessageFormatGreaterThanOrEqualTo; + break; + + case EDaeTestComparisonMethod::GreaterThan: + FormatString = ErrorMessageFormatGreaterThan; + break; + } + + FString Message = FString::Format(*FormatString, {What, First, Second}); + OnTestFailed(Context, Message); + } +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestAssumeBlueprintFunctionLibrary.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestAssumeBlueprintFunctionLibrary.h new file mode 100644 index 0000000..89a50a0 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestAssumeBlueprintFunctionLibrary.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include "DaeTestAssumeBlueprintFunctionLibrary.generated.h" + +/** Utility functions for assuming state before acting in automated tests. */ +UCLASS() +class DAEDALICTESTAUTOMATIONPLUGIN_API UDaeTestAssumeBlueprintFunctionLibrary + : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** Expects the specified value to be true. Failed assumptions will cause automated tests to be skipped instead of failed. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context")) + static void AssumeTrue(bool bValue, const FString& What, UObject* Context = nullptr); + + /** Expects the specified value to be true. Failed assumptions will cause automated tests to be skipped instead of failed. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context")) + static void AssumeFalse(bool bValue, const FString& What, UObject* Context = nullptr); + +private: + static void OnTestSkipped(UObject* Context, const FString& Message); +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestAutomationPluginSettings.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestAutomationPluginSettings.h new file mode 100644 index 0000000..336f583 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestAutomationPluginSettings.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include "DaeTestAutomationPluginSettings.generated.h" + +DECLARE_MULTICAST_DELEGATE_OneParam(FDaeTestAutomationPluginSettingsTestMapPathChangedSignature, + const FString&); + +/** Custom settings for this plugin. */ +UCLASS(config = Game, defaultconfig) +class DAEDALICTESTAUTOMATIONPLUGIN_API UDaeTestAutomationPluginSettings : public UObject +{ + GENERATED_BODY() + +public: + /** Path to look for test maps in. */ + UPROPERTY(config, EditAnywhere) + FString TestMapPath; + + UDaeTestAutomationPluginSettings(); + + virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override; + + /** Event when the path to look for test maps in has changed. */ + FDaeTestAutomationPluginSettingsTestMapPathChangedSignature OnTestMapPathChanged; +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestBlueprintFunctionLibrary.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestBlueprintFunctionLibrary.h new file mode 100644 index 0000000..cdd5f25 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestBlueprintFunctionLibrary.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include "DaeTestBlueprintFunctionLibrary.generated.h" + +class ADaeTestTriggerBox; + +/** Utility functions for automating tests. */ +UCLASS() +class DAEDALICTESTAUTOMATIONPLUGIN_API UDaeTestBlueprintFunctionLibrary + : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** Triggers the output link after the specified number of frames. */ + UFUNCTION(BlueprintCallable, + meta = (Latent, WorldContext = "WorldContextObject", LatentInfo = "LatentInfo")) + static void DelayFrames(UObject* WorldContextObject, struct FLatentActionInfo LatentInfo, + int32 NumFrames = 1); + + /** Triggers the output link after the specified trigger box has been triggered. */ + UFUNCTION(BlueprintCallable, + meta = (Latent, WorldContext = "WorldContextObject", LatentInfo = "LatentInfo")) + static void DelayUntilTriggered(UObject* WorldContextObject, + struct FLatentActionInfo LatentInfo, + ADaeTestTriggerBox* TestTriggerBox); +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestComparisonMethod.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestComparisonMethod.h new file mode 100644 index 0000000..82afbbd --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestComparisonMethod.h @@ -0,0 +1,12 @@ +#pragma once + +#include "DaeTestComparisonMethod.generated.h" + +UENUM(BlueprintType) +enum class EDaeTestComparisonMethod : uint8 +{ + LessThan, + LessThanOrEqualTo, + GreaterThanOrEqualTo, + GreaterThan +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestDelayBlueprintFunctionLibrary.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestDelayBlueprintFunctionLibrary.h new file mode 100644 index 0000000..83fe844 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestDelayBlueprintFunctionLibrary.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include "DaeTestDelayBlueprintFunctionLibrary.generated.h" + +class ADaeTestTriggerBox; + +/** Utility functions for sequencing actions in automated tests. */ +UCLASS() +class DAEDALICTESTAUTOMATIONPLUGIN_API UDaeTestDelayBlueprintFunctionLibrary + : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** Triggers the output link after the specified number of frames. */ + UFUNCTION(BlueprintCallable, + meta = (Latent, WorldContext = "WorldContextObject", LatentInfo = "LatentInfo")) + static void DelayFrames(UObject* WorldContextObject, struct FLatentActionInfo LatentInfo, + int32 NumFrames = 1); + + /** Triggers the output link after the specified trigger box has been triggered. */ + UFUNCTION(BlueprintCallable, + meta = (Latent, WorldContext = "WorldContextObject", LatentInfo = "LatentInfo")) + static void DelayUntilTriggered(UObject* WorldContextObject, + struct FLatentActionInfo LatentInfo, + ADaeTestTriggerBox* TestTriggerBox); +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestInputBlueprintFunctionLibrary.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestInputBlueprintFunctionLibrary.h new file mode 100644 index 0000000..f3ba780 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestInputBlueprintFunctionLibrary.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include "DaeTestInputBlueprintFunctionLibrary.generated.h" + +/** Utility functions for simulating input in automated tests. */ +UCLASS() +class DAEDALICTESTAUTOMATIONPLUGIN_API UDaeTestInputBlueprintFunctionLibrary + : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** Applies the input action with the specified name once. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context")) + static void ApplyInputAction(UObject* Context, const FName& ActionName, + EInputEvent InputEventType = EInputEvent::IE_Pressed); + + /** Applies the input axis with the specified name. Pass AxisValue 0.0f to reset the input axis. */ + UFUNCTION(BlueprintCallable, meta = (HidePin = "Context", DefaultToSelf = "Context")) + static void ApplyInputAxis(UObject* Context, const FName& AxisName, float AxisValue = 1.0f); +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestLogCategory.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestLogCategory.h new file mode 100644 index 0000000..4fc89ec --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestLogCategory.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +DECLARE_LOG_CATEGORY_EXTERN(LogDaeTest, Log, All); diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestParameterProviderActor.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestParameterProviderActor.h new file mode 100644 index 0000000..c476767 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestParameterProviderActor.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include "DaeTestParameterProviderActor.generated.h" + +/** Provides a set of parameters for tests, which are run once per parameter. */ +UCLASS() +class DAEDALICTESTAUTOMATIONPLUGIN_API ADaeTestParameterProviderActor : public AActor +{ + GENERATED_BODY() + +public: + /** Gets the parameters to run the test with, one per run. */ + UFUNCTION(BlueprintNativeEvent) + TArray GetParameters(); +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestParameterProviderActorBlueprint.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestParameterProviderActorBlueprint.h new file mode 100644 index 0000000..095fb03 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestParameterProviderActorBlueprint.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include "DaeTestParameterProviderActorBlueprint.generated.h" + +/** Provides a set of parameters for tests, which are run once per parameter. */ +UCLASS(BlueprintType) +class DAEDALICTESTAUTOMATIONPLUGIN_API UDaeTestParameterProviderActorBlueprint : public UBlueprint +{ + GENERATED_BODY() + +public: +#if WITH_EDITOR + // UBlueprint interface + virtual bool SupportedByDefaultBlueprintFactory() const override + { + return false; + } + // End of UBlueprint interface +#endif +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestResult.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestResult.h new file mode 100644 index 0000000..682e199 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestResult.h @@ -0,0 +1,30 @@ +#pragma once + +/** Result set of a single test. */ +class DAEDALICTESTAUTOMATIONPLUGIN_API FDaeTestResult +{ +public: + /** Name of the test. */ + FString TestName; + + /** (Optional) Why the test failed. */ + FString FailureMessage; + + /** (Optional) Why the test was skipped. */ + FString SkipReason; + + /** Time the test ran, in seconds. */ + float TimeSeconds; + + FDaeTestResult(); + FDaeTestResult(FString InTestName, float InTimeSeconds); + + /** Whether the test finished without failure, or not. */ + bool WasSuccessful() const; + + /** Whether the test has failed. */ + bool HasFailed() const; + + /** Whether this test has been skipped instead of being run. */ + bool WasSkipped() const; +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestSuiteActor.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestSuiteActor.h new file mode 100644 index 0000000..cf52c2d --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestSuiteActor.h @@ -0,0 +1,111 @@ +#pragma once + +#include "DaeTestSuiteResult.h" +#include +#include +#include "DaeTestSuiteActor.generated.h" + +class ADaeTestActor; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDaeTestSuiteActorTestSuiteSuccessfulSignature, + ADaeTestSuiteActor*, TestSuite); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDaeTestSuiteActorTestSuiteFailedSignature, + ADaeTestSuiteActor*, TestSuite); + +/** Collection of automated tests. */ +UCLASS() +class DAEDALICTESTAUTOMATIONPLUGIN_API ADaeTestSuiteActor : public AActor +{ + GENERATED_BODY() + +public: + ADaeTestSuiteActor(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + virtual void BeginPlay() override; + virtual void Tick(float DeltaSeconds) override; + + /** Runs all tests of this suite, in order. */ + void RunAllTests(); + + /** Whether this test suite is currently running. */ + bool IsRunning() const; + + /** Gets the test that is currently running. */ + ADaeTestActor* GetCurrentTest() const; + + /** Gets the parameter for the current test run. */ + UObject* GetCurrentTestParameter() const; + + /** Gets the name of the current test. */ + FString GetCurrentTestName() const; + + /** Results of the whole test suite. */ + FDaeTestSuiteResult GetResult() const; + + /** Event when this test suite should set up. */ + virtual void NotifyOnBeforeAll(); + + /** Event when this test suite has finished all tests. */ + virtual void NotifyOnAfterAll(); + + /** Event when this test suite should set up for the next test. */ + virtual void NotifyOnBeforeEach(); + + /** Event when this test suite should has finished a test. */ + virtual void NotifyOnAfterEach(); + + /** Event when this test suite should set up. This is NOT a latent event. */ + UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "BeforeAll")) + void ReceiveOnBeforeAll(); + + /** Event when this test suite has finished all tests. This is NOT a latent event. */ + UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "AfterAll")) + void ReceiveOnAfterAll(); + + /** Event when this test suite should set up for the next test. This is NOT a latent event. */ + UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "BeforeEach")) + void ReceiveOnBeforeEach(); + + /** Event when this test suite should has finished a test. This is NOT a latent event. */ + UFUNCTION(BlueprintImplementableEvent, meta = (DisplayName = "AfterEach")) + void ReceiveOnAfterEach(); + + /** Event when this test suite has successfully finished all tests. */ + FDaeTestSuiteActorTestSuiteSuccessfulSignature OnTestSuiteSuccessful; + + /** Event when any tests of this test suite have failed. */ + FDaeTestSuiteActorTestSuiteFailedSignature OnTestSuiteFailed; + +private: + /** Tests to run in this level. */ + UPROPERTY(EditInstanceOnly) + TArray Tests; + + /** Whether to automatically run this test suite on BeginPlay in Play In Editor. */ + UPROPERTY(EditInstanceOnly) + bool bRunInPIE; + + /** Index of the current test. */ + int32 TestIndex; + + /** Index of the current parameter the current test is run with. */ + int32 TestParameterIndex; + + /** Time the current test has been running, in seconds. */ + float TestTimeSeconds; + + /** Results of the whole test suite. */ + FDaeTestSuiteResult Result; + + /** Runs the next test in this test suite. */ + void RunNextTest(); + + UFUNCTION() + void OnTestSuccessful(ADaeTestActor* Test, UObject* Parameter); + + UFUNCTION() + void OnTestFailed(ADaeTestActor* Test, UObject* Parameter, const FString& FailureMessage); + + UFUNCTION() + void OnTestSkipped(ADaeTestActor* Test, UObject* Parameter, const FString& SkipReason); +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestSuiteActorBlueprint.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestSuiteActorBlueprint.h new file mode 100644 index 0000000..fce0cdd --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestSuiteActorBlueprint.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include "DaeTestSuiteActorBlueprint.generated.h" + +/** Suite of automated tests to be run one after another. */ +UCLASS(BlueprintType) +class DAEDALICTESTAUTOMATIONPLUGIN_API UDaeTestSuiteActorBlueprint : public UBlueprint +{ + GENERATED_BODY() + +public: +#if WITH_EDITOR + // UBlueprint interface + virtual bool SupportedByDefaultBlueprintFactory() const override + { + return false; + } + // End of UBlueprint interface +#endif +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestSuiteResult.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestSuiteResult.h new file mode 100644 index 0000000..ea0e656 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestSuiteResult.h @@ -0,0 +1,35 @@ +#pragma once + +#include "DaeTestResult.h" + +/** Result set of a whole test suite. */ +class DAEDALICTESTAUTOMATIONPLUGIN_API FDaeTestSuiteResult +{ +public: + /** Name of the map the test suite ran in. */ + FString MapName; + + /** Name of the test suite that ran. */ + FString TestSuiteName; + + /** UTC date and time the test suite started running. */ + FDateTime Timestamp; + + /** Results of all individual tests of the test suite. */ + TArray TestResults; + + /** How many tests of the test suite have been run. */ + int32 NumTotalTests() const; + + /** How many tests of the test suite have been successful. */ + int32 NumSuccessfulTests() const; + + /** How many tests of the test suite have failed. */ + int32 NumFailedTests() const; + + /** How many tests of the test suite have been skipped instead of being run. */ + int32 NumSkippedTests() const; + + /** Combined time all tests ran, in seconds. */ + float GetTotalTimeSeconds() const; +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestTriggerBox.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestTriggerBox.h new file mode 100644 index 0000000..7821dad --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/DaeTestTriggerBox.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include "DaeTestTriggerBox.generated.h" + +/** Trigger box to be used in automated tests. */ +UCLASS() +class DAEDALICTESTAUTOMATIONPLUGIN_API ADaeTestTriggerBox : public ATriggerBox +{ + GENERATED_BODY() + +public: + virtual void BeginPlay() override; + + /** Whether this trigger box has been triggered at least once. */ + UFUNCTION(BlueprintPure) + bool WasTriggered() const; + +private: + /** Whether this trigger box has been triggered at least once. */ + bool bWasTriggered; + + UFUNCTION() + void OnActorBeginOverlapBroadcast(AActor* OverlappedActor, AActor* OtherActor); +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/IDaedalicTestAutomationPlugin.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/IDaedalicTestAutomationPlugin.h new file mode 100644 index 0000000..75039e4 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPlugin/Public/IDaedalicTestAutomationPlugin.h @@ -0,0 +1,40 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleInterface.h" +#include "Modules/ModuleManager.h" + + +/** + * The public interface to this module. In most cases, this interface is only public to sibling modules + * within this plugin. + */ +class IDaedalicTestAutomationPlugin : public IModuleInterface +{ + +public: + + /** + * Singleton-like access to this module's interface. This is just for convenience! + * Beware of calling this during the shutdown phase, though. Your module might have been unloaded already. + * + * @return Returns singleton instance, loading the module on demand if needed + */ + static inline IDaedalicTestAutomationPlugin& Get() + { + return FModuleManager::LoadModuleChecked< IDaedalicTestAutomationPlugin >( "DaedalicTestAutomationPlugin" ); + } + + /** + * Checks to see if this module is loaded and ready. It is only valid to call Get() if IsAvailable() returns true. + * + * @return True if the module is loaded and ready to use + */ + static inline bool IsAvailable() + { + return FModuleManager::Get().IsModuleLoaded( "DaedalicTestAutomationPlugin" ); + } +}; + diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/DaedalicTestAutomationPluginEditor.Build.cs b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/DaedalicTestAutomationPluginEditor.Build.cs new file mode 100644 index 0000000..e93ccd0 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/DaedalicTestAutomationPluginEditor.Build.cs @@ -0,0 +1,51 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +namespace UnrealBuildTool.Rules +{ + public class DaedalicTestAutomationPluginEditor : ModuleRules + { + public DaedalicTestAutomationPluginEditor(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicIncludePaths.AddRange( + new string[] { + // ... add public include paths required here ... + } + ); + + PrivateIncludePaths.AddRange( + new string[] { + // ... add other private include paths required here ... + } + ); + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreUObject", + "Engine", + "DaedalicTestAutomationPlugin", + "UnrealEd", + "BlueprintGraph", + "AssetTools" + } + ); + + PrivateDependencyModuleNames.AddRange( + new string[] + { + // ... add private dependencies that you statically link with here ... + } + ); + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } + } +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AssetTypeActions_DaeTestActorBlueprint.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AssetTypeActions_DaeTestActorBlueprint.cpp new file mode 100644 index 0000000..8344236 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AssetTypeActions_DaeTestActorBlueprint.cpp @@ -0,0 +1,35 @@ +#include "AssetTypeActions_DaeTestActorBlueprint.h" +#include "DaeTestActorBlueprint.h" +#include "DaeTestActorBlueprintFactory.h" + +#define LOCTEXT_NAMESPACE "DaedalicTestAutomationPlugin" + +FAssetTypeActions_DaeTestActorBlueprint::FAssetTypeActions_DaeTestActorBlueprint( + EAssetTypeCategories::Type InAssetTypeCategory) + : AssetTypeCategory(InAssetTypeCategory) +{ +} + +FText FAssetTypeActions_DaeTestActorBlueprint::GetName() const +{ + return NSLOCTEXT("DaedalicTestAutomationPlugin", "AssetTypeActions_DaeTestActorBlueprint", + "Test Actor Blueprint"); +} + +UClass* FAssetTypeActions_DaeTestActorBlueprint::GetSupportedClass() const +{ + return UDaeTestActorBlueprint::StaticClass(); +} + +uint32 FAssetTypeActions_DaeTestActorBlueprint::GetCategories() +{ + return AssetTypeCategory; +} + +UFactory* FAssetTypeActions_DaeTestActorBlueprint::GetFactoryForBlueprintType( + UBlueprint* InBlueprint) const +{ + return NewObject(); +} + +#undef LOCTEXT_NAMESPACE diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AssetTypeActions_DaeTestParameterProviderActorBlueprint.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AssetTypeActions_DaeTestParameterProviderActorBlueprint.cpp new file mode 100644 index 0000000..27ae1fc --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AssetTypeActions_DaeTestParameterProviderActorBlueprint.cpp @@ -0,0 +1,37 @@ +#include "AssetTypeActions_DaeTestParameterProviderActorBlueprint.h" +#include "DaeTestParameterProviderActorBlueprint.h" +#include "DaeTestParameterProviderActorBlueprintFactory.h" + +#define LOCTEXT_NAMESPACE "DaedalicTestAutomationPlugin" + +FAssetTypeActions_DaeTestParameterProviderActorBlueprint:: + FAssetTypeActions_DaeTestParameterProviderActorBlueprint( + EAssetTypeCategories::Type InAssetTypeCategory) + : AssetTypeCategory(InAssetTypeCategory) +{ +} + +FText FAssetTypeActions_DaeTestParameterProviderActorBlueprint::GetName() const +{ + return NSLOCTEXT("DaedalicTestAutomationPlugin", + "AssetTypeActions_DaeTestParameterProviderActorBlueprint", + "Test Parameter Provider Actor Blueprint"); +} + +UClass* FAssetTypeActions_DaeTestParameterProviderActorBlueprint::GetSupportedClass() const +{ + return UDaeTestParameterProviderActorBlueprint::StaticClass(); +} + +uint32 FAssetTypeActions_DaeTestParameterProviderActorBlueprint::GetCategories() +{ + return AssetTypeCategory; +} + +UFactory* FAssetTypeActions_DaeTestParameterProviderActorBlueprint::GetFactoryForBlueprintType( + UBlueprint* InBlueprint) const +{ + return NewObject(); +} + +#undef LOCTEXT_NAMESPACE diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AssetTypeActions_DaeTestSuiteActorBlueprint.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AssetTypeActions_DaeTestSuiteActorBlueprint.cpp new file mode 100644 index 0000000..c5db1db --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AssetTypeActions_DaeTestSuiteActorBlueprint.cpp @@ -0,0 +1,35 @@ +#include "AssetTypeActions_DaeTestSuiteActorBlueprint.h" +#include "DaeTestSuiteActorBlueprint.h" +#include "DaeTestSuiteActorBlueprintFactory.h" + +#define LOCTEXT_NAMESPACE "DaedalicTestAutomationPlugin" + +FAssetTypeActions_DaeTestSuiteActorBlueprint::FAssetTypeActions_DaeTestSuiteActorBlueprint( + EAssetTypeCategories::Type InAssetTypeCategory) + : AssetTypeCategory(InAssetTypeCategory) +{ +} + +FText FAssetTypeActions_DaeTestSuiteActorBlueprint::GetName() const +{ + return NSLOCTEXT("DaedalicTestAutomationPlugin", "AssetTypeActions_DaeTestSuiteActorBlueprint", + "Test Suite Actor Blueprint"); +} + +UClass* FAssetTypeActions_DaeTestSuiteActorBlueprint::GetSupportedClass() const +{ + return UDaeTestSuiteActorBlueprint::StaticClass(); +} + +uint32 FAssetTypeActions_DaeTestSuiteActorBlueprint::GetCategories() +{ + return AssetTypeCategory; +} + +UFactory* FAssetTypeActions_DaeTestSuiteActorBlueprint::GetFactoryForBlueprintType( + UBlueprint* InBlueprint) const +{ + return NewObject(); +} + +#undef LOCTEXT_NAMESPACE diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkCommands.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkCommands.cpp new file mode 100644 index 0000000..d9e6163 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkCommands.cpp @@ -0,0 +1,32 @@ +#include "DaeTestAutomationPluginAutomationTestFrameworkCommands.h" +#include "DaeTestSuiteActor.h" +#include +#include + +bool FDaeTestAutomationPluginWaitForEndOfTestSuite::Update() +{ + if (!GEditor) + { + return false; + } + + if (!GEditor->PlayWorld) + { + return false; + } + + if (!Context.CurrentTestSuite) + { + for (TActorIterator Iter(GEditor->PlayWorld); Iter; ++Iter) + { + Context.CurrentTestSuite = *Iter; + } + } + + if (Context.CurrentTestSuite) + { + return !Context.CurrentTestSuite->IsRunning(); + } + + return false; +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkIntegration.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkIntegration.cpp new file mode 100644 index 0000000..82e0416 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkIntegration.cpp @@ -0,0 +1,34 @@ +#include "DaeTestAutomationPluginAutomationTestFrameworkIntegration.h" +#include "DaeTestEditorLogCategory.h" +#include +#include +#include + +void FDaeTestAutomationPluginAutomationTestFrameworkIntegration::SetTestMapPath( + const FString& InTestMapPath) +{ + UE_LOG(LogDaeTestEditor, Log, TEXT("Discovering tests from: %s"), *InTestMapPath); + + // Unregister existing tests. + Tests.Empty(); + + // Register new tests (based on FLoadAllMapsInEditorTest). + TArray PackageFiles; + FEditorFileUtils::FindAllPackageFiles(PackageFiles); + + // Iterate over all files, adding the ones with the map extension. + const FString PatternToCheck = FString::Printf(TEXT("/%s/"), *InTestMapPath); + + for (const FString& Filename : PackageFiles) + { + if (FPaths::GetExtension(Filename, true) == FPackageName::GetMapPackageExtension() + && Filename.Contains(*PatternToCheck)) + { + TSharedPtr NewTest = + MakeShareable(new FDaeTestAutomationPluginAutomationTestFrameworkTest(Filename)); + Tests.Add(NewTest); + + UE_LOG(LogDaeTestEditor, Log, TEXT("Discovered test: %s"), *NewTest->GetMapName()); + } + } +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkTest.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkTest.cpp new file mode 100644 index 0000000..2c0be17 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkTest.cpp @@ -0,0 +1,66 @@ +#include "DaeTestAutomationPluginAutomationTestFrameworkTest.h" +#include "DaeTestAutomationPluginAutomationTestFrameworkCommands.h" +#include "DaeTestAutomationPluginAutomationTestFrameworkTestContext.h" +#include "DaeTestEditorLogCategory.h" +#include +#include +#include + +FDaeTestAutomationPluginAutomationTestFrameworkTest:: + FDaeTestAutomationPluginAutomationTestFrameworkTest(const FString& InMapName) + : FAutomationTestBase(InMapName, false) + , MapName(InMapName) +{ + // Test is automatically registered in FAutomationTestBase base class constructor. +} + +uint32 FDaeTestAutomationPluginAutomationTestFrameworkTest::GetTestFlags() const +{ + return EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter; +} + +uint32 FDaeTestAutomationPluginAutomationTestFrameworkTest::GetRequiredDeviceNum() const +{ + return 1; +} + +FString FDaeTestAutomationPluginAutomationTestFrameworkTest::GetTestSourceFileName() const +{ + return GetMapName(); +} + +int32 FDaeTestAutomationPluginAutomationTestFrameworkTest::GetTestSourceFileLine() const +{ + return 0; +} + +FString FDaeTestAutomationPluginAutomationTestFrameworkTest::GetMapName() const +{ + return MapName; +} + +void FDaeTestAutomationPluginAutomationTestFrameworkTest::GetTests( + TArray& OutBeautifiedNames, TArray& OutTestCommands) const +{ + OutBeautifiedNames.Add(GetBeautifiedTestName()); + OutTestCommands.Add(FString()); +} + +bool FDaeTestAutomationPluginAutomationTestFrameworkTest::RunTest(const FString& Parameters) +{ + UE_LOG(LogDaeTestEditor, Log, TEXT("Running test for map: %s"), *MapName); + + FDaeTestAutomationPluginAutomationTestFrameworkTestContext Context; + + ADD_LATENT_AUTOMATION_COMMAND(FEditorLoadMap(MapName)); + ADD_LATENT_AUTOMATION_COMMAND(FStartPIECommand(false)); + ADD_LATENT_AUTOMATION_COMMAND(FDaeTestAutomationPluginWaitForEndOfTestSuite(Context)); + ADD_LATENT_AUTOMATION_COMMAND(FEndPlayMapCommand()); + + return true; +} + +FString FDaeTestAutomationPluginAutomationTestFrameworkTest::GetBeautifiedTestName() const +{ + return TEXT("DaedalicTestAutomationPlugin.") + FPaths::GetBaseFilename(MapName); +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkTestContext.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkTestContext.cpp new file mode 100644 index 0000000..b40cb79 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkTestContext.cpp @@ -0,0 +1,7 @@ +#include "DaeTestAutomationPluginAutomationTestFrameworkTestContext.h" + +FDaeTestAutomationPluginAutomationTestFrameworkTestContext:: + FDaeTestAutomationPluginAutomationTestFrameworkTestContext() + : CurrentTestSuite(nullptr) +{ +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/DaeTestActorBlueprintFactory.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/DaeTestActorBlueprintFactory.cpp new file mode 100644 index 0000000..b971211 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/DaeTestActorBlueprintFactory.cpp @@ -0,0 +1,70 @@ +#include "DaeTestActorBlueprintFactory.h" +#include "DaeTestActor.h" +#include "DaeTestActorBlueprint.h" +#include +#include +#include +#include +#include +#include +#include + +UDaeTestActorBlueprintFactory::UDaeTestActorBlueprintFactory( + const FObjectInitializer& ObjectInitializer /*= FObjectInitializer::Get()*/) + : Super(ObjectInitializer) +{ + bCreateNew = true; + bEditAfterNew = true; + SupportedClass = UDaeTestActorBlueprint::StaticClass(); +} + +UObject* UDaeTestActorBlueprintFactory::FactoryCreateNew(UClass* Class, UObject* InParent, + FName Name, EObjectFlags Flags, + UObject* Context, FFeedbackContext* Warn) +{ + // Create blueprint asset. + UBlueprint* Blueprint = + FKismetEditorUtilities::CreateBlueprint(ADaeTestActor::StaticClass(), InParent, Name, + BPTYPE_Normal, + UDaeTestActorBlueprint::StaticClass(), + UBlueprintGeneratedClass::StaticClass()); + + if (!IsValid(Blueprint)) + { + return nullptr; + } + + // Create special blueprint graph. + UEdGraph* EdGraph = FBlueprintEditorUtils::CreateNewGraph(Blueprint, + TEXT("Test Blueprint Graph"), + UEdGraph::StaticClass(), + UEdGraphSchema_K2::StaticClass()); + if (Blueprint->UbergraphPages.Num()) + { + FBlueprintEditorUtils::RemoveGraphs(Blueprint, Blueprint->UbergraphPages); + } + + FBlueprintEditorUtils::AddUbergraphPage(Blueprint, EdGraph); + Blueprint->LastEditedDocuments.Add(EdGraph); + EdGraph->bAllowDeletion = false; + + UBlueprintEditorSettings* Settings = GetMutableDefault(); + + if (Settings && Settings->bSpawnDefaultBlueprintNodes) + { + int32 NodePositionY = 0; + FKismetEditorUtilities::AddDefaultEventNode(Blueprint, EdGraph, + FName(TEXT("ReceiveOnAssume")), + ADaeTestActor::StaticClass(), NodePositionY); + FKismetEditorUtilities::AddDefaultEventNode(Blueprint, EdGraph, + FName(TEXT("ReceiveOnArrange")), + ADaeTestActor::StaticClass(), NodePositionY); + FKismetEditorUtilities::AddDefaultEventNode(Blueprint, EdGraph, FName(TEXT("ReceiveOnAct")), + ADaeTestActor::StaticClass(), NodePositionY); + FKismetEditorUtilities::AddDefaultEventNode(Blueprint, EdGraph, + FName(TEXT("ReceiveOnAssert")), + ADaeTestActor::StaticClass(), NodePositionY); + } + + return Blueprint; +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/DaeTestEditorLogCategory.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/DaeTestEditorLogCategory.cpp new file mode 100644 index 0000000..e324f4b --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/DaeTestEditorLogCategory.cpp @@ -0,0 +1,3 @@ +#include "DaeTestEditorLogCategory.h" + +DEFINE_LOG_CATEGORY(LogDaeTestEditor); diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/DaeTestParameterProviderActorBlueprintFactory.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/DaeTestParameterProviderActorBlueprintFactory.cpp new file mode 100644 index 0000000..5eb59b7 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/DaeTestParameterProviderActorBlueprintFactory.cpp @@ -0,0 +1,30 @@ +#include "DaeTestParameterProviderActorBlueprintFactory.h" +#include "DaeTestParameterProviderActor.h" +#include "DaeTestParameterProviderActorBlueprint.h" +#include +#include +#include +#include +#include +#include +#include + +UDaeTestParameterProviderActorBlueprintFactory::UDaeTestParameterProviderActorBlueprintFactory( + const FObjectInitializer& ObjectInitializer /*= FObjectInitializer::Get()*/) + : Super(ObjectInitializer) +{ + bCreateNew = true; + bEditAfterNew = true; + SupportedClass = UDaeTestParameterProviderActorBlueprint::StaticClass(); +} + +UObject* UDaeTestParameterProviderActorBlueprintFactory::FactoryCreateNew( + UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, + FFeedbackContext* Warn) +{ + // Create blueprint asset. + return FKismetEditorUtilities::CreateBlueprint( + ADaeTestParameterProviderActor::StaticClass(), InParent, Name, BPTYPE_Normal, + UDaeTestParameterProviderActorBlueprint::StaticClass(), + UBlueprintGeneratedClass::StaticClass()); +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/DaeTestSuiteActorBlueprintFactory.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/DaeTestSuiteActorBlueprintFactory.cpp new file mode 100644 index 0000000..add0125 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/DaeTestSuiteActorBlueprintFactory.cpp @@ -0,0 +1,76 @@ +#include "DaeTestSuiteActorBlueprintFactory.h" +#include "DaeTestSuiteActor.h" +#include "DaeTestSuiteActorBlueprint.h" +#include +#include +#include +#include +#include +#include +#include + +UDaeTestSuiteActorBlueprintFactory::UDaeTestSuiteActorBlueprintFactory( + const FObjectInitializer& ObjectInitializer /*= FObjectInitializer::Get()*/) + : Super(ObjectInitializer) +{ + bCreateNew = true; + bEditAfterNew = true; + SupportedClass = UDaeTestSuiteActorBlueprint::StaticClass(); +} + +UObject* UDaeTestSuiteActorBlueprintFactory::FactoryCreateNew(UClass* Class, UObject* InParent, + FName Name, EObjectFlags Flags, + UObject* Context, + FFeedbackContext* Warn) +{ + // Create blueprint asset. + UBlueprint* Blueprint = + FKismetEditorUtilities::CreateBlueprint(ADaeTestSuiteActor::StaticClass(), InParent, Name, + BPTYPE_Normal, + UDaeTestSuiteActorBlueprint::StaticClass(), + UBlueprintGeneratedClass::StaticClass()); + + if (!IsValid(Blueprint)) + { + return nullptr; + } + + // Create special blueprint graph. + UEdGraph* EdGraph = FBlueprintEditorUtils::CreateNewGraph(Blueprint, + TEXT("Test Suite Blueprint Graph"), + UEdGraph::StaticClass(), + UEdGraphSchema_K2::StaticClass()); + if (Blueprint->UbergraphPages.Num()) + { + FBlueprintEditorUtils::RemoveGraphs(Blueprint, Blueprint->UbergraphPages); + } + + FBlueprintEditorUtils::AddUbergraphPage(Blueprint, EdGraph); + Blueprint->LastEditedDocuments.Add(EdGraph); + EdGraph->bAllowDeletion = false; + + UBlueprintEditorSettings* Settings = GetMutableDefault(); + + if (Settings && Settings->bSpawnDefaultBlueprintNodes) + { + int32 NodePositionY = 0; + FKismetEditorUtilities::AddDefaultEventNode(Blueprint, EdGraph, + FName(TEXT("ReceiveOnBeforeAll")), + ADaeTestSuiteActor::StaticClass(), + NodePositionY); + FKismetEditorUtilities::AddDefaultEventNode(Blueprint, EdGraph, + FName(TEXT("ReceiveOnAfterAll")), + ADaeTestSuiteActor::StaticClass(), + NodePositionY); + FKismetEditorUtilities::AddDefaultEventNode(Blueprint, EdGraph, + FName(TEXT("ReceiveOnBeforeEach")), + ADaeTestSuiteActor::StaticClass(), + NodePositionY); + FKismetEditorUtilities::AddDefaultEventNode(Blueprint, EdGraph, + FName(TEXT("ReceiveOnAfterEach")), + ADaeTestSuiteActor::StaticClass(), + NodePositionY); + } + + return Blueprint; +} diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/DaedalicTestAutomationPluginEditor.cpp b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/DaedalicTestAutomationPluginEditor.cpp new file mode 100644 index 0000000..3f7b425 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Private/DaedalicTestAutomationPluginEditor.cpp @@ -0,0 +1,128 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#include "AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkIntegration.h" +#include "AssetTypeActions_DaeTestActorBlueprint.h" +#include "AssetTypeActions_DaeTestParameterProviderActorBlueprint.h" +#include "AssetTypeActions_DaeTestSuiteActorBlueprint.h" +#include "DaeTestAutomationPluginSettings.h" +#include "DaedalicTestAutomationPluginEditorClasses.h" +#include "IDaedalicTestAutomationPluginEditor.h" +#include +#include +#include +#include +#include +#include + +#define LOCTEXT_NAMESPACE "DaedalicTestAutomationPlugin" + +class FDaedalicTestAutomationPluginEditor : public IDaedalicTestAutomationPluginEditor +{ +public: + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + +private: + /** Asset catetory for test automation assets. */ + EAssetTypeCategories::Type DaedalicTestAutomationAssetCategory; + + /** Asset type actions registered by this module. */ + TArray> AssetTypeActions; + + /** Integration with the Unreal Automation Test Framework. */ + FDaeTestAutomationPluginAutomationTestFrameworkIntegration AutomationTestFrameworkIntegration; + + void RegisterAssetTypeAction(class IAssetTools& AssetTools, + TSharedRef Action); + + void OnTestMapPathChanged(const FString& NewTestMapPath); +}; + +IMPLEMENT_MODULE(FDaedalicTestAutomationPluginEditor, DaedalicTestAutomationPluginEditor) + +void FDaedalicTestAutomationPluginEditor::StartupModule() +{ + // Register asset types. + IAssetTools& AssetTools = + FModuleManager::LoadModuleChecked("AssetTools").Get(); + + DaedalicTestAutomationAssetCategory = + AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("DaedalicTestAutomationPlugin")), + NSLOCTEXT("DaedalicTestAutomationPlugin", + "DaedalicTestAutomationAssetCategory", + "Test Automation")); + + TSharedRef TestActorBlueprintAction = MakeShareable( + new FAssetTypeActions_DaeTestActorBlueprint(DaedalicTestAutomationAssetCategory)); + RegisterAssetTypeAction(AssetTools, TestActorBlueprintAction); + + TSharedRef TestSuiteActorBlueprintAction = MakeShareable( + new FAssetTypeActions_DaeTestSuiteActorBlueprint(DaedalicTestAutomationAssetCategory)); + RegisterAssetTypeAction(AssetTools, TestSuiteActorBlueprintAction); + + TSharedRef TestParameterProviderActorBlueprintAction = + MakeShareable(new FAssetTypeActions_DaeTestParameterProviderActorBlueprint( + DaedalicTestAutomationAssetCategory)); + RegisterAssetTypeAction(AssetTools, TestParameterProviderActorBlueprintAction); + + // Register settings. + if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr("Settings")) + { + UDaeTestAutomationPluginSettings* TestAutomationPluginSettings = + GetMutableDefault(); + + ISettingsSectionPtr SettingsSection = SettingsModule->RegisterSettings( + "Project", "Plugins", "DaedalicTestAutomationPlugin", + NSLOCTEXT("DaedalicTestAutomationPlugin", "DaeTestAutomationPluginSettings.DisplayName", + "Daedalic Test Automation Plugin"), + NSLOCTEXT("DaedalicTestAutomationPlugin", "DaeTestAutomationPluginSettings.Description", + "Configure the discovery of automated tests."), + TestAutomationPluginSettings); + + TestAutomationPluginSettings->OnTestMapPathChanged.AddRaw( + this, &FDaedalicTestAutomationPluginEditor::OnTestMapPathChanged); + + OnTestMapPathChanged(TestAutomationPluginSettings->TestMapPath); + } +} + +void FDaedalicTestAutomationPluginEditor::ShutdownModule() +{ + // Unregister asset types. + if (FModuleManager::Get().IsModuleLoaded("AssetTools")) + { + IAssetTools& AssetToolsModule = + FModuleManager::GetModuleChecked("AssetTools").Get(); + for (auto& AssetTypeAction : AssetTypeActions) + { + if (AssetTypeAction.IsValid()) + { + AssetToolsModule.UnregisterAssetTypeActions(AssetTypeAction.ToSharedRef()); + } + } + } + + AssetTypeActions.Empty(); + + // Unregister settings. + if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr("Settings")) + { + SettingsModule->UnregisterSettings("Editor", "Plugins", "DaedalicTestAutomationPlugin"); + } +} + +void FDaedalicTestAutomationPluginEditor::RegisterAssetTypeAction( + class IAssetTools& AssetTools, TSharedRef Action) +{ + AssetTools.RegisterAssetTypeActions(Action); + AssetTypeActions.Add(Action); +} + +void FDaedalicTestAutomationPluginEditor::OnTestMapPathChanged(const FString& NewTestMapPath) +{ + // Discover tests. + AutomationTestFrameworkIntegration.SetTestMapPath(NewTestMapPath); +} + +#undef LOCTEXT_NAMESPACE diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AssetTypeActions_DaeTestActorBlueprint.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AssetTypeActions_DaeTestActorBlueprint.h new file mode 100644 index 0000000..f7fd3bb --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AssetTypeActions_DaeTestActorBlueprint.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include + +class UFactory; + +/** Asset action for creating new test actor blueprints with special initial blueprint graphs. */ +class FAssetTypeActions_DaeTestActorBlueprint : public FAssetTypeActions_Blueprint +{ +public: + FAssetTypeActions_DaeTestActorBlueprint(EAssetTypeCategories::Type InAssetTypeCategory); + + // IAssetTypeActions Implementation + virtual FText GetName() const override; + virtual UClass* GetSupportedClass() const override; + virtual uint32 GetCategories() override; + // End IAssetTypeActions Implementation + + // FAssetTypeActions_Blueprint interface + virtual UFactory* GetFactoryForBlueprintType(UBlueprint* InBlueprint) const override; + +private: + EAssetTypeCategories::Type AssetTypeCategory; +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AssetTypeActions_DaeTestParameterProviderActorBlueprint.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AssetTypeActions_DaeTestParameterProviderActorBlueprint.h new file mode 100644 index 0000000..9675afe --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AssetTypeActions_DaeTestParameterProviderActorBlueprint.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include +#include + +class UFactory; + +/** Asset action for creating new test parameter provider actor blueprints with special initial blueprint graphs. */ +class FAssetTypeActions_DaeTestParameterProviderActorBlueprint : public FAssetTypeActions_Blueprint +{ +public: + FAssetTypeActions_DaeTestParameterProviderActorBlueprint( + EAssetTypeCategories::Type InAssetTypeCategory); + + // IAssetTypeActions Implementation + virtual FText GetName() const override; + virtual UClass* GetSupportedClass() const override; + virtual uint32 GetCategories() override; + // End IAssetTypeActions Implementation + + // FAssetTypeActions_Blueprint interface + virtual UFactory* GetFactoryForBlueprintType(UBlueprint* InBlueprint) const override; + +private: + EAssetTypeCategories::Type AssetTypeCategory; +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AssetTypeActions_DaeTestSuiteActorBlueprint.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AssetTypeActions_DaeTestSuiteActorBlueprint.h new file mode 100644 index 0000000..6d7769a --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AssetTypeActions_DaeTestSuiteActorBlueprint.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include + +class UFactory; + +/** Asset action for creating new test suite actor blueprints with special initial blueprint graphs. */ +class FAssetTypeActions_DaeTestSuiteActorBlueprint : public FAssetTypeActions_Blueprint +{ +public: + FAssetTypeActions_DaeTestSuiteActorBlueprint(EAssetTypeCategories::Type InAssetTypeCategory); + + // IAssetTypeActions Implementation + virtual FText GetName() const override; + virtual UClass* GetSupportedClass() const override; + virtual uint32 GetCategories() override; + // End IAssetTypeActions Implementation + + // FAssetTypeActions_Blueprint interface + virtual UFactory* GetFactoryForBlueprintType(UBlueprint* InBlueprint) const override; + +private: + EAssetTypeCategories::Type AssetTypeCategory; +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkCommands.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkCommands.h new file mode 100644 index 0000000..4fe6ef8 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkCommands.h @@ -0,0 +1,12 @@ +#pragma once + +#include "DaeTestAutomationPluginAutomationTestFrameworkTestContext.h" +#include +#include + +class ADaeTestSuiteActor; + +/** Waits for the current test suite to finish. */ +DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER( + FDaeTestAutomationPluginWaitForEndOfTestSuite, + FDaeTestAutomationPluginAutomationTestFrameworkTestContext, Context); diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkIntegration.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkIntegration.h new file mode 100644 index 0000000..8b0dac6 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkIntegration.h @@ -0,0 +1,19 @@ +#pragma once + +#include "DaeTestAutomationPluginAutomationTestFrameworkTest.h" +#include + +/** Integration with the Unreal Automation Test Framework. Discovers tests based on the plugin settings. */ +class FDaeTestAutomationPluginAutomationTestFrameworkIntegration +{ +public: + /** Sets the path to look for test maps in, re-discovering tests afterwards. */ + void SetTestMapPath(const FString& InTestMapPath); + +private: + /** Path to look for test maps in. */ + FString TestMapPath; + + /** Currently registered automation tests. */ + TArray> Tests; +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkTest.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkTest.h new file mode 100644 index 0000000..3b9b5e3 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkTest.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +/** Single test to be registered with the Unreal Automation Test Framework. Implementation based on IMPLEMENT_SIMPLE_AUTOMATION_TEST_PRIVATE macro. */ +class FDaeTestAutomationPluginAutomationTestFrameworkTest : FAutomationTestBase +{ +public: + FDaeTestAutomationPluginAutomationTestFrameworkTest(const FString& InMapName); + + virtual uint32 GetTestFlags() const override; + virtual uint32 GetRequiredDeviceNum() const override; + virtual FString GetTestSourceFileName() const override; + virtual int32 GetTestSourceFileLine() const override; + + FString GetMapName() const; + +protected: + virtual void GetTests(TArray& OutBeautifiedNames, + TArray& OutTestCommands) const override; + virtual bool RunTest(const FString& Parameters) override; + virtual FString GetBeautifiedTestName() const override; + +private: + FString MapName; +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkTestContext.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkTestContext.h new file mode 100644 index 0000000..e61580a --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/AutomationTestFramework/DaeTestAutomationPluginAutomationTestFrameworkTestContext.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +class ADaeTestSuiteActor; + +/** Context to run a single test for the Unreal Automation Test Framework within. */ +class FDaeTestAutomationPluginAutomationTestFrameworkTestContext +{ +public: + ADaeTestSuiteActor* CurrentTestSuite; + + FDaeTestAutomationPluginAutomationTestFrameworkTestContext(); +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/DaeTestActorBlueprintFactory.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/DaeTestActorBlueprintFactory.h new file mode 100644 index 0000000..2e8366b --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/DaeTestActorBlueprintFactory.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include "DaeTestActorBlueprintFactory.generated.h" + +/** Factory for creating new test actor blueprints with special initial blueprint graphs. */ +UCLASS(HideCategories = Object, MinimalAPI) +class UDaeTestActorBlueprintFactory : public UFactory +{ + GENERATED_BODY() + +public: + UDaeTestActorBlueprintFactory( + const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + //~ Begin UFactory Interface + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, + EObjectFlags Flags, UObject* Context, + FFeedbackContext* Warn) override; + //~ Begin UFactory Interface +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/DaeTestEditorLogCategory.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/DaeTestEditorLogCategory.h new file mode 100644 index 0000000..14e8400 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/DaeTestEditorLogCategory.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +DECLARE_LOG_CATEGORY_EXTERN(LogDaeTestEditor, Log, All); diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/DaeTestParameterProviderActorBlueprintFactory.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/DaeTestParameterProviderActorBlueprintFactory.h new file mode 100644 index 0000000..93c9fd4 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/DaeTestParameterProviderActorBlueprintFactory.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include "DaeTestParameterProviderActorBlueprintFactory.generated.h" + +/** Factory for creating new test parameter provider actor blueprints with special initial blueprint graphs. */ +UCLASS(HideCategories = Object, MinimalAPI) +class UDaeTestParameterProviderActorBlueprintFactory : public UFactory +{ + GENERATED_BODY() + +public: + UDaeTestParameterProviderActorBlueprintFactory( + const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + //~ Begin UFactory Interface + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, + EObjectFlags Flags, UObject* Context, + FFeedbackContext* Warn) override; + //~ Begin UFactory Interface +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/DaeTestSuiteActorBlueprintFactory.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/DaeTestSuiteActorBlueprintFactory.h new file mode 100644 index 0000000..3e212eb --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/DaeTestSuiteActorBlueprintFactory.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include "DaeTestSuiteActorBlueprintFactory.generated.h" + +/** Factory for creating new test suite actor blueprints with special initial blueprint graphs. */ +UCLASS(HideCategories = Object, MinimalAPI) +class UDaeTestSuiteActorBlueprintFactory : public UFactory +{ + GENERATED_BODY() + +public: + UDaeTestSuiteActorBlueprintFactory( + const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + //~ Begin UFactory Interface + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, + EObjectFlags Flags, UObject* Context, + FFeedbackContext* Warn) override; + //~ Begin UFactory Interface +}; diff --git a/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/IDaedalicTestAutomationPluginEditor.h b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/IDaedalicTestAutomationPluginEditor.h new file mode 100644 index 0000000..f99efa9 --- /dev/null +++ b/DaedalicTestAutomationPlugin/Source/DaedalicTestAutomationPluginEditor/Public/IDaedalicTestAutomationPluginEditor.h @@ -0,0 +1,37 @@ +// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "Modules/ModuleInterface.h" +#include "Modules/ModuleManager.h" +#include "CoreMinimal.h" + +/** + * The public interface to this module. In most cases, this interface is only public to sibling modules + * within this plugin. + */ +class IDaedalicTestAutomationPluginEditor : public IModuleInterface +{ +public: + /** + * Singleton-like access to this module's interface. This is just for convenience! + * Beware of calling this during the shutdown phase, though. Your module might have been unloaded already. + * + * @return Returns singleton instance, loading the module on demand if needed + */ + static inline IDaedalicTestAutomationPluginEditor& Get() + { + return FModuleManager::LoadModuleChecked( + "DaedalicTestAutomationPluginEditor"); + } + + /** + * Checks to see if this module is loaded and ready. It is only valid to call Get() if IsAvailable() returns true. + * + * @return True if the module is loaded and ready to use + */ + static inline bool IsAvailable() + { + return FModuleManager::Get().IsModuleLoaded("DaedalicTestAutomationPluginEditor"); + } +}; diff --git a/Documentation/AssertCompareByte.png b/Documentation/AssertCompareByte.png new file mode 100644 index 0000000..370932a Binary files /dev/null and b/Documentation/AssertCompareByte.png differ diff --git a/Documentation/AssertCompareFloat.png b/Documentation/AssertCompareFloat.png new file mode 100644 index 0000000..5f58705 Binary files /dev/null and b/Documentation/AssertCompareFloat.png differ diff --git a/Documentation/AssertCompareInteger.png b/Documentation/AssertCompareInteger.png new file mode 100644 index 0000000..779e945 Binary files /dev/null and b/Documentation/AssertCompareInteger.png differ diff --git a/Documentation/AssertCompareInteger64.png b/Documentation/AssertCompareInteger64.png new file mode 100644 index 0000000..3e92fb5 Binary files /dev/null and b/Documentation/AssertCompareInteger64.png differ diff --git a/Documentation/AssertEqualByte.png b/Documentation/AssertEqualByte.png new file mode 100644 index 0000000..1206ef3 Binary files /dev/null and b/Documentation/AssertEqualByte.png differ diff --git a/Documentation/AssertEqualFloat.png b/Documentation/AssertEqualFloat.png new file mode 100644 index 0000000..d6d9662 Binary files /dev/null and b/Documentation/AssertEqualFloat.png differ diff --git a/Documentation/AssertEqualInteger.png b/Documentation/AssertEqualInteger.png new file mode 100644 index 0000000..5952d8f Binary files /dev/null and b/Documentation/AssertEqualInteger.png differ diff --git a/Documentation/AssertEqualInteger64.png b/Documentation/AssertEqualInteger64.png new file mode 100644 index 0000000..323bf6f Binary files /dev/null and b/Documentation/AssertEqualInteger64.png differ diff --git a/Documentation/AssertEqualName.png b/Documentation/AssertEqualName.png new file mode 100644 index 0000000..7708bcf Binary files /dev/null and b/Documentation/AssertEqualName.png differ diff --git a/Documentation/AssertEqualRotator.png b/Documentation/AssertEqualRotator.png new file mode 100644 index 0000000..4f2e3e5 Binary files /dev/null and b/Documentation/AssertEqualRotator.png differ diff --git a/Documentation/AssertEqualString.png b/Documentation/AssertEqualString.png new file mode 100644 index 0000000..7588044 Binary files /dev/null and b/Documentation/AssertEqualString.png differ diff --git a/Documentation/AssertEqualText.png b/Documentation/AssertEqualText.png new file mode 100644 index 0000000..b32a1b1 Binary files /dev/null and b/Documentation/AssertEqualText.png differ diff --git a/Documentation/AssertEqualTransform.png b/Documentation/AssertEqualTransform.png new file mode 100644 index 0000000..498a701 Binary files /dev/null and b/Documentation/AssertEqualTransform.png differ diff --git a/Documentation/AssertEqualVector.png b/Documentation/AssertEqualVector.png new file mode 100644 index 0000000..b1295d1 Binary files /dev/null and b/Documentation/AssertEqualVector.png differ diff --git a/Documentation/AssertFail.png b/Documentation/AssertFail.png new file mode 100644 index 0000000..63e4c5c Binary files /dev/null and b/Documentation/AssertFail.png differ diff --git a/Documentation/AssertFalse.png b/Documentation/AssertFalse.png new file mode 100644 index 0000000..d31f802 Binary files /dev/null and b/Documentation/AssertFalse.png differ diff --git a/Documentation/AssertImageIsSet.png b/Documentation/AssertImageIsSet.png new file mode 100644 index 0000000..f6ada10 Binary files /dev/null and b/Documentation/AssertImageIsSet.png differ diff --git a/Documentation/AssertInRangeByte.png b/Documentation/AssertInRangeByte.png new file mode 100644 index 0000000..06b1350 Binary files /dev/null and b/Documentation/AssertInRangeByte.png differ diff --git a/Documentation/AssertInRangeFloat.png b/Documentation/AssertInRangeFloat.png new file mode 100644 index 0000000..9d79d32 Binary files /dev/null and b/Documentation/AssertInRangeFloat.png differ diff --git a/Documentation/AssertInRangeInteger.png b/Documentation/AssertInRangeInteger.png new file mode 100644 index 0000000..0802cf0 Binary files /dev/null and b/Documentation/AssertInRangeInteger.png differ diff --git a/Documentation/AssertInRangeInteger64.png b/Documentation/AssertInRangeInteger64.png new file mode 100644 index 0000000..50657ea Binary files /dev/null and b/Documentation/AssertInRangeInteger64.png differ diff --git a/Documentation/AssertInvalid.png b/Documentation/AssertInvalid.png new file mode 100644 index 0000000..405c799 Binary files /dev/null and b/Documentation/AssertInvalid.png differ diff --git a/Documentation/AssertNotEqualByte.png b/Documentation/AssertNotEqualByte.png new file mode 100644 index 0000000..d390e34 Binary files /dev/null and b/Documentation/AssertNotEqualByte.png differ diff --git a/Documentation/AssertNotEqualFloat.png b/Documentation/AssertNotEqualFloat.png new file mode 100644 index 0000000..5135f37 Binary files /dev/null and b/Documentation/AssertNotEqualFloat.png differ diff --git a/Documentation/AssertNotEqualInteger.png b/Documentation/AssertNotEqualInteger.png new file mode 100644 index 0000000..48c76df Binary files /dev/null and b/Documentation/AssertNotEqualInteger.png differ diff --git a/Documentation/AssertNotEqualInteger64.png b/Documentation/AssertNotEqualInteger64.png new file mode 100644 index 0000000..6d58489 Binary files /dev/null and b/Documentation/AssertNotEqualInteger64.png differ diff --git a/Documentation/AssertNotEqualName.png b/Documentation/AssertNotEqualName.png new file mode 100644 index 0000000..308fca0 Binary files /dev/null and b/Documentation/AssertNotEqualName.png differ diff --git a/Documentation/AssertNotEqualRotator.png b/Documentation/AssertNotEqualRotator.png new file mode 100644 index 0000000..2aec389 Binary files /dev/null and b/Documentation/AssertNotEqualRotator.png differ diff --git a/Documentation/AssertNotEqualString.png b/Documentation/AssertNotEqualString.png new file mode 100644 index 0000000..79764f6 Binary files /dev/null and b/Documentation/AssertNotEqualString.png differ diff --git a/Documentation/AssertNotEqualText.png b/Documentation/AssertNotEqualText.png new file mode 100644 index 0000000..e23c8cb Binary files /dev/null and b/Documentation/AssertNotEqualText.png differ diff --git a/Documentation/AssertNotEqualTransform.png b/Documentation/AssertNotEqualTransform.png new file mode 100644 index 0000000..3032b29 Binary files /dev/null and b/Documentation/AssertNotEqualTransform.png differ diff --git a/Documentation/AssertNotEqualVector.png b/Documentation/AssertNotEqualVector.png new file mode 100644 index 0000000..1648411 Binary files /dev/null and b/Documentation/AssertNotEqualVector.png differ diff --git a/Documentation/AssertNotInRangeByte.png b/Documentation/AssertNotInRangeByte.png new file mode 100644 index 0000000..48a6930 Binary files /dev/null and b/Documentation/AssertNotInRangeByte.png differ diff --git a/Documentation/AssertNotInRangeFloat.png b/Documentation/AssertNotInRangeFloat.png new file mode 100644 index 0000000..c1a1bc9 Binary files /dev/null and b/Documentation/AssertNotInRangeFloat.png differ diff --git a/Documentation/AssertNotInRangeInteger.png b/Documentation/AssertNotInRangeInteger.png new file mode 100644 index 0000000..19a16d5 Binary files /dev/null and b/Documentation/AssertNotInRangeInteger.png differ diff --git a/Documentation/AssertNotInRangeInteger64.png b/Documentation/AssertNotInRangeInteger64.png new file mode 100644 index 0000000..defcc9b Binary files /dev/null and b/Documentation/AssertNotInRangeInteger64.png differ diff --git a/Documentation/AssertRichTextIsSet.png b/Documentation/AssertRichTextIsSet.png new file mode 100644 index 0000000..b37ecee Binary files /dev/null and b/Documentation/AssertRichTextIsSet.png differ diff --git a/Documentation/AssertTextIsSet.png b/Documentation/AssertTextIsSet.png new file mode 100644 index 0000000..0e29c0d Binary files /dev/null and b/Documentation/AssertTextIsSet.png differ diff --git a/Documentation/AssertTrue.png b/Documentation/AssertTrue.png new file mode 100644 index 0000000..04b15cf Binary files /dev/null and b/Documentation/AssertTrue.png differ diff --git a/Documentation/AssertValid.png b/Documentation/AssertValid.png new file mode 100644 index 0000000..adbaea1 Binary files /dev/null and b/Documentation/AssertValid.png differ diff --git a/Documentation/AssertWasNotTriggered.png b/Documentation/AssertWasNotTriggered.png new file mode 100644 index 0000000..e897ae4 Binary files /dev/null and b/Documentation/AssertWasNotTriggered.png differ diff --git a/Documentation/AssertWasTriggered.png b/Documentation/AssertWasTriggered.png new file mode 100644 index 0000000..d4ebe5e Binary files /dev/null and b/Documentation/AssertWasTriggered.png differ diff --git a/Documentation/AssertWidgetIsVisible.png b/Documentation/AssertWidgetIsVisible.png new file mode 100644 index 0000000..c5e291b Binary files /dev/null and b/Documentation/AssertWidgetIsVisible.png differ diff --git a/Documentation/Assertions.md b/Documentation/Assertions.md new file mode 100644 index 0000000..091522f --- /dev/null +++ b/Documentation/Assertions.md @@ -0,0 +1,71 @@ +# Assertions + +Daedalic Test Automation Plugin comes with the following assertion nodes for use in your automated tests: + +## Basic Assertions + +| Node | Description | +| --- | --- | +| ![Assert Fail](AssertFail.png) | Finishes the current test as failure. | +| ![Assert True](AssertTrue.png) | Expects the specified value to be true. | +| ![Assert False](AssertFalse.png) | Expects the specified value to be false. | +| ![Assert Valid](AssertValid.png) | Expects the specified object to be valid. | +| ![Assert Invalid](AssertInvalid.png) | Expects the specified object not to be valid. | +| ![Assert Was Triggered](AssertWasTriggered.png) | Expects the specified trigger box to be triggered. | +| ![Assert Was Not Triggered](AssertWasNotTriggered.png) | Expects the specified trigger box not to be triggered. | + +## Equality + +| Node | Description | +| --- | --- | +| ![Assert Equal (Byte)](AssertEqualByte.png) | Expects the specified bytes to be equal. | +| ![Assert Equal (Integer)](AssertEqualInteger.png) | Expects the specified 32-bit integers to be equal. | +| ![Assert Equal (Integer64)](AssertEqualInteger64.png) | Expects the specified 64-bit integers to be equal. | +| ![Assert Equal (Float)](AssertEqualFloat.png) | Expects the specified floats to be (nearly) equal. | +| ![Assert Equal (Name)](AssertEqualName.png) | Expects the specified names to be equal. | +| ![Assert Equal (String)](AssertEqualString.png) | Expects the specified strings to be equal. | +| ![Assert Equal (Text)](AssertEqualText.png) | Expects the specified texts to be equal. | +| ![Assert Equal (Vector)](AssertEqualVector.png) | Expects the specified vectors to be (nearly) equal. | +| ![Assert Equal (Rotator)](AssertEqualRotator.png) | Expects the specified rotators to be (nearly) equal. | +| ![Assert Equal (Transform)](AssertEqualTransform.png) | Expects the specified transforms to be (nearly) equal. | + +## Inequality + +| Node | Description | +| --- | --- | +| ![Assert Not Equal (Byte)](AssertNotEqualByte.png) | Expects the specified bytes not to be equal. | +| ![Assert Compare (Byte)](AssertCompareByte.png) | Compares the specified bytes for order. | +| ![Assert Not Equal (Integer)](AssertNotEqualInteger.png) | Expects the specified 32-bit integers not to be equal. | +| ![Assert Compare (Integer)](AssertCompareInteger.png) | Compares the specified 32-bit integers for order. | +| ![Assert Not Equal (Integer64)](AssertNotEqualInteger64.png) | Expects the specified 64-bit integers not to be equal. | +| ![Assert Compare (Integer64)](AssertCompareInteger64.png) | Compares the specified 64-bit integers for order. | +| ![Assert Not Equal (Float)](AssertNotEqualFloat.png) | Expects the specified floats not to be equal. | +| ![Assert Compare (Float)](AssertCompareFloat.png) | Compares the specified floats for order. | +| ![Assert Not Equal (Name)](AssertNotEqualName.png) | Expects the specified names not to be equal. | +| ![Assert Not Equal (String)](AssertNotEqualString.png) | Expects the specified strings not to be equal. | +| ![Assert Not Equal (Text)](AssertNotEqualText.png) | Expects the specified texts not to be equal. | +| ![Assert Not Equal (Vector)](AssertNotEqualVector.png) | Expects the specified vectors not to be equal. | +| ![Assert Not Equal (Rotator)](AssertNotEqualRotator.png) | Expects the specified rotators not to be equal. | +| ![Assert Not Equal (Transform)](AssertNotEqualTransform.png) | Expects the specified transforms not to be equal. | + +## Range Checks + +| Node | Description | +| --- | --- | +| ![Assert In Range (Byte)](AssertInRangeByte.png) | Expects Value to be between MinInclusive and MaxInclusive. | +| ![Assert In Range (Integer)](AssertInRangeInteger.png) | Expects Value to be between MinInclusive and MaxInclusive. | +| ![Assert In Range (Integer64)](AssertInRangeInteger64.png) | Expects Value to be between MinInclusive and MaxInclusive. | +| ![Assert In Range (Float)](AssertInRangeFloat.png) | Expects Value to be between MinInclusive and MaxInclusive. | +| ![Assert Not In Range (Byte)](AssertNotInRangeByte.png) | Expects Value not to be between MinInclusive and MaxInclusive. | +| ![Assert Not In Range (Integer)](AssertNotInRangeInteger.png) | Expects Value not to be between MinInclusive and MaxInclusive. | +| ![Assert Not In Range (Integer64)](AssertNotInRangeInteger64.png) | Expects Value not to be between MinInclusive and MaxInclusive. | +| ![Assert Not In Range (Float)](AssertNotInRangeFloat.png) | Expects Value not to be between MinInclusive and MaxInclusive. | + +## UMG Widgets + +| Node | Description | +| --- | --- | +| ![Assert Widget Is Visible](AssertWidgetIsVisible.png) | Expects the specified widget to be valid and visible (e.g. added to viewport, not hidden or collapsed). | +| ![Assert Text Is Set](AssertTextIsSet.png) | Expects the specified text not to be empty. | +| ![Assert Rich Text Is Set](AssertRichTextIsSet.png) | Expects the specified rich text not to be empty. | +| ![Assert Image Is Set](AssertImageIsSet.png) | Expects the specified image to be set up to use a texture or material. | diff --git a/Documentation/Assumption.png b/Documentation/Assumption.png new file mode 100644 index 0000000..641926e Binary files /dev/null and b/Documentation/Assumption.png differ diff --git a/Documentation/AutomationWindow.png b/Documentation/AutomationWindow.png new file mode 100644 index 0000000..c71cc80 Binary files /dev/null and b/Documentation/AutomationWindow.png differ diff --git a/Documentation/CreatingTests.png b/Documentation/CreatingTests.png new file mode 100644 index 0000000..b2a3e54 Binary files /dev/null and b/Documentation/CreatingTests.png differ diff --git a/Documentation/CreatingTests.vsdx b/Documentation/CreatingTests.vsdx new file mode 100644 index 0000000..e637428 Binary files /dev/null and b/Documentation/CreatingTests.vsdx differ diff --git a/Documentation/DelayFrames.png b/Documentation/DelayFrames.png new file mode 100644 index 0000000..fc5e155 Binary files /dev/null and b/Documentation/DelayFrames.png differ diff --git a/Documentation/DelayUntil.png b/Documentation/DelayUntil.png new file mode 100644 index 0000000..1758b3f Binary files /dev/null and b/Documentation/DelayUntil.png differ diff --git a/Documentation/DelayUntilInvalid.png b/Documentation/DelayUntilInvalid.png new file mode 100644 index 0000000..b1ff35b Binary files /dev/null and b/Documentation/DelayUntilInvalid.png differ diff --git a/Documentation/DelayUntilTriggered.png b/Documentation/DelayUntilTriggered.png new file mode 100644 index 0000000..d5b9743 Binary files /dev/null and b/Documentation/DelayUntilTriggered.png differ diff --git a/Documentation/DelayUntilValid.png b/Documentation/DelayUntilValid.png new file mode 100644 index 0000000..863c19f Binary files /dev/null and b/Documentation/DelayUntilValid.png differ diff --git a/Documentation/DelayUntilWidgetVisible.png b/Documentation/DelayUntilWidgetVisible.png new file mode 100644 index 0000000..763f955 Binary files /dev/null and b/Documentation/DelayUntilWidgetVisible.png differ diff --git a/Documentation/Delays.md b/Documentation/Delays.md new file mode 100644 index 0000000..c4e67a9 --- /dev/null +++ b/Documentation/Delays.md @@ -0,0 +1,12 @@ +# Delays + +Daedalic Test Automation Plugin comes with the following additional delay nodes that you might find useful when building your tests: + +| Node | Description | +| --- | --- | +| ![Delay Until](DelayUntil.png) | Triggers the output link after the specified condition becomes true. | +| ![Delay Until Valid](DelayUntilValid.png) | Triggers the output link after the specified object becomes valid. | +| ![Delay Until Invalid](DelayUntilInvalid.png) | Triggers the output link after the specified object becomes invalid. | +| ![Delay Until Triggered](DelayUntilTriggered.png) | Triggers the output link after the specified trigger box has been triggered. | +| ![Delay Until Widget Visible](DelayUntilWidgetVisible.png) | Triggers the output link after the specified widget becomes valid and visible (e.g. added to viewport, not hidden or collapsed). | +| ![Delay Frames](DelayFrames.png) | Triggers the output link after the specified number of frames. | diff --git a/Documentation/JUnitReport.png b/Documentation/JUnitReport.png new file mode 100644 index 0000000..d9a3e4f Binary files /dev/null and b/Documentation/JUnitReport.png differ diff --git a/Documentation/ParameterProviderImplementation01.png b/Documentation/ParameterProviderImplementation01.png new file mode 100644 index 0000000..36f4fc7 Binary files /dev/null and b/Documentation/ParameterProviderImplementation01.png differ diff --git a/Documentation/ParameterProviderImplementation02.png b/Documentation/ParameterProviderImplementation02.png new file mode 100644 index 0000000..f7083b7 Binary files /dev/null and b/Documentation/ParameterProviderImplementation02.png differ diff --git a/Documentation/ParameterProviders.png b/Documentation/ParameterProviders.png new file mode 100644 index 0000000..14577b7 Binary files /dev/null and b/Documentation/ParameterProviders.png differ diff --git a/Documentation/ParameterizedTest.png b/Documentation/ParameterizedTest.png new file mode 100644 index 0000000..11e0f86 Binary files /dev/null and b/Documentation/ParameterizedTest.png differ diff --git a/Documentation/RunningTests.png b/Documentation/RunningTests.png new file mode 100644 index 0000000..961887f Binary files /dev/null and b/Documentation/RunningTests.png differ diff --git a/Documentation/RunningTests.vsdx b/Documentation/RunningTests.vsdx new file mode 100644 index 0000000..bda3c8d Binary files /dev/null and b/Documentation/RunningTests.vsdx differ diff --git a/Documentation/SimpleTest.png b/Documentation/SimpleTest.png new file mode 100644 index 0000000..fe8a350 Binary files /dev/null and b/Documentation/SimpleTest.png differ diff --git a/Documentation/SimulatingInput.png b/Documentation/SimulatingInput.png new file mode 100644 index 0000000..c61b388 Binary files /dev/null and b/Documentation/SimulatingInput.png differ diff --git a/Documentation/SkipReason.png b/Documentation/SkipReason.png new file mode 100644 index 0000000..9b1fdc9 Binary files /dev/null and b/Documentation/SkipReason.png differ diff --git a/Documentation/TestParameter.png b/Documentation/TestParameter.png new file mode 100644 index 0000000..79774aa Binary files /dev/null and b/Documentation/TestParameter.png differ diff --git a/Documentation/TestTimeouts.png b/Documentation/TestTimeouts.png new file mode 100644 index 0000000..88dd1d1 Binary files /dev/null and b/Documentation/TestTimeouts.png differ diff --git a/README.md b/README.md index 53e54be..3a755da 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,366 @@ -# ue4-test-automation -Facilitates setting up integration test suits with Unreal Engine 4 Gauntlet. +# Daedalic Test Automation Plugin + +[![license](https://img.shields.io/github/license/DaedalicEntertainment/ue4-test-automation.svg?maxAge=2592000)](https://github.com/DaedalicEntertainment/ue4-test-automation/blob/develop/LICENSE) + +The _Daedalic Test Automation Plugin_ facilitates creating and running integration tests with the [Gauntlet Automation Framework](https://docs.unrealengine.com/en-US/Programming/Automation/Gauntlet/index.html) of [Unreal Engine 4](https://www.unrealengine.com). + +Automated testing, when applied instead of or in addition to manual testing, provides multiple benefits: + +* _Clean code._ Automating tests requires the tested code to be very modular, which improves maintainability in general. +* _Fast results._ Running automated tests is usually a lot faster than testing manually, and can be performed unattended. +* _Better coverage._ As a result, we can test more parts of our game when using automated testing than without, allowing manual testers to focus on other parts of the game. +* _Increased stability_. Being able to run automated tests greatly increases the confidence of the development team when applying bigger changes to the code base, allowing them to catch regressions early (e.g. after optimizing performance). + +After using the plugin for automating tests of _The Lord of the Rings™: Gollum™_, we decided to share it with the rest of the world. We feel like software testing is far too important not to be supported by automation, and test automation still hasn't fully found its way into game development. We believe that this is party because creating automated tests for games tends to be tedious, and we want to improve on that. + + +## Contents + +1. [Setup](#setup) + 1. [Prerequisites](#prerequisites) + 1. [Adding The Plugin](#adding-the-plugin) +1. [Creating Tests](#creating-tests) + 1. [Assertions](#assertions) + 1. [Delays](#delays) + 1. [Simulating Input](#simulating-input) + 1. [Test Trigger Boxes](#test-trigger-boxes) + 1. [Test Timeouts](#test-timeouts) + 1. [Test Suite Lifecycle](#test-suite-lifecycle) + 1. [Parameterized Tests](#parameterized-tests) + 1. [Skipping Tests](#skipping-tests) + 1. [Assumptions](#assumptions) +1. [Running Tests](#running-tests) + 1. [Play In Editor](#play-in-editor) + 1. [Automation Window](#automation-window) + 1. [Gauntlet](#gauntlet) +1. [Best Practices](#best-practices) +1. [Bugs, Questions & Feature Requests](#bugs-questions--feature-requests) +1. [Development Cycle](#development-cycle) +1. [Contributing](#contributing) +1. [Future Work](#future-work) +1. [License](#license) +1. [References](#references) + + +## Setup + +### Prerequisites + +Daedalic Test Automation Plugin currently supports the following Unreal Engine Versions: + +* 4.23 + +Note that you currently have to compile the plugin yourself, and thus need a C++ Unreal project. + +If you want to use [Gauntlet](#gauntlet) for running your tests, you'll need a source version of Unreal Engine as well. + +### Adding The Plugin + +1. Clone the repository. +1. Close the Unreal Editor. +1. Copy the `DaedalicTestAutomationPlugin` folder to the `Plugins` folder next to your `.uproject` file. +1. Copy the `DaedalicTestAutomationPlugin.Automation` folder to the `Build/Scripts` folder next to your ```.uproject``` file. +1. Start the Unreal Editor. +1. Enable the plugin in Edit > Plugins > Daedalic Entertainment. +1. Close the Unreal Editor. +1. Right-click your `.uproject` file and select _Generate Visual Studio project files_. +1. Build the resulting solution in Visual Studio. +1. Start the Unreal Editor. + +## Creating Tests + +Daedalic Test Automation Plugin is fully exposed to blueprints in order to allow everyone to easily create tests. Each level represents a _test suite_, which in turn can consist of multiple _tests_. + +You'll be using Gauntlet to run one or more test suits, or the Unreal Editor to run a single test suite. + +![Creating Tests](Documentation/CreatingTests.png) + +In order to create a new test suite with a single test: + +1. Create a new level. +1. Add a _Dae Test Suite Actor_ to the level. +1. Create a _Dae Test Actor_ blueprint (e.g. through right-click in Content Browser > Create Advanced Asset > Test Automation > Test Actor Blueprint). +1. Implement the Arrange, Act and Assert events of the test actor (see below). +1. Add an instance of the test actor blueprint to the level. +1. Add the test actor reference to the list of tests of the test suite actor. + +Automated tests in the Daedalic Test Automation Plugin are built with the Arrange-Act-Assert pattern in mind: + +* In _Arrange_, you should set up your test environment, get references to required actors and components, and prepare everything for the actual test. +* In _Act_, you should perform the actual action to test. Here, you're allowed to use latent actions, such as delays, to test what you want to test. Because we don't know when you're finished, you have to call _Finish Act_ when you're done. +* In _Assert_, you should use the built-in assertion framework to verify the results of your tests, e.g. check the state of variables or positions of actors. + +If any of the assertions performed in the Assert step fail, the test will be marked as failed. + +![Simple Test Blueprint](Documentation/SimpleTest.png) + +You can verify your test suite by entering PIE and filtering your log by the `LogDaeTest` log category. + +``` +LogDaeTest: Display: ADaeTestSuiteActor::RunAllTests - Test Suite: DaeTestSuiteActor_1 +LogDaeTest: Display: ADaeTestSuiteActor::RunNextTest - Test: BP_TestCalculatorAddsNumbers_2 +LogDaeTest: Display: ADaeTestSuiteActor::OnTestSuccessful - Test: BP_TestCalculatorAddsNumbers_2 +LogDaeTest: Display: ADaeTestSuiteActor::RunNextTest - All tests finished. +``` + +You'll also find a handful of example tests in the Content folder of the plugin. + +### Assertions + +There's a whole lot of _assertion_ nodes for use in your automated tests, including equality and range checks for all basic blueprint types, or verifying the state of UMG widgets. Take a look at [Documentation/Assertions.md](Documentation/Assertions.md) for more details. + +### Delays + +Daedalic Test Automation Plugin comes with additional _delay_ nodes that you might find useful when building your tests. Take a look at [Documentation/Delays.md](Documentation/Delays.md) for more details. + +### Simulating Input + +We provide blueprint nodes for simulating _player input_, both actions and axes. Actions will be applied once immediately, while axes will be applied until explicitly reset by applying it again. You can use all actions and axes defined in your input mappings (Edit > Project Settings > Engine > Input). + +Simulated input is especially helpful when combined with [Delays](#delays): + +![Simulating Input](Documentation/SimulatingInput.png) + +### Test Trigger Boxes + +Daedalic Test Automation Plugin ships with a convenience _Dae Test Trigger Box_ that allows you to set up test delays and assertions more quickly. These trigger boxes will just set a flag when triggered, and write a log. Our built-in [delays](Documentation/Delays.md) and [assertions](Documentation/Assertions.md) will use that flag to check if the box has been triggered. + +### Test Timeouts + +At your _Dae Gauntlet Test Actor_ blueprint (or instance), you can specify a _timeout_ for the test (defaults to 30 seconds). + +![Test Timeouts](Documentation/TestTimeouts.png) + +If your test times out during the Act stage, we'll execute all assertions immediately instead of waiting for the Act stage to finish. This allows your test to finish with just warnings instead of errors, in case you just set up some wrong delays, for instance. However, if your assertions actually fail after the timeout, the test will be marked as failed as usual. + +``` +LogDaeTest: Display: ADaeTestSuiteActor::RunAllTests - Test Suite: DaeTestSuiteActor_1 +LogDaeTest: Display: ADaeTestSuiteActor::RunNextTest - Test: BP_TestMoveForward_2 +LogDaeTest: Warning: Timed out after 5.000000 seconds +LogDaeTest: Error: Assertion failed - Trigger box DaeTestTriggerBox_1 wasn't triggered +LogDaeTest: Error: ADaeTestSuiteActor::OnTestFailed - Test: BP_TestMoveForward_2, FailureMessage: Assertion failed - Trigger box DaeTestTriggerBox_1 wasn't triggered +LogDaeTest: Display: ADaeTestSuiteActor::RunNextTest - All tests finished. +``` + +### Test Suite Lifecycle + +Instead of just adding a default _Dae Test Suite Actor_ to your level, you can create a test suite blueprint instead (e.g. through right-click in Content Browser > Create Advanced Asset > Test Automation > Test Suite Actor Blueprint). + +Test suite blueprints allow you to implement the following _lifecycle events_: + +* `BeforeAll`: Executed before running the first test. +* `BeforeEach`: Executed every time before running a test. +* `AfterEach`: Executed every time after running a test. +* `AfterAll`: Executed after running the last test. + +After creating your test suite blueprint, you can add instances of that blueprint to your test levels just as you would with the default test suite actor. Then, add test actor references to the list of tests of your test suite as usual. + +### Parameterized Tests + +In case you want to run the same test multiple times with just slightly different configurations, Daedalic Test Automation Plugin offers _parameterized tests_. You can specify any number of parameters for your test instance (or blueprint). + +In order to provide a consistent test API, these parameters have to be of type UObject, so if you have any other type you want to pass in as parameter, you'll need to wrap them with an UObject. Using UObject parameters also enables you to reference other actors in your test level. We're using soft references to the parameter objects, enabling you to reference actors from other streaming levels as well. + +![Parameterized Test](Documentation/ParameterizedTest.png) + +The parameter references will be resolved and passed to all test events, where you can perform your test actions on them: + +![Test Parameter](Documentation/TestParameter.png) + +The test will be run once for each parameter, and each test run will be treated exactly as if you'd run a non-parameterized test: + +* the test time is reset for each parameter +* BeforeEach and AfterEach are called for each parameter +* test reports include one test per parameter + +``` +LogDaeTest: Display: ADaeTestSuiteActor::RunAllTests - Test Suite: DaeTestSuiteActor_1 +LogDaeTest: Display: ADaeTestSuiteActor::RunNextTest - Test: BP_TestParameterized_TwoParameters - BP_TestParameter1 +LogBlueprintUserMessages: [BP_TestParameterized_TwoParameters] BP_TestParameter1 +LogDaeTest: Display: ADaeTestSuiteActor::OnTestSuccessful - Test: BP_TestParameterized_TwoParameters - BP_TestParameter1 +LogDaeTest: Display: ADaeTestSuiteActor::RunNextTest - Test: BP_TestParameterized_TwoParameters - BP_TestParameter2 +LogBlueprintUserMessages: [BP_TestParameterized_TwoParameters] BP_TestParameter2 +LogDaeTest: Display: ADaeTestSuiteActor::OnTestSuccessful - Test: BP_TestParameterized_TwoParameters - BP_TestParameter2 +LogDaeTest: Display: ADaeTestSuiteActor::RunNextTest - All tests finished. +``` + +Sometimes, you'll want to provide the test parameters dynamically, e.g. when you need to convert them to UObjects. Daedalic Test Automation Plugin features _Dae Test Parameter Provider Actors_ for this: You can create parameter provider blueprints (e.g. through right-click in Content Browser > Create Advanced Asset > Test Automation > Test Parameter Provider Actor Blueprint). Then, you can override the `GetParameters` function to provide parameters for your test. + +![Parameter Provider Implementation](Documentation/ParameterProviderImplementation01.png) +![Parameter Provider Implementation](Documentation/ParameterProviderImplementation02.png) + +Finally, you'll have to add your provider to your test level, and to your test: + +![Parameter Providers](Documentation/ParameterProviders.png) + +For each parameterized test, all parameter providers are applied exactly once, before the first run of that test. + +### Skipping Tests + +If you want to temporarily _disable_ a test, you may specify a Skip Reason at your +_Dae Test Actor_ blueprint (or instance). Setting the Skip Reason to a non-empty string will cause the test to be skipped with the specified message. We don't provide a way of skipping a test without specifying a reason, because we feel that people should always know why a test is currently disabled. + +![Skip Reason](Documentation/SkipReason.png) + +Skipped tests will be marked as neither successful nor failed, and will show up explicitly as skipped in test reports. + +``` +LogDaeTest: Display: ADaeTestSuiteActor::RunAllTests - Test Suite: DaeTestSuiteActor_1 +LogDaeTest: Display: ADaeTestSuiteActor::RunNextTest - Test: BP_TestClimbLedge +LogDaeTest: Display: ADaeTestSuiteActor::OnTestSkipped - Test: BP_TestClimbLedge, SkipReason: Does not seem to succeed reliably. Occasionally, we fail to get a hold of the ledge after jumping if done automatically by the test. +LogDaeTest: Display: ADaeTestSuiteActor::RunNextTest - All tests finished. +``` + +### Assumptions + +You can also specify dynamic conditions for skipping a test through _assumptions_. This extends the above test model from Arrange-Act-Assert to Assume-Arrange-Act-Assert: You can make an arbitrary number of assumptions before even arranging up your test (e.g. check the value of some configuration variable or command-line parameter, or check the current runtime platform). + +![Assumption](Documentation/Assumption.png) + +If any of your assumptions fail, the test will be skipped instead of being marked as failure: + +``` +LogDaeTest: Display: ADaeTestSuiteActor::RunAllTests - Test Suite: DaeTestSuiteActor_1 +LogDaeTest: Display: ADaeTestSuiteActor::RunNextTest - Test: BP_TestAssume_2 +LogDaeTest: Display: ADaeTestSuiteActor::OnTestSkipped - Test: BP_TestAssume_2, SkipReason: Assumption failed - Running on XboxOne - Expected: True, but was: False +LogDaeTest: Display: ADaeTestSuiteActor::RunNextTest - All tests finished. +``` + + +## Running Tests + +### Play In Editor + +You can run each test suite by just entering _Play In Editor_, if "Run in PIE" is checked for that test suite (default). + +### Automation Window + +In order to run multiple tests, you can use the _Automation window_ of the session frontend of the Unreal Editor (Window > Test Automation). There, your tests will be shown under the category DaedalicTestAutomationPlugin. + +![Automation Window](Documentation/AutomationWindow.png) + +By default, the plugin will look in your `Maps/AutomatedTests` content folder for tests, but you can change that from Edit > Project Settings > Plugins > Daedalic Test Automation Plugin. + +### Gauntlet + +In order to run multiple tests from command-line (e.g. as part of your CI/CD pipeline), we recommend using _Gauntlet_, which can be run by passing a specific set of parameters to the Unreal Automation Tool (UAT). + +![Running Tests](Documentation/RunningTests.png) + +When using Gauntlet for running automated tests, as we don't want to force you to modify your version of Unreal Engine, we need to know where your source version of the engine can be found on disk. + +Set the `UNREAL_ENGINE_4_PATH` environment variable to the root folder of your source checkout, e.g. the directory that contains files like `Setup.bat` or `GenerateProjectFiles.bat`. + +Note that you might need to restart your shells and/or Visual Studio in order to have your changes take effect. + +If everything is set up correctly, `DaedalicTestAutomationPlugin.Automation` will be discovered when generating your project files (because the engine finds it in your Build directory). The project will then use your environment variable to publish its build results to the `Engine\Binaries\DotNET\AutomationScripts` folder your engine, where they can be discovered by the Unreal Automation Tool for running Gauntlet. + +Now, here's an example command line to get started: + +``` +"C:\Projects\UnrealEngine\Engine\Build\BatchFiles\RunUAT.bat" +RunUnreal +-project="C:\Projects\UnrealGame\UnrealGame.uproject" +-scriptdir="C:\Projects\UnrealGame" +-platform=Win64 +-configuration=Development +-build=editor +-test="DaedalicTestAutomationPlugin.Automation.DaeGauntletTest" +``` + +In the command line above: + +* `RunUAT.bat` starts the Unreal Automation Tool (UAT). +* `RunUnreal` tells the UAT to run Gauntlet. +* `-project` specifies the full path to your Unreal project file. +* `-scriptdir` tells UAT to compile and load your UAT extensions (in this case, at least `DaedalicTestAutomationPlugin.Automation`). +* `-build` tells Gauntlet to use your editor project instead of a packaged build. +* `-test` tells Gauntlet to use our custom controller (which in turn runs the test suites). + +This will run all tests the plugin finds in your Test Map Path (see [Automation Window](#automation-window)). Gauntlet will tell you which tests have been run, along with their results. It will also tell you where to find the log files of the test runs (artifacts). You can specify `-verbose` as additional parameter to the UAT to get even more feedback. + +Because documentation on Gauntlet is still sparse, you occasionally might want to check back on the original source files to learn about supported parameters and internal workings: + +* `Gauntlet.UnrealBuildSource.ResolveBuildReference` will tell you more about valid options for the `-build` parameter (e.g. running a staged build) +* `EpicGame.EpicGameTestConfig` (and its base classes) is used by our `DaedalicTestAutomationPlugin.Automation.DaeTestConfig` and can tell you more about valid options for the `-test` parameter. +* `Gauntlet.ArgumentWithParams.CreateFromString` is used for actually parsing the `-test` parameter. + +You can also specify additional parameters along with the `test` parameter for the test run: + +* `JUnitReportPath`: Generates a JUnit XML report to publish with your CI/CD pipeline. +* `TestName`: Runs the specified test, only, instead of all tests. + +Example: + +``` +"C:\Projects\UnrealEngine\Engine\Build\BatchFiles\RunUAT.bat" +RunUnreal +-project="C:\Projects\UnrealGame\UnrealGame.uproject" +-scriptdir="C:\Projects\UnrealGame" +-platform=Win64 +-configuration=Development +-build=editor +-test="DaedalicTestAutomationPlugin.Automation.DaeGauntletTest(JUnitReportPath=C:\Projects\UnrealGame\Saved\junit-report.xml)" +``` + +When generating JUnit reports, the plugin uses a standardized format (based on `org.junit.platform.reporting.legacy.xml.XmlReportWriter.writeTestsuite`), allowing you to publish the report just as you would when using JUnit. Here's an example of how the results look like when published with Jenkins: + +![Jenkins JUnit Report](Documentation/JUnitReport.png) + + +## Best Practices + +There are a few best practices we learned when writing tests, and we want to share those with you. We also recommend taking a closer look at the [References](#References) below, which go into more detail. + +1. _Keep your tests small._ Large tests that fail don't tell you much about the actual problem, especially when they contain multiple assertions. Try creating multiple small tests instead. +1. _Mock your dependencies._ Testing complex objects often forces you to violate the previous rule. Consider refactoring the code under test by splitting up your classes, and inject mock objects for dependencies where possible. +1. _Keep your tests fast._ Slow tests will eventually keep your team from running them. Even integration tests should never take much longer than a few seconds. +1. _Use meaningful test names._ Good test names can tell you much about the issue at a very first glance. Consider even using a common pattern for all tests, i.e. including the object and function under test, and the assertion you're making. +1. _Don't repeat yourself._ As with all software development, don't copy and paste your test logic. Use BeforeAll/BeforeEach/AfterEach/AfterAll and/or move test logic to function libraries where possible. + + +## Bugs, Questions & Feature Requests + +Daedalic Test Automation Plugin is still under heavy development. Whenever you're experiencing issues, missing a feature, or you just don't understand a part of the plugin, after verifying that you are using the [latest version](https://github.com/DaedalicEntertainment/ue4-test-automation/releases) and having checked whether a [similar issue](https://github.com/DaedalicEntertainment/ue4-test-automation/issues) has already been reported, feel free to [open a new issue](https://github.com/DaedalicEntertainment/ue4-test-automation/issues/new). In order to help us resolving your problem as fast as possible, please include the following details in your report: + +* Steps to reproduce +* What happened? +* What did you expect to happen? + +After being able to reproduce the issue, we'll look into fixing it immediately. + + +## Development Cycle + +We know that using this plugin in production requires you to be completely sure about stability and compatibility. Thus, new releases are created using [Semantic Versioning](http://semver.org/). In short: + +* Version numbers are specified as MAJOR.MINOR.PATCH. +* MAJOR version increases indicate incompatible API changes. +* MINOR version increases indicate added functionality in a backwards compatible manner. +* PATCH version increases indicate backwards compatible bug fixes. + +You'll always find all available releases and their respective release notes at: + +https://github.com/DaedalicEntertainment/ue4-test-automation/releases + + +## Contributing + +You want to contribute to Daedalic Test Automation Plugin? Great! Take a look at [Contributing](CONTRIBUTING.md) to get started right away! + + +## Future Work + +While the plugin already serves as solid base for creating automated tests, there's still a lot of things we'd like to add in the future. If you're interested in more details, take a look at the [GitHub milestones](https://github.com/DaedalicEntertainment/ue4-test-automation/milestones). + + +## License + +Daedalic Test Automation Plugin is released under the [MIT License](https://github.com/DaedalicEntertainment/ue4-test-automation/blob/develop/LICENSE). + + +## References + +* Masella, Robert. Rare, Ltd. Automated Testing of Gameplay Features in 'Sea of Thieves'. https://www.gdcvault.com/play/1026366/Automated-Testing-of-Gameplay-Features, 2019. +* Fray, Andrew. Spry Fox. Practical Unit Tests. https://www.gdcvault.com/play/1020353/Practical-Unit, 2014. +* The JUnit Team. JUnit 5 User Guide. https://junit.org/junit5/docs/current/user-guide, 2020.