From b6c90380768952b76e0c88dfdd66f69e5e3865bd Mon Sep 17 00:00:00 2001 From: muit Date: Mon, 31 Aug 2020 00:09:31 +0200 Subject: [PATCH 1/7] Reduced Automatron to a single header file, added namespaces This allows its insertion into any test modules without the need for dependencies or installing Automatron as a plugin --- Automatron.uplugin | 2 +- Source/Automatron/Automatron.Build.cs | 6 +- Source/Automatron/Private/Automatron.cpp | 7 - .../Automatron/Private/AutomatronModule.cpp | 7 +- .../Automatron/Private/Base/TestSpecBase.cpp | 375 ----- Source/Automatron/Private/Misc/Macros.h | 11 - Source/Automatron/Private/TestSpec.cpp | 165 --- Source/Automatron/Public/Automatron.h | 1238 ++++++++++++++++- Source/Automatron/Public/AutomatronModule.h | 3 - Source/Automatron/Public/Base/TestSpecBase.h | 516 ------- Source/Automatron/Public/TestSpec.h | 129 -- Source/AutomatronTest/AutomatronTest.Build.cs | 9 +- .../Private/Automatron.spec.cpp | 4 +- 13 files changed, 1242 insertions(+), 1230 deletions(-) delete mode 100644 Source/Automatron/Private/Automatron.cpp delete mode 100644 Source/Automatron/Private/Base/TestSpecBase.cpp delete mode 100644 Source/Automatron/Private/Misc/Macros.h delete mode 100644 Source/Automatron/Private/TestSpec.cpp delete mode 100644 Source/Automatron/Public/Base/TestSpecBase.h delete mode 100644 Source/Automatron/Public/TestSpec.h diff --git a/Automatron.uplugin b/Automatron.uplugin index 2d6920a..98a9d33 100644 --- a/Automatron.uplugin +++ b/Automatron.uplugin @@ -1,7 +1,7 @@ { "FileVersion": 3, "Version": 3, - "VersionName": "1.1a", + "VersionName": "1.2", "FriendlyName": "Automatron", "Description": "Plugin that provides improved automated tests for C++ and Blueprints", "Category": "Other", diff --git a/Source/Automatron/Automatron.Build.cs b/Source/Automatron/Automatron.Build.cs index 16f4893..b699bd3 100644 --- a/Source/Automatron/Automatron.Build.cs +++ b/Source/Automatron/Automatron.Build.cs @@ -18,13 +18,9 @@ public Automatron(ReadOnlyTargetRules Target) : base(Target) "FunctionalTesting" }); - PrivateDependencyModuleNames.AddRange(new string[] - { - }); - if (Target.bBuildEditor) { - PrivateDependencyModuleNames.Add("UnrealEd"); + PublicDependencyModuleNames.Add("UnrealEd"); } } } diff --git a/Source/Automatron/Private/Automatron.cpp b/Source/Automatron/Private/Automatron.cpp deleted file mode 100644 index ed380c7..0000000 --- a/Source/Automatron/Private/Automatron.cpp +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2020 Splash Damage, Ltd. - All Rights Reserved. - -#include "Automatron.h" - -#if WITH_DEV_AUTOMATION_TESTS - -#endif //WITH_DEV_AUTOMATION_TESTS diff --git a/Source/Automatron/Private/AutomatronModule.cpp b/Source/Automatron/Private/AutomatronModule.cpp index ec179ed..0383de0 100644 --- a/Source/Automatron/Private/AutomatronModule.cpp +++ b/Source/Automatron/Private/AutomatronModule.cpp @@ -2,9 +2,4 @@ #include "AutomatronModule.h" -#define LOCTEXT_NAMESPACE "FAutomatronModule" - - -#undef LOCTEXT_NAMESPACE - -IMPLEMENT_MODULE(FAutomatronModule, Automatron) \ No newline at end of file +IMPLEMENT_MODULE(FAutomatronModule, Automatron) diff --git a/Source/Automatron/Private/Base/TestSpecBase.cpp b/Source/Automatron/Private/Base/TestSpecBase.cpp deleted file mode 100644 index 67e4714..0000000 --- a/Source/Automatron/Private/Base/TestSpecBase.cpp +++ /dev/null @@ -1,375 +0,0 @@ -// Copyright 2020 Splash Damage, Ltd. - All Rights Reserved. - -#include "Base/TestSpecBase.h" - -bool FTestSpecBase::FSingleExecuteLatentCommand::Update() -{ - if (bSkipIfErrored && Spec->HasAnyErrors()) - { - return true; - } - - Predicate(); - return true; -} - - -bool FTestSpecBase::FUntilDoneLatentCommand::Update() -{ - if (!bIsRunning) - { - if (bSkipIfErrored && Spec->HasAnyErrors()) - { - return true; - } - - Predicate(FDoneDelegate::CreateSP(this, &FUntilDoneLatentCommand::Done)); - bIsRunning = true; - StartedRunning = FDateTime::UtcNow(); - } - - if (bDone) - { - Reset(); - return true; - } - else if (FDateTime::UtcNow() >= StartedRunning + Timeout) - { - Reset(); - Spec->AddError(TEXT("Latent command timed out."), 0); - return true; - } - - return false; -} - - -bool FTestSpecBase::FAsyncUntilDoneLatentCommand::Update() -{ - if (!Future.IsValid()) - { - if (bSkipIfErrored && Spec->HasAnyErrors()) - { - return true; - } - - Future = Async(Execution, [this]() { - Predicate(FDoneDelegate::CreateRaw(this, &FAsyncUntilDoneLatentCommand::Done)); - }); - - StartedRunning = FDateTime::UtcNow(); - } - - if (bDone) - { - Reset(); - return true; - } - else if (FDateTime::UtcNow() >= StartedRunning + Timeout) - { - Reset(); - Spec->AddError(TEXT("Latent command timed out."), 0); - return true; - } - return false; -} - - -bool FTestSpecBase::FAsyncLatentCommand::Update() -{ - if (!Future.IsValid()) - { - if (bSkipIfErrored && Spec->HasAnyErrors()) - { - return true; - } - - Future = Async(Execution, [this]() { - Predicate(); - bDone = true; - }); - - StartedRunning = FDateTime::UtcNow(); - } - - if (bDone) - { - Reset(); - return true; - } - else if (FDateTime::UtcNow() >= StartedRunning + Timeout) - { - Reset(); - Spec->AddError(TEXT("Latent command timed out."), 0); - return true; - } - - return false; -} - - -bool FTestSpecBase::RunTest(const FString& InParameters) -{ - EnsureDefinitions(); - - if (!InParameters.IsEmpty()) - { - const TSharedRef* SpecToRun = IdToSpecMap.Find(InParameters); - if (SpecToRun != nullptr) - { - for (int32 Index = 0; Index < (*SpecToRun)->Commands.Num(); ++Index) - { - FAutomationTestFramework::GetInstance().EnqueueLatentCommand((*SpecToRun)->Commands[Index]); - } - } - } - else - { - TArray> Specs; - IdToSpecMap.GenerateValueArray(Specs); - - for (int32 SpecIndex = 0; SpecIndex < Specs.Num(); SpecIndex++) - { - for (int32 CommandIndex = 0; CommandIndex < Specs[SpecIndex]->Commands.Num(); ++CommandIndex) - { - FAutomationTestFramework::GetInstance().EnqueueLatentCommand(Specs[SpecIndex]->Commands[CommandIndex]); - } - } - } - - TestsRemaining = GetNumTests(); - return true; -} - -FString FTestSpecBase::GetTestSourceFileName(const FString& InTestName) const -{ - FString TestId = InTestName; - if (TestId.StartsWith(TestName + TEXT(" "))) - { - TestId = InTestName.RightChop(TestName.Len() + 1); - } - - const TSharedRef* Spec = IdToSpecMap.Find(TestId); - if (Spec != nullptr) - { - return (*Spec)->Filename; - } - - return GetTestSourceFileName(); -} - -int32 FTestSpecBase::GetTestSourceFileLine(const FString& InTestName) const -{ - FString TestId = InTestName; - if (TestId.StartsWith(TestName + TEXT(" "))) - { - TestId = InTestName.RightChop(TestName.Len() + 1); - } - - const TSharedRef* Spec = IdToSpecMap.Find(TestId); - if (Spec != nullptr) - { - return (*Spec)->LineNumber; - } - - return GetTestSourceFileLine(); -} - -void FTestSpecBase::GetTests(TArray& OutBeautifiedNames, TArray& OutTestCommands) const -{ - EnsureDefinitions(); - - TArray> Specs; - IdToSpecMap.GenerateValueArray(Specs); - - for (int32 Index = 0; Index < Specs.Num(); Index++) - { - OutTestCommands.Push(Specs[Index]->Id); - OutBeautifiedNames.Push(Specs[Index]->Description); - } -} - -void FTestSpecBase::Describe(const FString& InDescription, TFunction DoWork) -{ - const TSharedRef ParentScope = DefinitionScopeStack.Last(); - const TSharedRef NewScope = MakeShared(); - NewScope->Description = InDescription; - ParentScope->Children.Push(NewScope); - - DefinitionScopeStack.Push(NewScope); - PushDescription(InDescription); - DoWork(); - PopDescription(InDescription); - DefinitionScopeStack.Pop(); - - if (NewScope->It.Num() == 0 && NewScope->Children.Num() == 0) - { - ParentScope->Children.Remove(NewScope); - } -} - -void FTestSpecBase::PreDefine() -{ - BeforeEach([this]() - { - CurrentContext = CurrentContext.NextContext(); - }); -} -void FTestSpecBase::PostDefine() -{ - AfterEach([this]() - { - if (IsLastTest()) - { - CurrentContext = {}; - } - }); -} - -void FTestSpecBase::BakeDefinitions() -{ - TArray> Stack; - Stack.Push(RootDefinitionScope.ToSharedRef()); - - TArray> BeforeEach; - TArray> AfterEach; - - while (Stack.Num() > 0) - { - const TSharedRef Scope = Stack.Last(); - - BeforeEach.Append(Scope->BeforeEach); - // ScopeAfter each are added reversed - AfterEach.Reserve(AfterEach.Num() + Scope->AfterEach.Num()); - for(int32 i = Scope->AfterEach.Num() - 1; i >= 0; --i) - { - AfterEach.Add(Scope->AfterEach[i]); - } - - for (int32 ItIndex = 0; ItIndex < Scope->It.Num(); ItIndex++) - { - TSharedRef It = Scope->It[ItIndex]; - - TSharedRef Spec = MakeShared(); - Spec->Id = It->Id; - Spec->Description = It->Description; - Spec->Filename = It->Filename; - Spec->LineNumber = It->LineNumber; - Spec->Commands.Append(BeforeEach); - Spec->Commands.Add(It->Command); - - // Add after each reversed - for (int32 i = AfterEach.Num() - 1; i >= 0; --i) - { - Spec->Commands.Add(AfterEach[i]); - } - - check(!IdToSpecMap.Contains(Spec->Id)); - IdToSpecMap.Add(Spec->Id, Spec); - } - Scope->It.Empty(); - - if (Scope->Children.Num() > 0) - { - Stack.Append(Scope->Children); - Scope->Children.Empty(); - } - else - { - while (Stack.Num() > 0 && Stack.Last()->Children.Num() == 0 && Stack.Last()->It.Num() == 0) - { - const TSharedRef PoppedScope = Stack.Pop(); - - if (PoppedScope->BeforeEach.Num() > 0) - { - BeforeEach.RemoveAt(BeforeEach.Num() - PoppedScope->BeforeEach.Num(), PoppedScope->BeforeEach.Num()); - } - - if (PoppedScope->AfterEach.Num() > 0) - { - AfterEach.RemoveAt(AfterEach.Num() - PoppedScope->AfterEach.Num(), PoppedScope->AfterEach.Num()); - } - } - } - } - - RootDefinitionScope.Reset(); - DefinitionScopeStack.Reset(); - bHasBeenDefined = true; -} - -void FTestSpecBase::Redefine() -{ - Description.Empty(); - IdToSpecMap.Empty(); - RootDefinitionScope.Reset(); - DefinitionScopeStack.Empty(); - bHasBeenDefined = false; -} - -FString FTestSpecBase::GetDescription() const -{ - FString CompleteDescription; - for (int32 Index = 0; Index < Description.Num(); ++Index) - { - if (Description[Index].IsEmpty()) - { - continue; - } - - if (CompleteDescription.IsEmpty()) - { - CompleteDescription = Description[Index]; - } - else if (FChar::IsWhitespace(CompleteDescription[CompleteDescription.Len() - 1]) || FChar::IsWhitespace(Description[Index][0])) - { - CompleteDescription = CompleteDescription + TEXT(".") + Description[Index]; - } - else - { - CompleteDescription = FString::Printf(TEXT("%s.%s"), *CompleteDescription, *Description[Index]); - } - } - - return CompleteDescription; -} - -FString FTestSpecBase::GetId() const -{ - if (Description.Last().EndsWith(TEXT("]"))) - { - FString ItDescription = Description.Last(); - ItDescription.RemoveAt(ItDescription.Len() - 1); - - int32 StartingBraceIndex = INDEX_NONE; - if (ItDescription.FindLastChar(TEXT('['), StartingBraceIndex) && StartingBraceIndex != ItDescription.Len() - 1) - { - FString CommandId = ItDescription.RightChop(StartingBraceIndex + 1); - return CommandId; - } - } - - FString CompleteId; - for (int32 Index = 0; Index < Description.Num(); ++Index) - { - if (Description[Index].IsEmpty()) - { - continue; - } - - if (CompleteId.IsEmpty()) - { - CompleteId = Description[Index]; - } - else if (FChar::IsWhitespace(CompleteId[CompleteId.Len() - 1]) || FChar::IsWhitespace(Description[Index][0])) - { - CompleteId = CompleteId + Description[Index]; - } - else - { - CompleteId = FString::Printf(TEXT("%s %s"), *CompleteId, *Description[Index]); - } - } - - return CompleteId; -} diff --git a/Source/Automatron/Private/Misc/Macros.h b/Source/Automatron/Private/Misc/Macros.h deleted file mode 100644 index 3c26da0..0000000 --- a/Source/Automatron/Private/Misc/Macros.h +++ /dev/null @@ -1,11 +0,0 @@ - -#pragma once - -#include - - -#define MakeSure(Condition) \ - if(!ensure(Condition)) return - -#define MakeSureMsg(Condition, Format, ...) \ - if(!ensureMsgf(Condition, Format, ##__VA_ARGS__)) return diff --git a/Source/Automatron/Private/TestSpec.cpp b/Source/Automatron/Private/TestSpec.cpp deleted file mode 100644 index d867cd5..0000000 --- a/Source/Automatron/Private/TestSpec.cpp +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright 2020 Splash Damage, Ltd. - All Rights Reserved. - -#include "TestSpec.h" -#include - -#if WITH_EDITOR -#include -#include -#endif - - -void FTestSpec::PreDefine() -{ - FTestSpecBase::PreDefine(); - - if (!bUseWorld) - { - return; - } - - LatentBeforeEach(EAsyncExecution::ThreadPool, [this](const auto & Done) - { - PrepareTestWorld(FSpecBaseOnWorldReady::CreateLambda([this, &Done](UWorld * InWorld) - { - World = InWorld; - Done.Execute(); - })); - }); -} - -void FTestSpec::PostDefine() -{ - AfterEach([this]() - { - // If this spec initialized a PIE world, tear it down - if (!bReuseWorldForAllTests || IsLastTest()) - { - ReleaseTestWorld(); - } - }); - - FTestSpecBase::PostDefine(); -} - -void FTestSpec::PrepareTestWorld(FSpecBaseOnWorldReady OnWorldReady) -{ - checkf(!IsInGameThread(), TEXT("PrepareTestWorld can only be done asynchronously. (LatentBeforeEach with ThreadPool or TaskGraph)")); - - UWorld* SelectedWorld = FindGameWorld(); - -#if WITH_EDITOR - // If there was no PIE world, start it and try again - if (bCanUsePIEWorld && !SelectedWorld && GIsEditor) - { - bool bPIEWorldIsReady = false; - FDelegateHandle PIEStartedHandle; - AsyncTask(ENamedThreads::GameThread, [&]() - { - PIEStartedHandle = FEditorDelegates::PostPIEStarted.AddLambda([&](const bool bIsSimulating) - { - // Notify the thread about the world being ready - bPIEWorldIsReady = true; - FEditorDelegates::PostPIEStarted.Remove(PIEStartedHandle); - }); - FEditorPromotionTestUtilities::StartPIE(false); - }); - - // Wait while PIE initializes - while (!bPIEWorldIsReady) - { - FPlatformProcess::Sleep(0.005f); - } - - SelectedWorld = FindGameWorld(); - bInitializedPIE = SelectedWorld != nullptr; - bInitializedWorld = bInitializedPIE; - } -#endif - - if (!SelectedWorld) - { - SelectedWorld = GWorld; -#if WITH_EDITOR - if (GIsEditor) - { - UE_LOG(LogTemp, Warning, TEXT("Test using GWorld. Not correct for PIE")); - } -#endif - } - - OnWorldReady.ExecuteIfBound(SelectedWorld); -} - -void FTestSpec::ReleaseTestWorld() -{ - if (!IsInGameThread()) - { - AsyncTask(ENamedThreads::GameThread, [this]() - { - ReleaseTestWorld(); - }); - return; - } - -#if WITH_EDITOR - if (bInitializedPIE) - { - FEditorPromotionTestUtilities::EndPIE(); - bInitializedPIE = false; - return; - } -#endif - - if (!bInitializedWorld) - { - return; - } - - // If world is not PIE, we take care of its teardown - UWorld* WorldPtr = World.Get(); - if(WorldPtr && !WorldPtr->IsPlayInEditor()) - { - WorldPtr->BeginTearingDown(); - - // Cancel any pending connection to a server - GEngine->CancelPending(WorldPtr); - - // Shut down any existing game connections - GEngine->ShutdownWorldNetDriver(WorldPtr); - - for (FActorIterator ActorIt(WorldPtr); ActorIt; ++ActorIt) - { - ActorIt->RouteEndPlay(EEndPlayReason::Quit); - } - - if (WorldPtr->GetGameInstance() != nullptr) - { - WorldPtr->GetGameInstance()->Shutdown(); - } - - World->FlushLevelStreaming(EFlushLevelStreamingType::Visibility); - World->CleanupWorld(); - } -} - -UWorld* FTestSpec::FindGameWorld() -{ - const TIndirectArray& WorldContexts = GEngine->GetWorldContexts(); - for (const FWorldContext& Context : WorldContexts) - { - if (Context.World() != nullptr) - { - if (Context.WorldType == EWorldType::PIE /*&& Context.PIEInstance == 0*/) - { - return Context.World(); - } - - if (Context.WorldType == EWorldType::Game) - { - return Context.World(); - } - } - } - return nullptr; -} diff --git a/Source/Automatron/Public/Automatron.h b/Source/Automatron/Public/Automatron.h index d615b7c..c39a406 100644 --- a/Source/Automatron/Public/Automatron.h +++ b/Source/Automatron/Public/Automatron.h @@ -1,15 +1,621 @@ -// Copyright 2020 Splash Damage, Ltd. - All Rights Reserved. +// AUTOMATRON +// Version: 1.2 +// Can be used as a header-only library or implemented as a module + +//BSD 3-Clause License +// +//Copyright (c) 2020, Splash Damage +//All rights reserved. +// +//Redistribution and use in source and binary forms, with or without +//modification, are permitted provided that the following conditions are met: +// +//1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +//2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +//3. Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +//AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +//IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +//FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +//DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +//SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +//CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +//OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +//OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + #pragma once #include -#include +#include #include #include -#include -#include "TestSpec.h" +#if WITH_EDITOR +# include +# include +#endif + + +//////////////////////////////////////////////////////////////// +// DEFINITIONS + +#if WITH_DEV_AUTOMATION_TESTS +namespace Automatron +{ + class FTestSpecBase; + + namespace Spec + { + ///////////////////////////////////////////////////// + // Initializes an spec instance at global execution time + // and registers it to the system + template + struct TRegister + { + static TRegister Register; + + T Instance; + + TRegister() : Instance{} + { + Instance.Setup(); + } + }; + + + ///////////////////////////////////////////////////// + // Represents an instance of a test + struct FContext + { + private: + + int32 Id = 0; + + public: + FContext(int32 Id = 0) : Id(Id) {} + + FContext NextContext() const { return { Id + 1 }; } + int32 GetId() const { return Id; } + + friend uint32 GetTypeHash(const FContext& Item) { return Item.Id; } + bool operator==(const FContext& Other) const { return Id == Other.Id; } + operator bool() const { return Id > 0; } + }; + + + struct FIt + { + FString Description; + FString Id; + FString Filename; + int32 LineNumber; + TSharedRef Command; + + FIt(FString InDescription, FString InId, FString InFilename, int32 InLineNumber, TSharedRef InCommand) + : Description(MoveTemp(InDescription)) + , Id(MoveTemp(InId)) + , Filename(MoveTemp(InFilename)) + , LineNumber(MoveTemp(InLineNumber)) + , Command(MoveTemp(InCommand)) + { } + }; + }; + + namespace Commands + { + class FSingleExecuteLatent : public IAutomationLatentCommand + { + private: + + const FTestSpecBase& Spec; + const TFunction Predicate; + const bool bSkipIfErrored = false; + + public: + FSingleExecuteLatent(const FTestSpecBase& InSpec, TFunction InPredicate, bool bInSkipIfErrored = false) + : Spec(InSpec) + , Predicate(MoveTemp(InPredicate)) + , bSkipIfErrored(bInSkipIfErrored) + { } + virtual ~FSingleExecuteLatent() {} + + virtual bool Update() override; + }; + + class FUntilDoneLatent : public IAutomationLatentCommand + { + private: + + FTestSpecBase& Spec; + const TFunction Predicate; + const FTimespan Timeout; + const bool bSkipIfErrored = false; + + bool bIsRunning = false; + FThreadSafeBool bDone = false; + FDateTime StartedRunning; + + public: + + FUntilDoneLatent(FTestSpecBase& InSpec, TFunction InPredicate, const FTimespan& InTimeout, bool bInSkipIfErrored = false) + : Spec(InSpec) + , Predicate(MoveTemp(InPredicate)) + , Timeout(InTimeout) + , bSkipIfErrored(bInSkipIfErrored) + {} + virtual ~FUntilDoneLatent() {} + + virtual bool Update() override; + + private: + + void Done() { bDone = true; } + void Reset(); + }; + + class FAsyncUntilDoneLatent : public IAutomationLatentCommand + { + private: + + FTestSpecBase& Spec; + const EAsyncExecution Execution; + const TFunction Predicate; + const FTimespan Timeout; + const bool bSkipIfErrored = false; + + FThreadSafeBool bDone = false; + TFuture Future; + FDateTime StartedRunning; + + public: + + FAsyncUntilDoneLatent(FTestSpecBase& InSpec, EAsyncExecution InExecution, TFunction InPredicate, const FTimespan& InTimeout, bool bInSkipIfErrored = false) + : Spec(InSpec) + , Execution(InExecution) + , Predicate(MoveTemp(InPredicate)) + , Timeout(InTimeout) + , bSkipIfErrored(bInSkipIfErrored) + {} + virtual ~FAsyncUntilDoneLatent() {} + + virtual bool Update() override; + + private: + + void Done() { bDone = true; } + void Reset(); + }; + + class FAsyncLatent : public IAutomationLatentCommand + { + private: + + FTestSpecBase& Spec; + const EAsyncExecution Execution; + const TFunction Predicate; + const FTimespan Timeout; + const bool bSkipIfErrored = false; + + FThreadSafeBool bDone = false; + FDateTime StartedRunning; + TFuture Future; + + public: + + FAsyncLatent(FTestSpecBase& InSpec, EAsyncExecution InExecution, TFunction InPredicate, const FTimespan& InTimeout, bool bInSkipIfErrored = false) + : Spec(InSpec) + , Execution(InExecution) + , Predicate(MoveTemp(InPredicate)) + , Timeout(InTimeout) + , bSkipIfErrored(bInSkipIfErrored) + {} + virtual ~FAsyncLatent() {} + + virtual bool Update() override; + + private: + + void Done() { bDone = true; } + void Reset(); + }; + }; + + + class FTestSpecBase + : public FAutomationTestBase + , public TSharedFromThis + { + private: + struct FSpecDefinitionScope + { + FString Description; + + TArray> BeforeEach; + TArray> It; + TArray> AfterEach; + + TArray> Children; + }; + + struct FSpec + { + FString Id; + FString Description; + FString Filename; + int32 LineNumber; + TArray> Commands; + }; + + + protected: + + /* The timespan for how long a block should be allowed to execute before giving up and failing the test */ + FTimespan DefaultTimeout = FTimespan::FromSeconds(30); + + /* Whether or not BeforeEach and It blocks should skip execution if the test has already failed */ + bool bEnableSkipIfError = true; + + private: + + TArray Description; + + TMap> IdToSpecMap; + + TSharedPtr RootDefinitionScope; + + TArray> DefinitionScopeStack; + + bool bHasBeenDefined = false; + + int32 TestsRemaining = 0; + + // The context of the active test + Spec::FContext CurrentContext; + + + public: + + FTestSpecBase(const FString& InName, const bool bInComplexTask) + : FAutomationTestBase(InName, bInComplexTask) + , RootDefinitionScope(MakeShared()) + { + DefinitionScopeStack.Push(RootDefinitionScope.ToSharedRef()); + } + + virtual ~FTestSpecBase() {} + + virtual bool RunTest(const FString& InParameters) override; + + virtual bool IsStressTest() const { return false; } + virtual uint32 GetRequiredDeviceNum() const override { return 1; } + + virtual FString GetTestSourceFileName() const override; + virtual int32 GetTestSourceFileLine() const override; + virtual FString GetTestSourceFileName(const FString& InTestName) const override; + virtual int32 GetTestSourceFileLine(const FString& InTestName) const override; + + virtual void GetTests(TArray& OutBeautifiedNames, TArray& OutTestCommands) const override; + + + // BEGIN Enabled Scopes + void Describe(const FString& InDescription, TFunction DoWork); + + void It(const FString& InDescription, TFunction DoWork) + { + const TSharedRef CurrentScope = DefinitionScopeStack.Last(); + const TArray Stack = FPlatformStackWalk::GetStack(1, 1); + + PushDescription(InDescription); + auto Command = MakeShared(*this, DoWork, bEnableSkipIfError); + CurrentScope->It.Push(MakeShared(GetDescription(), GetId(), Stack[0].Filename, Stack[0].LineNumber, Command)); + PopDescription(InDescription); + } + + void It(const FString& InDescription, EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) + { + const TSharedRef CurrentScope = DefinitionScopeStack.Last(); + const TArray Stack = FPlatformStackWalk::GetStack(1, 1); + + PushDescription(InDescription); + auto Command = MakeShared(*this, Execution, DoWork, Timeout, bEnableSkipIfError); + CurrentScope->It.Push(MakeShared(GetDescription(), GetId(), Stack[0].Filename, Stack[0].LineNumber, Command)); + PopDescription(InDescription); + } + + void It(const FString& InDescription, EAsyncExecution Execution, TFunction DoWork) + { + It(InDescription, Execution, DefaultTimeout, DoWork); + } + + void LatentIt(const FString& InDescription, const FTimespan& Timeout, TFunction DoWork) + { + const TSharedRef CurrentScope = DefinitionScopeStack.Last(); + const TArray Stack = FPlatformStackWalk::GetStack(1, 1); + + PushDescription(InDescription); + auto Command = MakeShared(*this, DoWork, Timeout, bEnableSkipIfError); + CurrentScope->It.Push(MakeShared(GetDescription(), GetId(), Stack[0].Filename, Stack[0].LineNumber, Command)); + PopDescription(InDescription); + } + + void LatentIt(const FString& InDescription, TFunction DoWork) + { + LatentIt(InDescription, DefaultTimeout, DoWork); + } + + void LatentIt(const FString& InDescription, EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) + { + const TSharedRef CurrentScope = DefinitionScopeStack.Last(); + const TArray Stack = FPlatformStackWalk::GetStack(1, 1); + + PushDescription(InDescription); + auto Command = MakeShared(*this, Execution, DoWork, Timeout, bEnableSkipIfError); + CurrentScope->It.Push(MakeShared(GetDescription(), GetId(), Stack[0].Filename, Stack[0].LineNumber, Command)); + PopDescription(InDescription); + } + + void LatentIt(const FString& InDescription, EAsyncExecution Execution, TFunction DoWork) + { + LatentIt(InDescription, Execution, DefaultTimeout, DoWork); + } + + void BeforeEach(TFunction DoWork) + { + DefinitionScopeStack.Last()->BeforeEach.Push( + MakeShared(*this, DoWork, bEnableSkipIfError) + ); + } + + void BeforeEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) + { + DefinitionScopeStack.Last()->BeforeEach.Push( + MakeShared(*this, Execution, DoWork, Timeout, bEnableSkipIfError) + ); + } + + void BeforeEach(EAsyncExecution Execution, TFunction DoWork) + { + BeforeEach(Execution, DefaultTimeout, DoWork); + } + + void LatentBeforeEach(const FTimespan& Timeout, TFunction DoWork) + { + DefinitionScopeStack.Last()->BeforeEach.Push( + MakeShared(*this, DoWork, Timeout, bEnableSkipIfError) + ); + } + + void LatentBeforeEach(TFunction DoWork) + { + LatentBeforeEach(DefaultTimeout, DoWork); + } + + void LatentBeforeEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) + { + DefinitionScopeStack.Last()->BeforeEach.Push( + MakeShared(*this, Execution, DoWork, Timeout, bEnableSkipIfError) + ); + } + + void LatentBeforeEach(EAsyncExecution Execution, TFunction DoWork) + { + LatentBeforeEach(Execution, DefaultTimeout, DoWork); + } + + void AfterEach(TFunction DoWork) + { + DefinitionScopeStack.Last()->AfterEach.Push( + MakeShared(*this, DoWork) + ); + } + + void AfterEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) + { + DefinitionScopeStack.Last()->AfterEach.Push( + MakeShared(*this, Execution, DoWork, Timeout) + ); + } + + void AfterEach(EAsyncExecution Execution, TFunction DoWork) + { + AfterEach(Execution, DefaultTimeout, DoWork); + } + + void LatentAfterEach(const FTimespan& Timeout, TFunction DoWork) + { + DefinitionScopeStack.Last()->AfterEach.Push( + MakeShared(*this, DoWork, Timeout) + ); + } + + void LatentAfterEach(TFunction DoWork) + { + LatentAfterEach(DefaultTimeout, DoWork); + } + + void LatentAfterEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) + { + DefinitionScopeStack.Last() ->AfterEach.Push( + MakeShared(*this, Execution, DoWork, Timeout) + ); + } + + void LatentAfterEach(EAsyncExecution Execution, TFunction DoWork) + { + LatentAfterEach(Execution, DefaultTimeout, DoWork); + } + // END Enabled Scopes + + + // BEGIN Disabled Scopes + void xDescribe(const FString& InDescription, TFunction DoWork) {} + + void xIt(const FString& InDescription, TFunction DoWork) {} + void xIt(const FString& InDescription, EAsyncExecution Execution, TFunction DoWork) {} + void xIt(const FString& InDescription, EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) {} + + void xLatentIt(const FString& InDescription, TFunction DoWork) {} + void xLatentIt(const FString& InDescription, const FTimespan& Timeout, TFunction DoWork) {} + void xLatentIt(const FString& InDescription, EAsyncExecution Execution, TFunction DoWork) {} + void xLatentIt(const FString& InDescription, EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) {} + + void xBeforeEach(TFunction DoWork) {} + void xBeforeEach(EAsyncExecution Execution, TFunction DoWork) {} + void xBeforeEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) {} + + void xLatentBeforeEach(TFunction DoWork) {} + void xLatentBeforeEach(const FTimespan& Timeout, TFunction DoWork) {} + void xLatentBeforeEach(EAsyncExecution Execution, TFunction DoWork) {} + void xLatentBeforeEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) {} + + void xAfterEach(TFunction DoWork) {} + void xAfterEach(EAsyncExecution Execution, TFunction DoWork) {} + void xAfterEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) {} + + void xLatentAfterEach(TFunction DoWork) {} + void xLatentAfterEach(const FTimespan& Timeout, TFunction DoWork) {} + void xLatentAfterEach(EAsyncExecution Execution, TFunction DoWork) {} + void xLatentAfterEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) {} + // END Disabled Scopes + + + int32 GetNumTests() const { return IdToSpecMap.Num(); } + int32 GetTestsRemaining() const { return GetNumTests() - CurrentContext.GetId(); } + Spec::FContext GetCurrentContext() const { return CurrentContext; } + bool IsFirstTest() const { return CurrentContext.GetId() == 1; } + bool IsLastTest() const { return CurrentContext.GetId() == GetNumTests(); } + + protected: + + void EnsureDefinitions() const; + + virtual void RunDefine() + { + PreDefine(); + Define(); + PostDefine(); + } + virtual void PreDefine(); + virtual void Define() = 0; + virtual void PostDefine(); + + void BakeDefinitions(); + + void Redefine(); + + private: + + void PushDescription(const FString& InDescription) + { + Description.Add(InDescription); + } + + void PopDescription(const FString& InDescription) + { + Description.RemoveAt(Description.Num() - 1); + } + + FString GetDescription() const; + + FString GetId() const; + }; + + + DECLARE_DELEGATE_OneParam(FSpecBaseOnWorldReady, UWorld*); + + class FTestSpec : public FTestSpecBase + { + public: + + // Should a world be initialized? + bool bUseWorld = true; + + // If true the world used for testing will be reused for all tests + bool bReuseWorldForAllTests = true; + + // If true and in editor, a PIE instance will be used to test + bool bCanUsePIEWorld = true; + + private: + + FString ClassName; + FString PrettyName; + FString FileName; + int32 LineNumber = -1; + uint32 Flags = 0; + + bool bInitializedWorld = false; + #if WITH_EDITOR + bool bInitializedPIE = false; + #endif + + TWeakObjectPtr World; + + + public: + + FTestSpec() : FTestSpecBase("", false) {} + + virtual FString GetTestSourceFileName() const override { return FileName; } + virtual int32 GetTestSourceFileLine() const override { return LineNumber; } + virtual uint32 GetTestFlags() const override { return Flags; } + + const FString& GetClassName() const { return ClassName; } + const FString& GetPrettyName() const { return PrettyName; } + + protected: + + virtual FString GetBeautifiedTestName() const override { return PrettyName; } + + template + void Setup(FString&& InName, FString&& InPrettyName, FString&& InFileName, int32 InLineNumber); + + // Used to indicate a test is pending to be implemented. + void TestNotImplemented() + { + AddWarning(TEXT("Test not implemented"), 1); + } + + virtual void PreDefine() override; + virtual void PostDefine() override; + + void PrepareTestWorld(FSpecBaseOnWorldReady OnWorldReady); + void ReleaseTestWorld(); + + UWorld* GetWorld() const { return World.Get(); } + + private: + + void Reregister(const FString& NewName) + { + FAutomationTestFramework::Get().UnregisterAutomationTest(TestName); + TestName = NewName; + FAutomationTestFramework::Get().RegisterAutomationTest(TestName, this); + } + + // Finds the first available game world (Standalone or PIE) + static UWorld* FindGameWorld(); + }; +} + +#endif //WITH_DEV_AUTOMATION_TESTS + + +//////////////////////////////////////////////////////////////// +// GENERATION MACROS + +#if WITH_DEV_AUTOMATION_TESTS #define GENERATE_SPEC(TClass, PrettyName, TFlags) \ GENERATE_SPEC_PRIVATE(TClass, PrettyName, TFlags, __FILE__, __LINE__) @@ -20,18 +626,634 @@ private: \ { \ FTestSpec::Setup(TEXT(#TClass), TEXT(PrettyName), FileName, LineNumber); \ } \ - static TSpecRegister& __meta_register() \ + static Automatron::Spec::TRegister& __meta_register() \ { \ - return TSpecRegister::Register; \ + return Automatron::Spec::TRegister::Register; \ } \ - friend TSpecRegister; \ + friend Automatron::Spec::TRegister; \ \ virtual void Define() override - #define SPEC(TClass, TParent, PrettyName, TFlags) \ class TClass : public TParent \ { \ GENERATE_SPEC(TClass, PrettyName, TFlags); \ }; \ void TClass::Define() + +#else //WITH_DEV_AUTOMATION_TESTS + +#define GENERATE_SPEC(TClass, PrettyName, TFlags) +#define SPEC(TClass, TParent, PrettyName, TFlags) \ +class TClass \ +{ \ + void Define(); \ +}; \ +void TClass::Define() + +#endif //WITH_DEV_AUTOMATION_TESTS + + +//////////////////////////////////////////////////////////////// +// DECLARATIONS + +#if WITH_DEV_AUTOMATION_TESTS +namespace Automatron +{ + namespace Spec + { + } + + namespace Commands + { + inline bool FSingleExecuteLatent::Update() + { + if (bSkipIfErrored && Spec.HasAnyErrors()) + { + return true; + } + + Predicate(); + return true; + } + + inline bool FUntilDoneLatent::Update() + { + if (!bIsRunning) + { + if (bSkipIfErrored && Spec.HasAnyErrors()) + { + return true; + } + + Predicate(FDoneDelegate::CreateSP(this, &FUntilDoneLatent::Done)); + bIsRunning = true; + StartedRunning = FDateTime::UtcNow(); + } + + if (bDone) + { + Reset(); + return true; + } + else if (FDateTime::UtcNow() >= StartedRunning + Timeout) + { + Reset(); + Spec.AddError(TEXT("Latent command timed out."), 0); + return true; + } + + return false; + } + + inline void FUntilDoneLatent::Reset() + { + // Reset the done for the next potential run of this command + bDone = false; + bIsRunning = false; + } + + inline bool FAsyncUntilDoneLatent::Update() + { + if (!Future.IsValid()) + { + if (bSkipIfErrored && Spec.HasAnyErrors()) + { + return true; + } + + Future = Async(Execution, [this]() { + Predicate(FDoneDelegate::CreateRaw(this, &FAsyncUntilDoneLatent::Done)); + }); + + StartedRunning = FDateTime::UtcNow(); + } + + if (bDone) + { + Reset(); + return true; + } + else if (FDateTime::UtcNow() >= StartedRunning + Timeout) + { + Reset(); + Spec.AddError(TEXT("Latent command timed out."), 0); + return true; + } + return false; + } + + inline void FAsyncUntilDoneLatent::Reset() + { + // Reset the done for the next potential run of this command + bDone = false; + Future = TFuture(); + } + + inline bool FAsyncLatent::Update() + { + if (!Future.IsValid()) + { + if (bSkipIfErrored && Spec.HasAnyErrors()) + { + return true; + } + + Future = Async(Execution, [this]() { + Predicate(); + bDone = true; + }); + + StartedRunning = FDateTime::UtcNow(); + } + + if (bDone) + { + Reset(); + return true; + } + else if (FDateTime::UtcNow() >= StartedRunning + Timeout) + { + Reset(); + Spec.AddError(TEXT("Latent command timed out."), 0); + return true; + } + + return false; + } + + inline void FAsyncLatent::Reset() + { + // Reset the done for the next potential run of this command + bDone = false; + Future = TFuture(); + } + } + + + inline void FTestSpecBase::EnsureDefinitions() const + { + if (!bHasBeenDefined) + { + const_cast(this)->RunDefine(); + const_cast(this)->BakeDefinitions(); + } + } + + inline FString FTestSpecBase::GetTestSourceFileName() const + { + return FAutomationTestBase::GetTestSourceFileName(); + } + + inline int32 FTestSpecBase::GetTestSourceFileLine() const + { + return FAutomationTestBase::GetTestSourceFileLine(); + } + + inline bool FTestSpecBase::RunTest(const FString& InParameters) + { + EnsureDefinitions(); + + if (!InParameters.IsEmpty()) + { + const TSharedRef* SpecToRun = IdToSpecMap.Find(InParameters); + if (SpecToRun != nullptr) + { + for (int32 Index = 0; Index < (*SpecToRun)->Commands.Num(); ++Index) + { + FAutomationTestFramework::GetInstance().EnqueueLatentCommand((*SpecToRun)->Commands[Index]); + } + } + } + else + { + TArray> Specs; + IdToSpecMap.GenerateValueArray(Specs); + + for (int32 SpecIndex = 0; SpecIndex < Specs.Num(); SpecIndex++) + { + for (int32 CommandIndex = 0; CommandIndex < Specs[SpecIndex]->Commands.Num(); ++CommandIndex) + { + FAutomationTestFramework::GetInstance().EnqueueLatentCommand(Specs[SpecIndex]->Commands[CommandIndex]); + } + } + } + + TestsRemaining = GetNumTests(); + return true; + } + + inline FString FTestSpecBase::GetTestSourceFileName(const FString& InTestName) const + { + FString TestId = InTestName; + if (TestId.StartsWith(TestName + TEXT(" "))) + { + TestId = InTestName.RightChop(TestName.Len() + 1); + } + + const TSharedRef* Spec = IdToSpecMap.Find(TestId); + if (Spec != nullptr) + { + return (*Spec)->Filename; + } + + return GetTestSourceFileName(); + } + + inline int32 FTestSpecBase::GetTestSourceFileLine(const FString& InTestName) const + { + FString TestId = InTestName; + if (TestId.StartsWith(TestName + TEXT(" "))) + { + TestId = InTestName.RightChop(TestName.Len() + 1); + } + + const TSharedRef* Spec = IdToSpecMap.Find(TestId); + if (Spec != nullptr) + { + return (*Spec)->LineNumber; + } + + return GetTestSourceFileLine(); + } + + inline void FTestSpecBase::GetTests(TArray& OutBeautifiedNames, TArray& OutTestCommands) const + { + EnsureDefinitions(); + + TArray> Specs; + IdToSpecMap.GenerateValueArray(Specs); + + for (int32 Index = 0; Index < Specs.Num(); Index++) + { + OutTestCommands.Push(Specs[Index]->Id); + OutBeautifiedNames.Push(Specs[Index]->Description); + } + } + + inline void FTestSpecBase::Describe(const FString& InDescription, TFunction DoWork) + { + const TSharedRef ParentScope = DefinitionScopeStack.Last(); + const TSharedRef NewScope = MakeShared(); + NewScope->Description = InDescription; + ParentScope->Children.Push(NewScope); + + DefinitionScopeStack.Push(NewScope); + PushDescription(InDescription); + DoWork(); + PopDescription(InDescription); + DefinitionScopeStack.Pop(); + + if (NewScope->It.Num() == 0 && NewScope->Children.Num() == 0) + { + ParentScope->Children.Remove(NewScope); + } + } + + inline void FTestSpecBase::PreDefine() + { + BeforeEach([this]() + { + CurrentContext = CurrentContext.NextContext(); + }); + } + + inline void FTestSpecBase::PostDefine() + { + AfterEach([this]() + { + if (IsLastTest()) + { + CurrentContext = {}; + } + }); + } + + inline void FTestSpecBase::BakeDefinitions() + { + TArray> Stack; + Stack.Push(RootDefinitionScope.ToSharedRef()); + + TArray> BeforeEach; + TArray> AfterEach; + + while (Stack.Num() > 0) + { + const TSharedRef Scope = Stack.Last(); + + BeforeEach.Append(Scope->BeforeEach); + // ScopeAfter each are added reversed + AfterEach.Reserve(AfterEach.Num() + Scope->AfterEach.Num()); + for(int32 i = Scope->AfterEach.Num() - 1; i >= 0; --i) + { + AfterEach.Add(Scope->AfterEach[i]); + } + + for (int32 ItIndex = 0; ItIndex < Scope->It.Num(); ItIndex++) + { + TSharedRef It = Scope->It[ItIndex]; + + TSharedRef Spec = MakeShared(); + Spec->Id = It->Id; + Spec->Description = It->Description; + Spec->Filename = It->Filename; + Spec->LineNumber = It->LineNumber; + Spec->Commands.Append(BeforeEach); + Spec->Commands.Add(It->Command); + + // Add after each reversed + for (int32 i = AfterEach.Num() - 1; i >= 0; --i) + { + Spec->Commands.Add(AfterEach[i]); + } + + check(!IdToSpecMap.Contains(Spec->Id)); + IdToSpecMap.Add(Spec->Id, Spec); + } + Scope->It.Empty(); + + if (Scope->Children.Num() > 0) + { + Stack.Append(Scope->Children); + Scope->Children.Empty(); + } + else + { + while (Stack.Num() > 0 && Stack.Last()->Children.Num() == 0 && Stack.Last()->It.Num() == 0) + { + const TSharedRef PoppedScope = Stack.Pop(); + + if (PoppedScope->BeforeEach.Num() > 0) + { + BeforeEach.RemoveAt(BeforeEach.Num() - PoppedScope->BeforeEach.Num(), PoppedScope->BeforeEach.Num()); + } + + if (PoppedScope->AfterEach.Num() > 0) + { + AfterEach.RemoveAt(AfterEach.Num() - PoppedScope->AfterEach.Num(), PoppedScope->AfterEach.Num()); + } + } + } + } + + RootDefinitionScope.Reset(); + DefinitionScopeStack.Reset(); + bHasBeenDefined = true; + } + + inline void FTestSpecBase::Redefine() + { + Description.Empty(); + IdToSpecMap.Empty(); + RootDefinitionScope.Reset(); + DefinitionScopeStack.Empty(); + bHasBeenDefined = false; + } + + inline FString FTestSpecBase::GetDescription() const + { + FString CompleteDescription; + for (int32 Index = 0; Index < Description.Num(); ++Index) + { + if (Description[Index].IsEmpty()) + { + continue; + } + + if (CompleteDescription.IsEmpty()) + { + CompleteDescription = Description[Index]; + } + else if (FChar::IsWhitespace(CompleteDescription[CompleteDescription.Len() - 1]) || FChar::IsWhitespace(Description[Index][0])) + { + CompleteDescription = CompleteDescription + TEXT(".") + Description[Index]; + } + else + { + CompleteDescription = FString::Printf(TEXT("%s.%s"), *CompleteDescription, *Description[Index]); + } + } + + return CompleteDescription; + } + + inline FString FTestSpecBase::GetId() const + { + if (Description.Last().EndsWith(TEXT("]"))) + { + FString ItDescription = Description.Last(); + ItDescription.RemoveAt(ItDescription.Len() - 1); + + int32 StartingBraceIndex = INDEX_NONE; + if (ItDescription.FindLastChar(TEXT('['), StartingBraceIndex) && StartingBraceIndex != ItDescription.Len() - 1) + { + FString CommandId = ItDescription.RightChop(StartingBraceIndex + 1); + return CommandId; + } + } + + FString CompleteId; + for (int32 Index = 0; Index < Description.Num(); ++Index) + { + if (Description[Index].IsEmpty()) + { + continue; + } + + if (CompleteId.IsEmpty()) + { + CompleteId = Description[Index]; + } + else if (FChar::IsWhitespace(CompleteId[CompleteId.Len() - 1]) || FChar::IsWhitespace(Description[Index][0])) + { + CompleteId = CompleteId + Description[Index]; + } + else + { + CompleteId = FString::Printf(TEXT("%s %s"), *CompleteId, *Description[Index]); + } + } + + return CompleteId; + } + + + inline void FTestSpec::PreDefine() + { + FTestSpecBase::PreDefine(); + + if (!bUseWorld) + { + return; + } + + LatentBeforeEach(EAsyncExecution::ThreadPool, [this](const auto & Done) + { + PrepareTestWorld(FSpecBaseOnWorldReady::CreateLambda([this, &Done](UWorld * InWorld) + { + World = InWorld; + Done.Execute(); + })); + }); + } + + inline void FTestSpec::PostDefine() + { + AfterEach([this]() + { + // If this spec initialized a PIE world, tear it down + if (!bReuseWorldForAllTests || IsLastTest()) + { + ReleaseTestWorld(); + } + }); + + FTestSpecBase::PostDefine(); + } + + inline void FTestSpec::PrepareTestWorld(FSpecBaseOnWorldReady OnWorldReady) + { + checkf(!IsInGameThread(), TEXT("PrepareTestWorld can only be done asynchronously. (LatentBeforeEach with ThreadPool or TaskGraph)")); + + UWorld* SelectedWorld = FindGameWorld(); + + #if WITH_EDITOR + // If there was no PIE world, start it and try again + if (bCanUsePIEWorld && !SelectedWorld && GIsEditor) + { + bool bPIEWorldIsReady = false; + FDelegateHandle PIEStartedHandle; + AsyncTask(ENamedThreads::GameThread, [&]() + { + PIEStartedHandle = FEditorDelegates::PostPIEStarted.AddLambda([&](const bool bIsSimulating) + { + // Notify the thread about the world being ready + bPIEWorldIsReady = true; + FEditorDelegates::PostPIEStarted.Remove(PIEStartedHandle); + }); + FEditorPromotionTestUtilities::StartPIE(false); + }); + + // Wait while PIE initializes + while (!bPIEWorldIsReady) + { + FPlatformProcess::Sleep(0.005f); + } + + SelectedWorld = FindGameWorld(); + bInitializedPIE = SelectedWorld != nullptr; + bInitializedWorld = bInitializedPIE; + } + #endif + + if (!SelectedWorld) + { + SelectedWorld = GWorld; + #if WITH_EDITOR + if (GIsEditor) + { + UE_LOG(LogTemp, Warning, TEXT("Test using GWorld. Not correct for PIE")); + } + #endif + } + + OnWorldReady.ExecuteIfBound(SelectedWorld); + } + + inline void FTestSpec::ReleaseTestWorld() + { + if (!IsInGameThread()) + { + AsyncTask(ENamedThreads::GameThread, [this]() + { + ReleaseTestWorld(); + }); + return; + } + + #if WITH_EDITOR + if (bInitializedPIE) + { + FEditorPromotionTestUtilities::EndPIE(); + bInitializedPIE = false; + return; + } + #endif + + if (!bInitializedWorld) + { + return; + } + + // If world is not PIE, we take care of its teardown + UWorld* WorldPtr = World.Get(); + if(WorldPtr && !WorldPtr->IsPlayInEditor()) + { + WorldPtr->BeginTearingDown(); + + // Cancel any pending connection to a server + GEngine->CancelPending(WorldPtr); + + // Shut down any existing game connections + GEngine->ShutdownWorldNetDriver(WorldPtr); + + for (FActorIterator ActorIt(WorldPtr); ActorIt; ++ActorIt) + { + ActorIt->RouteEndPlay(EEndPlayReason::Quit); + } + + if (WorldPtr->GetGameInstance() != nullptr) + { + WorldPtr->GetGameInstance()->Shutdown(); + } + + World->FlushLevelStreaming(EFlushLevelStreamingType::Visibility); + World->CleanupWorld(); + } + } + + inline UWorld* FTestSpec::FindGameWorld() + { + const TIndirectArray& WorldContexts = GEngine->GetWorldContexts(); + for (const FWorldContext& Context : WorldContexts) + { + if (Context.World() != nullptr) + { + if (Context.WorldType == EWorldType::PIE /*&& Context.PIEInstance == 0*/) + { + return Context.World(); + } + + if (Context.WorldType == EWorldType::Game) + { + return Context.World(); + } + } + } + return nullptr; + } + + template + inline void FTestSpec::Setup(FString&& InName, FString&& InPrettyName, FString&& InFileName, int32 InLineNumber) + { + static_assert(TFlags & EAutomationTestFlags::ApplicationContextMask, "AutomationTest has no application flag. It shouldn't run. See AutomationTest.h."); \ + static_assert(((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::SmokeFilter) || + ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::EngineFilter) || + ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::ProductFilter) || + ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::PerfFilter) || + ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::StressFilter) || + ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::NegativeFilter), + "All AutomationTests must have exactly 1 filter type specified. See AutomationTest.h."); + + ClassName = InName; + PrettyName = MoveTemp(InPrettyName); + FileName = MoveTemp(InFileName); + LineNumber = InLineNumber; + Flags = TFlags; + + Reregister(InName); + } +} + + +#endif //WITH_DEV_AUTOMATION_TESTS diff --git a/Source/Automatron/Public/AutomatronModule.h b/Source/Automatron/Public/AutomatronModule.h index a1f6a11..f825046 100644 --- a/Source/Automatron/Public/AutomatronModule.h +++ b/Source/Automatron/Public/AutomatronModule.h @@ -4,13 +4,10 @@ #include #include -#include "TestSpec.h" class FAutomatronModule : public IModuleInterface { - static TArray> SpecInstances; - public: /** Begin IModuleInterface implementation */ diff --git a/Source/Automatron/Public/Base/TestSpecBase.h b/Source/Automatron/Public/Base/TestSpecBase.h deleted file mode 100644 index f355f04..0000000 --- a/Source/Automatron/Public/Base/TestSpecBase.h +++ /dev/null @@ -1,516 +0,0 @@ -// Copyright 2020 Splash Damage, Ltd. - All Rights Reserved. - -#pragma once - -#include -#include - - -struct AUTOMATRON_API FTestContext -{ -private: - - int32 Id = 0; - -public: - FTestContext(int32 Id = 0) : Id(Id) {} - - FTestContext NextContext() const { return { Id + 1 }; } - int32 GetId() const { return Id; } - - friend uint32 GetTypeHash(const FTestContext& Item) { return Item.Id; } - bool operator==(const FTestContext& Other) const { return Id == Other.Id; } - operator bool() const { return Id > 0; } -}; - - -class AUTOMATRON_API FTestSpecBase - : public FAutomationTestBase - , public TSharedFromThis -{ -private: - - class FSingleExecuteLatentCommand : public IAutomationLatentCommand - { - private: - - const FTestSpecBase* const Spec; - const TFunction Predicate; - const bool bSkipIfErrored; - - public: - FSingleExecuteLatentCommand(const FTestSpecBase* const InSpec, TFunction InPredicate, bool bInSkipIfErrored = false) - : Spec(InSpec) - , Predicate(MoveTemp(InPredicate)) - , bSkipIfErrored(bInSkipIfErrored) - { } - virtual ~FSingleExecuteLatentCommand() {} - - virtual bool Update() override; - }; - - class FUntilDoneLatentCommand : public IAutomationLatentCommand - { - private: - - FTestSpecBase* const Spec; - const TFunction Predicate; - const FTimespan Timeout; - const bool bSkipIfErrored; - - bool bIsRunning; - FDateTime StartedRunning; - FThreadSafeBool bDone; - - public: - - FUntilDoneLatentCommand(FTestSpecBase* const InSpec, TFunction InPredicate, const FTimespan& InTimeout, bool bInSkipIfErrored = false) - : Spec(InSpec) - , Predicate(MoveTemp(InPredicate)) - , Timeout(InTimeout) - , bSkipIfErrored(bInSkipIfErrored) - , bIsRunning(false) - , bDone(false) - {} - virtual ~FUntilDoneLatentCommand() {} - - virtual bool Update() override; - - private: - - void Done() - { - bDone = true; - } - - void Reset() - { - // Reset the done for the next potential run of this command - bDone = false; - bIsRunning = false; - } - }; - - class FAsyncUntilDoneLatentCommand : public IAutomationLatentCommand - { - private: - - FTestSpecBase* const Spec; - const EAsyncExecution Execution; - const TFunction Predicate; - const FTimespan Timeout; - const bool bSkipIfErrored; - - FThreadSafeBool bDone; - FDateTime StartedRunning; - TFuture Future; - - public: - - FAsyncUntilDoneLatentCommand(FTestSpecBase* const InSpec, EAsyncExecution InExecution, TFunction InPredicate, const FTimespan& InTimeout, bool bInSkipIfErrored = false) - : Spec(InSpec) - , Execution(InExecution) - , Predicate(MoveTemp(InPredicate)) - , Timeout(InTimeout) - , bSkipIfErrored(bInSkipIfErrored) - , bDone(false) - {} - virtual ~FAsyncUntilDoneLatentCommand() {} - - virtual bool Update() override; - - private: - - void Done() - { - bDone = true; - } - - void Reset() - { - // Reset the done for the next potential run of this command - bDone = false; - Future = TFuture(); - } - }; - - class FAsyncLatentCommand : public IAutomationLatentCommand - { - private: - - FTestSpecBase* const Spec; - const EAsyncExecution Execution; - const TFunction Predicate; - const FTimespan Timeout; - const bool bSkipIfErrored; - - FThreadSafeBool bDone; - FDateTime StartedRunning; - TFuture Future; - - public: - - FAsyncLatentCommand(FTestSpecBase* const InSpec, EAsyncExecution InExecution, TFunction InPredicate, const FTimespan& InTimeout, bool bInSkipIfErrored = false) - : Spec(InSpec) - , Execution(InExecution) - , Predicate(MoveTemp(InPredicate)) - , Timeout(InTimeout) - , bSkipIfErrored(bInSkipIfErrored) - , bDone(false) - {} - virtual ~FAsyncLatentCommand() {} - - virtual bool Update() override; - - private: - - void Done() - { - bDone = true; - } - - void Reset() - { - // Reset the done for the next potential run of this command - bDone = false; - Future = TFuture(); - } - }; - - struct FSpecIt - { - FString Description; - FString Id; - FString Filename; - int32 LineNumber; - TSharedRef Command; - - FSpecIt(FString InDescription, FString InId, FString InFilename, int32 InLineNumber, TSharedRef InCommand) - : Description(MoveTemp(InDescription)) - , Id(MoveTemp(InId)) - , Filename(InFilename) - , LineNumber(MoveTemp(InLineNumber)) - , Command(MoveTemp(InCommand)) - { } - }; - - struct FSpecDefinitionScope - { - FString Description; - - TArray> BeforeEach; - TArray> It; - TArray> AfterEach; - - TArray> Children; - }; - - struct FSpec - { - FString Id; - FString Description; - FString Filename; - int32 LineNumber; - TArray> Commands; - }; - - -protected: - - /* The timespan for how long a block should be allowed to execute before giving up and failing the test */ - FTimespan DefaultTimeout = FTimespan::FromSeconds(30); - - /* Whether or not BeforeEach and It blocks should skip execution if the test has already failed */ - bool bEnableSkipIfError = true; - -private: - - TArray Description; - - TMap> IdToSpecMap; - - TSharedPtr RootDefinitionScope; - - TArray> DefinitionScopeStack; - - bool bHasBeenDefined = false; - - int32 TestsRemaining = 0; - - // The context of the active test - FTestContext CurrentContext; - - -public: - - FTestSpecBase(const FString& InName, const bool bInComplexTask) - : FAutomationTestBase(InName, bInComplexTask) - , RootDefinitionScope(MakeShared()) - { - DefinitionScopeStack.Push(RootDefinitionScope.ToSharedRef()); - } - - virtual ~FTestSpecBase() {} - - virtual bool RunTest(const FString& InParameters) override; - - virtual bool IsStressTest() const { return false; } - virtual uint32 GetRequiredDeviceNum() const override { return 1; } - - virtual FString GetTestSourceFileName() const override; - virtual int32 GetTestSourceFileLine() const override; - virtual FString GetTestSourceFileName(const FString& InTestName) const override; - virtual int32 GetTestSourceFileLine(const FString& InTestName) const override; - - virtual void GetTests(TArray& OutBeautifiedNames, TArray& OutTestCommands) const override; - - - // BEGIN Disabled Scopes - void xDescribe(const FString& InDescription, TFunction DoWork) {} - - void xIt(const FString& InDescription, TFunction DoWork) {} - void xIt(const FString& InDescription, EAsyncExecution Execution, TFunction DoWork) {} - void xIt(const FString& InDescription, EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) {} - - void xLatentIt(const FString& InDescription, TFunction DoWork) {} - void xLatentIt(const FString& InDescription, const FTimespan& Timeout, TFunction DoWork) {} - void xLatentIt(const FString& InDescription, EAsyncExecution Execution, TFunction DoWork) {} - void xLatentIt(const FString& InDescription, EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) {} - - void xBeforeEach(TFunction DoWork) {} - void xBeforeEach(EAsyncExecution Execution, TFunction DoWork) {} - void xBeforeEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) {} - - void xLatentBeforeEach(TFunction DoWork) {} - void xLatentBeforeEach(const FTimespan& Timeout, TFunction DoWork) {} - void xLatentBeforeEach(EAsyncExecution Execution, TFunction DoWork) {} - void xLatentBeforeEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) {} - - void xAfterEach(TFunction DoWork) {} - void xAfterEach(EAsyncExecution Execution, TFunction DoWork) {} - void xAfterEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) {} - - void xLatentAfterEach(TFunction DoWork) {} - void xLatentAfterEach(const FTimespan& Timeout, TFunction DoWork) {} - void xLatentAfterEach(EAsyncExecution Execution, TFunction DoWork) {} - void xLatentAfterEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) {} - // END Disabled Scopes - - - // BEGIN Enabled Scopes - void Describe(const FString& InDescription, TFunction DoWork); - - void It(const FString& InDescription, TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - const TArray Stack = FPlatformStackWalk::GetStack(1, 1); - - PushDescription(InDescription); - CurrentScope->It.Push(MakeShared(GetDescription(), GetId(), Stack[0].Filename, Stack[0].LineNumber, MakeShared(this, DoWork, bEnableSkipIfError))); - PopDescription(InDescription); - } - - void It(const FString& InDescription, EAsyncExecution Execution, TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - const TArray Stack = FPlatformStackWalk::GetStack(1, 1); - - PushDescription(InDescription); - CurrentScope->It.Push(MakeShared(GetDescription(), GetId(), Stack[0].Filename, Stack[0].LineNumber, MakeShared(this, Execution, DoWork, DefaultTimeout, bEnableSkipIfError))); - PopDescription(InDescription); - } - - void It(const FString& InDescription, EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - const TArray Stack = FPlatformStackWalk::GetStack(1, 1); - - PushDescription(InDescription); - CurrentScope->It.Push(MakeShared(GetDescription(), GetId(), Stack[0].Filename, Stack[0].LineNumber, MakeShared(this, Execution, DoWork, Timeout, bEnableSkipIfError))); - PopDescription(InDescription); - } - - void LatentIt(const FString& InDescription, TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - const TArray Stack = FPlatformStackWalk::GetStack(1, 1); - - PushDescription(InDescription); - CurrentScope->It.Push(MakeShared(GetDescription(), GetId(), Stack[0].Filename, Stack[0].LineNumber, MakeShared(this, DoWork, DefaultTimeout, bEnableSkipIfError))); - PopDescription(InDescription); - } - - void LatentIt(const FString& InDescription, const FTimespan& Timeout, TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - const TArray Stack = FPlatformStackWalk::GetStack(1, 1); - - PushDescription(InDescription); - CurrentScope->It.Push(MakeShared(GetDescription(), GetId(), Stack[0].Filename, Stack[0].LineNumber, MakeShared(this, DoWork, Timeout, bEnableSkipIfError))); - PopDescription(InDescription); - } - - void LatentIt(const FString& InDescription, EAsyncExecution Execution, TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - const TArray Stack = FPlatformStackWalk::GetStack(1, 1); - - PushDescription(InDescription); - CurrentScope->It.Push(MakeShared(GetDescription(), GetId(), Stack[0].Filename, Stack[0].LineNumber, MakeShared(this, Execution, DoWork, DefaultTimeout, bEnableSkipIfError))); - PopDescription(InDescription); - } - - void LatentIt(const FString& InDescription, EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - const TArray Stack = FPlatformStackWalk::GetStack(1, 1); - - PushDescription(InDescription); - CurrentScope->It.Push(MakeShared(GetDescription(), GetId(), Stack[0].Filename, Stack[0].LineNumber, MakeShared(this, Execution, DoWork, Timeout, bEnableSkipIfError))); - PopDescription(InDescription); - } - - void BeforeEach(TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - CurrentScope->BeforeEach.Push(MakeShareable(new FSingleExecuteLatentCommand(this, DoWork, bEnableSkipIfError))); - } - - void BeforeEach(EAsyncExecution Execution, TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - CurrentScope->BeforeEach.Push(MakeShared(this, Execution, DoWork, DefaultTimeout, bEnableSkipIfError)); - } - - void BeforeEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - CurrentScope->BeforeEach.Push(MakeShared(this, Execution, DoWork, Timeout, bEnableSkipIfError)); - } - - void LatentBeforeEach(TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - CurrentScope->BeforeEach.Push(MakeShared(this, DoWork, DefaultTimeout, bEnableSkipIfError)); - } - - void LatentBeforeEach(const FTimespan& Timeout, TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - CurrentScope->BeforeEach.Push(MakeShared(this, DoWork, Timeout, bEnableSkipIfError)); - } - - void LatentBeforeEach(EAsyncExecution Execution, TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - CurrentScope->BeforeEach.Push(MakeShared(this, Execution, DoWork, DefaultTimeout, bEnableSkipIfError)); - } - - void LatentBeforeEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - CurrentScope->BeforeEach.Push(MakeShared(this, Execution, DoWork, Timeout, bEnableSkipIfError)); - } - - void AfterEach(TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - CurrentScope->AfterEach.Push(MakeShareable(new FSingleExecuteLatentCommand(this, DoWork))); - } - - void AfterEach(EAsyncExecution Execution, TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - CurrentScope->AfterEach.Push(MakeShared(this, Execution, DoWork, DefaultTimeout)); - } - - void AfterEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - CurrentScope->AfterEach.Push(MakeShared(this, Execution, DoWork, Timeout)); - } - - void LatentAfterEach(TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - CurrentScope->AfterEach.Push(MakeShared(this, DoWork, DefaultTimeout)); - } - - void LatentAfterEach(const FTimespan& Timeout, TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - CurrentScope->AfterEach.Push(MakeShared(this, DoWork, Timeout)); - } - - void LatentAfterEach(EAsyncExecution Execution, TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - CurrentScope->AfterEach.Push(MakeShared(this, Execution, DoWork, DefaultTimeout)); - } - - void LatentAfterEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) - { - const TSharedRef CurrentScope = DefinitionScopeStack.Last(); - CurrentScope->AfterEach.Push(MakeShared(this, Execution, DoWork, Timeout)); - } - - int32 GetNumTests() const { return IdToSpecMap.Num(); } - int32 GetTestsRemaining() const { return GetNumTests() - CurrentContext.GetId(); } - FTestContext GetCurrentContext() const { return CurrentContext; } - bool IsFirstTest() const { return CurrentContext.GetId() == 1; } - bool IsLastTest() const { return CurrentContext.GetId() == GetNumTests(); } - -protected: - - void EnsureDefinitions() const; - - virtual void RunDefine() - { - PreDefine(); - Define(); - PostDefine(); - } - virtual void PreDefine(); - virtual void Define() = 0; - virtual void PostDefine(); - - void BakeDefinitions(); - - void Redefine(); - -private: - - void PushDescription(const FString& InDescription) - { - Description.Add(InDescription); - } - - void PopDescription(const FString& InDescription) - { - Description.RemoveAt(Description.Num() - 1); - } - - FString GetDescription() const; - - FString GetId() const; -}; - -inline void FTestSpecBase::EnsureDefinitions() const -{ - if (!bHasBeenDefined) - { - const_cast(this)->RunDefine(); - const_cast(this)->BakeDefinitions(); - } -} - - -inline FString FTestSpecBase::GetTestSourceFileName() const -{ - return FAutomationTestBase::GetTestSourceFileName(); -} - -inline int32 FTestSpecBase::GetTestSourceFileLine() const -{ - return FAutomationTestBase::GetTestSourceFileLine(); -} diff --git a/Source/Automatron/Public/TestSpec.h b/Source/Automatron/Public/TestSpec.h deleted file mode 100644 index eeb8510..0000000 --- a/Source/Automatron/Public/TestSpec.h +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright 2020 Splash Damage, Ltd. - All Rights Reserved. - -#pragma once - -#include -#include -#include -#include -#include - -#include "Base/TestSpecBase.h" - - -DECLARE_DELEGATE_OneParam(FSpecBaseOnWorldReady, UWorld*); - -// Initializes an spec instance at global execution time -// and registers it to the system -template -struct TSpecRegister -{ - static TSpecRegister Register; - - T Instance; - - TSpecRegister() : Instance{} - { - Instance.Setup(); - } -}; - -template -TSpecRegister TSpecRegister::Register{}; - - -class AUTOMATRON_API FTestSpec : public FTestSpecBase -{ -public: - - // Should a world be initialized? - bool bUseWorld = true; - - // If true the world used for testing will be reused for all tests - bool bReuseWorldForAllTests = true; - - // If true and in editor, a PIE instance will be used to test - bool bCanUsePIEWorld = true; - -private: - - FString ClassName; - FString PrettyName; - FString FileName; - int32 LineNumber = -1; - uint32 Flags = 0; - - bool bInitializedWorld = false; -#if WITH_EDITOR - bool bInitializedPIE = false; -#endif - - TWeakObjectPtr World; - - -public: - - FTestSpec() : FTestSpecBase("", false) {} - - virtual FString GetTestSourceFileName() const override { return FileName; } - virtual int32 GetTestSourceFileLine() const override { return LineNumber; } - virtual uint32 GetTestFlags() const override { return Flags; } - - const FString& GetClassName() const { return ClassName; } - const FString& GetPrettyName() const { return PrettyName; } - -protected: - - virtual FString GetBeautifiedTestName() const override { return PrettyName; } - - template - void Setup(FString&& InName, FString&& InPrettyName, FString&& InFileName, int32 InLineNumber); - - // Used to indicate a test is pending to be implemented. - void TestNotImplemented() - { - AddWarning(TEXT("Test not implemented"), 1); - } - - virtual void PreDefine() override; - virtual void PostDefine() override; - - void PrepareTestWorld(FSpecBaseOnWorldReady OnWorldReady); - void ReleaseTestWorld(); - - UWorld* GetWorld() const { return World.Get(); } - -private: - - void Reregister(const FString& NewName) - { - FAutomationTestFramework::Get().UnregisterAutomationTest(TestName); - TestName = NewName; - FAutomationTestFramework::Get().RegisterAutomationTest(TestName, this); - } - - // Finds the first available game world (Standalone or PIE) - static UWorld* FindGameWorld(); -}; - - -template -inline void FTestSpec::Setup(FString&& InName, FString&& InPrettyName, FString&& InFileName, int32 InLineNumber) -{ - static_assert(TFlags & EAutomationTestFlags::ApplicationContextMask, "AutomationTest has no application flag. It shouldn't run. See AutomationTest.h."); \ - static_assert(((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::SmokeFilter) || - ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::EngineFilter) || - ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::ProductFilter) || - ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::PerfFilter) || - ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::StressFilter) || - ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::NegativeFilter), - "All AutomationTests must have exactly 1 filter type specified. See AutomationTest.h."); - - ClassName = InName; - PrettyName = MoveTemp(InPrettyName); - FileName = MoveTemp(InFileName); - LineNumber = InLineNumber; - Flags = TFlags; - - Reregister(InName); -} diff --git a/Source/AutomatronTest/AutomatronTest.Build.cs b/Source/AutomatronTest/AutomatronTest.Build.cs index 88069d0..8462101 100644 --- a/Source/AutomatronTest/AutomatronTest.Build.cs +++ b/Source/AutomatronTest/AutomatronTest.Build.cs @@ -12,12 +12,17 @@ public AutomatronTest(ReadOnlyTargetRules Target) : base(Target) PublicDependencyModuleNames.AddRange(new string[] { "Core", - "Automatron", }); PrivateDependencyModuleNames.AddRange(new string[] { "CoreUObject", - "Engine" + "Engine", + "Automatron" }); + + if (Target.bBuildEditor) + { + PrivateDependencyModuleNames.Add("UnrealEd"); + } } } diff --git a/Source/AutomatronTest/Private/Automatron.spec.cpp b/Source/AutomatronTest/Private/Automatron.spec.cpp index 5b43632..ee043ee 100644 --- a/Source/AutomatronTest/Private/Automatron.spec.cpp +++ b/Source/AutomatronTest/Private/Automatron.spec.cpp @@ -8,12 +8,12 @@ #if WITH_DEV_AUTOMATION_TESTS -SPEC(FAutomatronSpec, FTestSpec, "Automatron", +SPEC(FAutomatronSpec, Automatron::FTestSpec, "Automatron", EAutomationTestFlags::EngineFilter | EAutomationTestFlags::HighPriority | EAutomationTestFlags::EditorContext) { - It("Can run a test", [this]() { + It("Can run a test", []() { // Succeed }); } From 40ccf68e99332f3fa9a32228e0d9dbb021c3eec6 Mon Sep 17 00:00:00 2001 From: muit Date: Tue, 8 Sep 2020 19:44:18 +0200 Subject: [PATCH 2/7] Formatted according to UE4 Standards, Improved world creation & ticking - Added clang-format file to define standard - Worlds can be created in standalone and their tick simulated --- .clang-format | 142 ++ Source/Automatron/Automatron.Build.cs | 7 +- Source/Automatron/Public/Automatron.h | 1621 +++++++++-------- Source/AutomatronTest/AutomatronTest.Build.cs | 5 +- 4 files changed, 1057 insertions(+), 718 deletions(-) create mode 100644 .clang-format diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..c6ef6a0 --- /dev/null +++ b/.clang-format @@ -0,0 +1,142 @@ +--- +Language: Cpp +AccessModifierOffset: -4 +AlignAfterOpenBracket: DontAlign +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Left +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: None +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: true +BinPackParameters: true +BreakBeforeBinaryOperators: None +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeComma +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + AfterControlStatement: Always + BeforeElse: true + BeforeCatch: true + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false +ColumnLimit: 110 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +AllowAllConstructorInitializersOnNextLine: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +FixNamespaceComments: true +ForEachMacros: + - for +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '.*(PCH).*' + Priority: -1 + - Regex: '".*"' + Priority: 1 + - Regex: '^<.*\.(h)>' + Priority: 3 + - Regex: '^<.*>' + Priority: 4 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseLabels: true +IndentPPDirectives: AfterHash +IndentWidth: 4 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 2 +NamespaceIndentation: All +ObjCBinPackProtocolList: Never +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +RawStringFormats: + - Language: Cpp + Delimiters: + - cc + - CC + - cpp + - Cpp + - CPP + - 'c++' + - 'C++' + CanonicalDelimiter: '' + BasedOnStyle: google + - Language: TextProto + Delimiters: + - pb + - PB + - proto + - PROTO + EnclosingFunctions: + - EqualsProto + - EquivToProto + - PARSE_PARTIAL_TEXT_PROTO + - PARSE_TEST_PROTO + - PARSE_TEXT_PROTO + - ParseTextOrDie + - ParseTextProtoOrDie + CanonicalDelimiter: '' +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: true +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 4 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 4 +UseTab: Always +... diff --git a/Source/Automatron/Automatron.Build.cs b/Source/Automatron/Automatron.Build.cs index b699bd3..b63f050 100644 --- a/Source/Automatron/Automatron.Build.cs +++ b/Source/Automatron/Automatron.Build.cs @@ -8,14 +8,15 @@ public Automatron(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; bEnforceIWYU = true; - bLegacyPublicIncludePaths = false; + bLegacyPublicIncludePaths = false; - PublicDependencyModuleNames.AddRange(new string[] + PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", - "FunctionalTesting" + "FunctionalTesting", + "EngineSettings" }); if (Target.bBuildEditor) diff --git a/Source/Automatron/Public/Automatron.h b/Source/Automatron/Public/Automatron.h index c39a406..9d53339 100644 --- a/Source/Automatron/Public/Automatron.h +++ b/Source/Automatron/Public/Automatron.h @@ -1,52 +1,59 @@ // AUTOMATRON // Version: 1.2 +// Repository: https://github.com/splash-damage/automatron // Can be used as a header-only library or implemented as a module -//BSD 3-Clause License +// BSD 3-Clause License // -//Copyright (c) 2020, Splash Damage -//All rights reserved. +// Copyright (c) 2020, Splash Damage +// All rights reserved. // -//Redistribution and use in source and binary forms, with or without -//modification, are permitted provided that the following conditions are met: +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: // -//1. Redistributions of source code must retain the above copyright notice, this +// 1. Redistributions of source code must retain the above copyright notice, +// this // list of conditions and the following disclaimer. // -//2. Redistributions in binary form must reproduce the above copyright notice, +// 2. Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // -//3. Neither the name of the copyright holder nor the names of its +// 3. Neither the name of the copyright holder nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // -//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -//AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -//IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -//FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -//DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -//SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -//CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -//OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -//OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. #pragma once #include +#include #include +#include +#include #include #include +#ifndef WITH_DEV_AUTOMATION_TESTS +# define WITH_DEV_AUTOMATION_TESTS 1 +#endif #if WITH_EDITOR -# include -# include +# include +# include #endif - //////////////////////////////////////////////////////////////// // DEFINITIONS @@ -55,12 +62,19 @@ namespace Automatron { class FTestSpecBase; + struct FTestWorldSettings + { + TSubclassOf GameMode = AGameModeBase::StaticClass(); + + bool bShouldTick = false; + }; + namespace Spec { ///////////////////////////////////////////////////// // Initializes an spec instance at global execution time // and registers it to the system - template + template struct TRegister { static TRegister Register; @@ -73,27 +87,39 @@ namespace Automatron } }; - ///////////////////////////////////////////////////// // Represents an instance of a test struct FContext { private: - int32 Id = 0; public: FContext(int32 Id = 0) : Id(Id) {} - FContext NextContext() const { return { Id + 1 }; } - int32 GetId() const { return Id; } + FContext NextContext() const + { + return {Id + 1}; + } + int32 GetId() const + { + return Id; + } - friend uint32 GetTypeHash(const FContext& Item) { return Item.Id; } - bool operator==(const FContext& Other) const { return Id == Other.Id; } - operator bool() const { return Id > 0; } + friend uint32 GetTypeHash(const FContext& Item) + { + return Item.Id; + } + bool operator==(const FContext& Other) const + { + return Id == Other.Id; + } + operator bool() const + { + return Id > 0; + } }; - struct FIt { FString Description; @@ -102,32 +128,33 @@ namespace Automatron int32 LineNumber; TSharedRef Command; - FIt(FString InDescription, FString InId, FString InFilename, int32 InLineNumber, TSharedRef InCommand) + FIt(FString InDescription, FString InId, FString InFilename, int32 InLineNumber, + TSharedRef InCommand) : Description(MoveTemp(InDescription)) , Id(MoveTemp(InId)) , Filename(MoveTemp(InFilename)) , LineNumber(MoveTemp(InLineNumber)) , Command(MoveTemp(InCommand)) - { } + {} }; - }; + }; // namespace Spec namespace Commands { class FSingleExecuteLatent : public IAutomationLatentCommand { private: - const FTestSpecBase& Spec; const TFunction Predicate; const bool bSkipIfErrored = false; public: - FSingleExecuteLatent(const FTestSpecBase& InSpec, TFunction InPredicate, bool bInSkipIfErrored = false) + FSingleExecuteLatent( + const FTestSpecBase& InSpec, TFunction InPredicate, bool bInSkipIfErrored = false) : Spec(InSpec) , Predicate(MoveTemp(InPredicate)) , bSkipIfErrored(bInSkipIfErrored) - { } + {} virtual ~FSingleExecuteLatent() {} virtual bool Update() override; @@ -136,7 +163,6 @@ namespace Automatron class FUntilDoneLatent : public IAutomationLatentCommand { private: - FTestSpecBase& Spec; const TFunction Predicate; const FTimespan Timeout; @@ -147,8 +173,8 @@ namespace Automatron FDateTime StartedRunning; public: - - FUntilDoneLatent(FTestSpecBase& InSpec, TFunction InPredicate, const FTimespan& InTimeout, bool bInSkipIfErrored = false) + FUntilDoneLatent(FTestSpecBase& InSpec, TFunction InPredicate, + const FTimespan& InTimeout, bool bInSkipIfErrored = false) : Spec(InSpec) , Predicate(MoveTemp(InPredicate)) , Timeout(InTimeout) @@ -159,15 +185,16 @@ namespace Automatron virtual bool Update() override; private: - - void Done() { bDone = true; } + void Done() + { + bDone = true; + } void Reset(); }; class FAsyncUntilDoneLatent : public IAutomationLatentCommand { private: - FTestSpecBase& Spec; const EAsyncExecution Execution; const TFunction Predicate; @@ -179,8 +206,9 @@ namespace Automatron FDateTime StartedRunning; public: - - FAsyncUntilDoneLatent(FTestSpecBase& InSpec, EAsyncExecution InExecution, TFunction InPredicate, const FTimespan& InTimeout, bool bInSkipIfErrored = false) + FAsyncUntilDoneLatent(FTestSpecBase& InSpec, EAsyncExecution InExecution, + TFunction InPredicate, const FTimespan& InTimeout, + bool bInSkipIfErrored = false) : Spec(InSpec) , Execution(InExecution) , Predicate(MoveTemp(InPredicate)) @@ -192,15 +220,16 @@ namespace Automatron virtual bool Update() override; private: - - void Done() { bDone = true; } + void Done() + { + bDone = true; + } void Reset(); }; class FAsyncLatent : public IAutomationLatentCommand { private: - FTestSpecBase& Spec; const EAsyncExecution Execution; const TFunction Predicate; @@ -212,8 +241,8 @@ namespace Automatron TFuture Future; public: - - FAsyncLatent(FTestSpecBase& InSpec, EAsyncExecution InExecution, TFunction InPredicate, const FTimespan& InTimeout, bool bInSkipIfErrored = false) + FAsyncLatent(FTestSpecBase& InSpec, EAsyncExecution InExecution, TFunction InPredicate, + const FTimespan& InTimeout, bool bInSkipIfErrored = false) : Spec(InSpec) , Execution(InExecution) , Predicate(MoveTemp(InPredicate)) @@ -225,16 +254,15 @@ namespace Automatron virtual bool Update() override; private: - - void Done() { bDone = true; } + void Done() + { + bDone = true; + } void Reset(); }; - }; + }; // namespace Commands - - class FTestSpecBase - : public FAutomationTestBase - , public TSharedFromThis + class FTestSpecBase : public FAutomationTestBase, public TSharedFromThis { private: struct FSpecDefinitionScope @@ -257,17 +285,16 @@ namespace Automatron TArray> Commands; }; - protected: - - /* The timespan for how long a block should be allowed to execute before giving up and failing the test */ + /* The timespan for how long a block should be allowed to execute before + * giving up and failing the test */ FTimespan DefaultTimeout = FTimespan::FromSeconds(30); - /* Whether or not BeforeEach and It blocks should skip execution if the test has already failed */ + /* Whether or not BeforeEach and It blocks should skip execution if the test + * has already failed */ bool bEnableSkipIfError = true; private: - TArray Description; TMap> IdToSpecMap; @@ -283,9 +310,7 @@ namespace Automatron // The context of the active test Spec::FContext CurrentContext; - public: - FTestSpecBase(const FString& InName, const bool bInComplexTask) : FAutomationTestBase(InName, bInComplexTask) , RootDefinitionScope(MakeShared()) @@ -297,16 +322,22 @@ namespace Automatron virtual bool RunTest(const FString& InParameters) override; - virtual bool IsStressTest() const { return false; } - virtual uint32 GetRequiredDeviceNum() const override { return 1; } + virtual bool IsStressTest() const + { + return false; + } + virtual uint32 GetRequiredDeviceNum() const override + { + return 1; + } virtual FString GetTestSourceFileName() const override; virtual int32 GetTestSourceFileLine() const override; virtual FString GetTestSourceFileName(const FString& InTestName) const override; virtual int32 GetTestSourceFileLine(const FString& InTestName) const override; - virtual void GetTests(TArray& OutBeautifiedNames, TArray& OutTestCommands) const override; - + virtual void GetTests( + TArray& OutBeautifiedNames, TArray& OutTestCommands) const override; // BEGIN Enabled Scopes void Describe(const FString& InDescription, TFunction DoWork); @@ -318,18 +349,22 @@ namespace Automatron PushDescription(InDescription); auto Command = MakeShared(*this, DoWork, bEnableSkipIfError); - CurrentScope->It.Push(MakeShared(GetDescription(), GetId(), Stack[0].Filename, Stack[0].LineNumber, Command)); + CurrentScope->It.Push(MakeShared( + GetDescription(), GetId(), Stack[0].Filename, Stack[0].LineNumber, Command)); PopDescription(InDescription); } - void It(const FString& InDescription, EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) + void It(const FString& InDescription, EAsyncExecution Execution, const FTimespan& Timeout, + TFunction DoWork) { const TSharedRef CurrentScope = DefinitionScopeStack.Last(); const TArray Stack = FPlatformStackWalk::GetStack(1, 1); PushDescription(InDescription); - auto Command = MakeShared(*this, Execution, DoWork, Timeout, bEnableSkipIfError); - CurrentScope->It.Push(MakeShared(GetDescription(), GetId(), Stack[0].Filename, Stack[0].LineNumber, Command)); + auto Command = + MakeShared(*this, Execution, DoWork, Timeout, bEnableSkipIfError); + CurrentScope->It.Push(MakeShared( + GetDescription(), GetId(), Stack[0].Filename, Stack[0].LineNumber, Command)); PopDescription(InDescription); } @@ -338,14 +373,16 @@ namespace Automatron It(InDescription, Execution, DefaultTimeout, DoWork); } - void LatentIt(const FString& InDescription, const FTimespan& Timeout, TFunction DoWork) + void LatentIt(const FString& InDescription, const FTimespan& Timeout, + TFunction DoWork) { const TSharedRef CurrentScope = DefinitionScopeStack.Last(); const TArray Stack = FPlatformStackWalk::GetStack(1, 1); PushDescription(InDescription); auto Command = MakeShared(*this, DoWork, Timeout, bEnableSkipIfError); - CurrentScope->It.Push(MakeShared(GetDescription(), GetId(), Stack[0].Filename, Stack[0].LineNumber, Command)); + CurrentScope->It.Push(MakeShared( + GetDescription(), GetId(), Stack[0].Filename, Stack[0].LineNumber, Command)); PopDescription(InDescription); } @@ -354,18 +391,22 @@ namespace Automatron LatentIt(InDescription, DefaultTimeout, DoWork); } - void LatentIt(const FString& InDescription, EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) + void LatentIt(const FString& InDescription, EAsyncExecution Execution, const FTimespan& Timeout, + TFunction DoWork) { const TSharedRef CurrentScope = DefinitionScopeStack.Last(); const TArray Stack = FPlatformStackWalk::GetStack(1, 1); PushDescription(InDescription); - auto Command = MakeShared(*this, Execution, DoWork, Timeout, bEnableSkipIfError); - CurrentScope->It.Push(MakeShared(GetDescription(), GetId(), Stack[0].Filename, Stack[0].LineNumber, Command)); + auto Command = MakeShared( + *this, Execution, DoWork, Timeout, bEnableSkipIfError); + CurrentScope->It.Push(MakeShared( + GetDescription(), GetId(), Stack[0].Filename, Stack[0].LineNumber, Command)); PopDescription(InDescription); } - void LatentIt(const FString& InDescription, EAsyncExecution Execution, TFunction DoWork) + void LatentIt(const FString& InDescription, EAsyncExecution Execution, + TFunction DoWork) { LatentIt(InDescription, Execution, DefaultTimeout, DoWork); } @@ -373,15 +414,13 @@ namespace Automatron void BeforeEach(TFunction DoWork) { DefinitionScopeStack.Last()->BeforeEach.Push( - MakeShared(*this, DoWork, bEnableSkipIfError) - ); + MakeShared(*this, DoWork, bEnableSkipIfError)); } void BeforeEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) { DefinitionScopeStack.Last()->BeforeEach.Push( - MakeShared(*this, Execution, DoWork, Timeout, bEnableSkipIfError) - ); + MakeShared(*this, Execution, DoWork, Timeout, bEnableSkipIfError)); } void BeforeEach(EAsyncExecution Execution, TFunction DoWork) @@ -392,8 +431,7 @@ namespace Automatron void LatentBeforeEach(const FTimespan& Timeout, TFunction DoWork) { DefinitionScopeStack.Last()->BeforeEach.Push( - MakeShared(*this, DoWork, Timeout, bEnableSkipIfError) - ); + MakeShared(*this, DoWork, Timeout, bEnableSkipIfError)); } void LatentBeforeEach(TFunction DoWork) @@ -401,11 +439,11 @@ namespace Automatron LatentBeforeEach(DefaultTimeout, DoWork); } - void LatentBeforeEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) + void LatentBeforeEach( + EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) { - DefinitionScopeStack.Last()->BeforeEach.Push( - MakeShared(*this, Execution, DoWork, Timeout, bEnableSkipIfError) - ); + DefinitionScopeStack.Last()->BeforeEach.Push(MakeShared( + *this, Execution, DoWork, Timeout, bEnableSkipIfError)); } void LatentBeforeEach(EAsyncExecution Execution, TFunction DoWork) @@ -416,15 +454,13 @@ namespace Automatron void AfterEach(TFunction DoWork) { DefinitionScopeStack.Last()->AfterEach.Push( - MakeShared(*this, DoWork) - ); + MakeShared(*this, DoWork)); } void AfterEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) { DefinitionScopeStack.Last()->AfterEach.Push( - MakeShared(*this, Execution, DoWork, Timeout) - ); + MakeShared(*this, Execution, DoWork, Timeout)); } void AfterEach(EAsyncExecution Execution, TFunction DoWork) @@ -435,8 +471,7 @@ namespace Automatron void LatentAfterEach(const FTimespan& Timeout, TFunction DoWork) { DefinitionScopeStack.Last()->AfterEach.Push( - MakeShared(*this, DoWork, Timeout) - ); + MakeShared(*this, DoWork, Timeout)); } void LatentAfterEach(TFunction DoWork) @@ -444,11 +479,11 @@ namespace Automatron LatentAfterEach(DefaultTimeout, DoWork); } - void LatentAfterEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) + void LatentAfterEach( + EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) { - DefinitionScopeStack.Last() ->AfterEach.Push( - MakeShared(*this, Execution, DoWork, Timeout) - ); + DefinitionScopeStack.Last()->AfterEach.Push( + MakeShared(*this, Execution, DoWork, Timeout)); } void LatentAfterEach(EAsyncExecution Execution, TFunction DoWork) @@ -457,18 +492,25 @@ namespace Automatron } // END Enabled Scopes - // BEGIN Disabled Scopes void xDescribe(const FString& InDescription, TFunction DoWork) {} void xIt(const FString& InDescription, TFunction DoWork) {} void xIt(const FString& InDescription, EAsyncExecution Execution, TFunction DoWork) {} - void xIt(const FString& InDescription, EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) {} + void xIt(const FString& InDescription, EAsyncExecution Execution, const FTimespan& Timeout, + TFunction DoWork) + {} void xLatentIt(const FString& InDescription, TFunction DoWork) {} - void xLatentIt(const FString& InDescription, const FTimespan& Timeout, TFunction DoWork) {} - void xLatentIt(const FString& InDescription, EAsyncExecution Execution, TFunction DoWork) {} - void xLatentIt(const FString& InDescription, EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) {} + void xLatentIt(const FString& InDescription, const FTimespan& Timeout, + TFunction DoWork) + {} + void xLatentIt(const FString& InDescription, EAsyncExecution Execution, + TFunction DoWork) + {} + void xLatentIt(const FString& InDescription, EAsyncExecution Execution, const FTimespan& Timeout, + TFunction DoWork) + {} void xBeforeEach(TFunction DoWork) {} void xBeforeEach(EAsyncExecution Execution, TFunction DoWork) {} @@ -477,7 +519,9 @@ namespace Automatron void xLatentBeforeEach(TFunction DoWork) {} void xLatentBeforeEach(const FTimespan& Timeout, TFunction DoWork) {} void xLatentBeforeEach(EAsyncExecution Execution, TFunction DoWork) {} - void xLatentBeforeEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) {} + void xLatentBeforeEach( + EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) + {} void xAfterEach(TFunction DoWork) {} void xAfterEach(EAsyncExecution Execution, TFunction DoWork) {} @@ -486,18 +530,33 @@ namespace Automatron void xLatentAfterEach(TFunction DoWork) {} void xLatentAfterEach(const FTimespan& Timeout, TFunction DoWork) {} void xLatentAfterEach(EAsyncExecution Execution, TFunction DoWork) {} - void xLatentAfterEach(EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) {} + void xLatentAfterEach( + EAsyncExecution Execution, const FTimespan& Timeout, TFunction DoWork) + {} // END Disabled Scopes - - int32 GetNumTests() const { return IdToSpecMap.Num(); } - int32 GetTestsRemaining() const { return GetNumTests() - CurrentContext.GetId(); } - Spec::FContext GetCurrentContext() const { return CurrentContext; } - bool IsFirstTest() const { return CurrentContext.GetId() == 1; } - bool IsLastTest() const { return CurrentContext.GetId() == GetNumTests(); } + int32 GetNumTests() const + { + return IdToSpecMap.Num(); + } + int32 GetTestsRemaining() const + { + return GetNumTests() - CurrentContext.GetId(); + } + Spec::FContext GetCurrentContext() const + { + return CurrentContext; + } + bool IsFirstTest() const + { + return CurrentContext.GetId() == 1; + } + bool IsLastTest() const + { + return CurrentContext.GetId() == GetNumTests(); + } protected: - void EnsureDefinitions() const; virtual void RunDefine() @@ -515,7 +574,6 @@ namespace Automatron void Redefine(); private: - void PushDescription(const FString& InDescription) { Description.Add(InDescription); @@ -531,13 +589,9 @@ namespace Automatron FString GetId() const; }; - - DECLARE_DELEGATE_OneParam(FSpecBaseOnWorldReady, UWorld*); - class FTestSpec : public FTestSpecBase { public: - // Should a world be initialized? bool bUseWorld = true; @@ -547,8 +601,9 @@ namespace Automatron // If true and in editor, a PIE instance will be used to test bool bCanUsePIEWorld = true; - private: + FTestWorldSettings DefaultWorldSettings; + private: FString ClassName; FString PrettyName; FString FileName; @@ -556,29 +611,44 @@ namespace Automatron uint32 Flags = 0; bool bInitializedWorld = false; - #if WITH_EDITOR +# if WITH_EDITOR bool bInitializedPIE = false; - #endif - - TWeakObjectPtr World; +# endif + TWeakObjectPtr MainWorld; public: - FTestSpec() : FTestSpecBase("", false) {} - virtual FString GetTestSourceFileName() const override { return FileName; } - virtual int32 GetTestSourceFileLine() const override { return LineNumber; } - virtual uint32 GetTestFlags() const override { return Flags; } + virtual FString GetTestSourceFileName() const override + { + return FileName; + } + virtual int32 GetTestSourceFileLine() const override + { + return LineNumber; + } + virtual uint32 GetTestFlags() const override + { + return Flags; + } - const FString& GetClassName() const { return ClassName; } - const FString& GetPrettyName() const { return PrettyName; } + const FString& GetClassName() const + { + return ClassName; + } + const FString& GetPrettyName() const + { + return PrettyName; + } protected: + virtual FString GetBeautifiedTestName() const override + { + return PrettyName; + } - virtual FString GetBeautifiedTestName() const override { return PrettyName; } - - template + template void Setup(FString&& InName, FString&& InPrettyName, FString&& InFileName, int32 InLineNumber); // Used to indicate a test is pending to be implemented. @@ -590,13 +660,27 @@ namespace Automatron virtual void PreDefine() override; virtual void PostDefine() override; - void PrepareTestWorld(FSpecBaseOnWorldReady OnWorldReady); - void ReleaseTestWorld(); + void PrepareTestWorld(TFunction OnWorldReady); - UWorld* GetWorld() const { return World.Get(); } + void ReleaseTestWorld(UWorld* World); - private: + // Creates an empty world from scratch + // @return world that was created + UWorld* CreateWorld(FTestWorldSettings Settings = {}); + + void TickWorldUntil(UWorld* World, bool bUseRealtime, TFunction Delegate); + void TickWorld(UWorld* World, float Duration, bool bUseRealtime = false); + + UGameInstance* CreateGameInstance(UObject* Context); + bool DestroyWorld(UWorld* World); + + UWorld* GetMainWorld() const + { + return MainWorld.Get(); + } + + private: void Reregister(const FString& NewName) { FAutomationTestFramework::Get().UnregisterAutomationTest(TestName); @@ -606,53 +690,58 @@ namespace Automatron // Finds the first available game world (Standalone or PIE) static UWorld* FindGameWorld(); + + static bool SetGameMode(UWorld* World, FTestWorldSettings& Settings); }; -} -#endif //WITH_DEV_AUTOMATION_TESTS + namespace Spec + { + template + TRegister TRegister::Register{}; + } +} // namespace Automatron +#endif // WITH_DEV_AUTOMATION_TESTS //////////////////////////////////////////////////////////////// // GENERATION MACROS #if WITH_DEV_AUTOMATION_TESTS - -#define GENERATE_SPEC(TClass, PrettyName, TFlags) \ - GENERATE_SPEC_PRIVATE(TClass, PrettyName, TFlags, __FILE__, __LINE__) - -#define GENERATE_SPEC_PRIVATE(TClass, PrettyName, TFlags, FileName, LineNumber) \ -private: \ - void Setup() \ - { \ - FTestSpec::Setup(TEXT(#TClass), TEXT(PrettyName), FileName, LineNumber); \ - } \ - static Automatron::Spec::TRegister& __meta_register() \ - { \ - return Automatron::Spec::TRegister::Register; \ - } \ - friend Automatron::Spec::TRegister; \ -\ - virtual void Define() override - -#define SPEC(TClass, TParent, PrettyName, TFlags) \ -class TClass : public TParent \ -{ \ - GENERATE_SPEC(TClass, PrettyName, TFlags); \ -}; \ -void TClass::Define() - -#else //WITH_DEV_AUTOMATION_TESTS - -#define GENERATE_SPEC(TClass, PrettyName, TFlags) -#define SPEC(TClass, TParent, PrettyName, TFlags) \ -class TClass \ -{ \ - void Define(); \ -}; \ -void TClass::Define() - -#endif //WITH_DEV_AUTOMATION_TESTS - +# define GENERATE_SPEC(TClass, PrettyName, TFlags) \ + GENERATE_SPEC_PRIVATE(TClass, PrettyName, TFlags, __FILE__, __LINE__) + +# define GENERATE_SPEC_PRIVATE(TClass, PrettyName, TFlags, FileName, LineNumber) \ + private: \ + void Setup() \ + { \ + FTestSpec::Setup(TEXT(#TClass), TEXT(PrettyName), FileName, LineNumber); \ + } \ + static Automatron::Spec::TRegister& __meta_register() \ + { \ + return Automatron::Spec::TRegister::Register; \ + } \ + friend Automatron::Spec::TRegister; \ + \ + virtual void Define() override + +# define SPEC(TClass, TParent, PrettyName, TFlags) \ + class TClass : public TParent \ + { \ + GENERATE_SPEC(TClass, PrettyName, TFlags); \ + }; \ + void TClass::Define() + +#else // WITH_DEV_AUTOMATION_TESTS + +# define GENERATE_SPEC(TClass, PrettyName, TFlags) +# define SPEC(TClass, TParent, PrettyName, TFlags) \ + class TClass \ + { \ + void Define(); \ + }; \ + void TClass::Define() + +#endif // WITH_DEV_AUTOMATION_TESTS //////////////////////////////////////////////////////////////// // DECLARATIONS @@ -660,51 +749,47 @@ void TClass::Define() #if WITH_DEV_AUTOMATION_TESTS namespace Automatron { - namespace Spec - { - } - namespace Commands { inline bool FSingleExecuteLatent::Update() - { - if (bSkipIfErrored && Spec.HasAnyErrors()) - { - return true; - } - - Predicate(); - return true; - } - - inline bool FUntilDoneLatent::Update() - { - if (!bIsRunning) - { - if (bSkipIfErrored && Spec.HasAnyErrors()) - { - return true; - } - - Predicate(FDoneDelegate::CreateSP(this, &FUntilDoneLatent::Done)); - bIsRunning = true; - StartedRunning = FDateTime::UtcNow(); - } - - if (bDone) - { - Reset(); - return true; - } - else if (FDateTime::UtcNow() >= StartedRunning + Timeout) - { - Reset(); - Spec.AddError(TEXT("Latent command timed out."), 0); - return true; - } - - return false; - } + { + if (bSkipIfErrored && Spec.HasAnyErrors()) + { + return true; + } + + Predicate(); + return true; + } + + inline bool FUntilDoneLatent::Update() + { + if (!bIsRunning) + { + if (bSkipIfErrored && Spec.HasAnyErrors()) + { + return true; + } + + Predicate(FDoneDelegate::CreateSP(this, &FUntilDoneLatent::Done)); + bIsRunning = true; + StartedRunning = FDateTime::UtcNow(); + } + + if (bDone) + { + Reset(); + return true; + } + else if (FDateTime::UtcNow() >= StartedRunning + Timeout) + { + Reset(); + Spec.AddError(TEXT("Latent command timed out."), 0); + return true; + } + + return false; + } inline void FUntilDoneLatent::Reset() { @@ -713,35 +798,35 @@ namespace Automatron bIsRunning = false; } - inline bool FAsyncUntilDoneLatent::Update() - { - if (!Future.IsValid()) - { - if (bSkipIfErrored && Spec.HasAnyErrors()) - { - return true; - } - - Future = Async(Execution, [this]() { - Predicate(FDoneDelegate::CreateRaw(this, &FAsyncUntilDoneLatent::Done)); - }); - - StartedRunning = FDateTime::UtcNow(); - } - - if (bDone) - { - Reset(); - return true; - } - else if (FDateTime::UtcNow() >= StartedRunning + Timeout) - { - Reset(); - Spec.AddError(TEXT("Latent command timed out."), 0); - return true; - } - return false; - } + inline bool FAsyncUntilDoneLatent::Update() + { + if (!Future.IsValid()) + { + if (bSkipIfErrored && Spec.HasAnyErrors()) + { + return true; + } + + Future = Async(Execution, [this]() { + Predicate(FDoneDelegate::CreateRaw(this, &FAsyncUntilDoneLatent::Done)); + }); + + StartedRunning = FDateTime::UtcNow(); + } + + if (bDone) + { + Reset(); + return true; + } + else if (FDateTime::UtcNow() >= StartedRunning + Timeout) + { + Reset(); + Spec.AddError(TEXT("Latent command timed out."), 0); + return true; + } + return false; + } inline void FAsyncUntilDoneLatent::Reset() { @@ -750,37 +835,37 @@ namespace Automatron Future = TFuture(); } - inline bool FAsyncLatent::Update() - { - if (!Future.IsValid()) - { - if (bSkipIfErrored && Spec.HasAnyErrors()) - { - return true; - } - - Future = Async(Execution, [this]() { - Predicate(); - bDone = true; - }); - - StartedRunning = FDateTime::UtcNow(); - } - - if (bDone) - { - Reset(); - return true; - } - else if (FDateTime::UtcNow() >= StartedRunning + Timeout) - { - Reset(); - Spec.AddError(TEXT("Latent command timed out."), 0); - return true; - } - - return false; - } + inline bool FAsyncLatent::Update() + { + if (!Future.IsValid()) + { + if (bSkipIfErrored && Spec.HasAnyErrors()) + { + return true; + } + + Future = Async(Execution, [this]() { + Predicate(); + bDone = true; + }); + + StartedRunning = FDateTime::UtcNow(); + } + + if (bDone) + { + Reset(); + return true; + } + else if (FDateTime::UtcNow() >= StartedRunning + Timeout) + { + Reset(); + Spec.AddError(TEXT("Latent command timed out."), 0); + return true; + } + + return false; + } inline void FAsyncLatent::Reset() { @@ -788,8 +873,7 @@ namespace Automatron bDone = false; Future = TFuture(); } - } - + } // namespace Commands inline void FTestSpecBase::EnsureDefinitions() const { @@ -811,439 +895,551 @@ namespace Automatron } inline bool FTestSpecBase::RunTest(const FString& InParameters) - { - EnsureDefinitions(); - - if (!InParameters.IsEmpty()) - { - const TSharedRef* SpecToRun = IdToSpecMap.Find(InParameters); - if (SpecToRun != nullptr) - { - for (int32 Index = 0; Index < (*SpecToRun)->Commands.Num(); ++Index) - { - FAutomationTestFramework::GetInstance().EnqueueLatentCommand((*SpecToRun)->Commands[Index]); - } - } - } - else - { - TArray> Specs; - IdToSpecMap.GenerateValueArray(Specs); - - for (int32 SpecIndex = 0; SpecIndex < Specs.Num(); SpecIndex++) - { - for (int32 CommandIndex = 0; CommandIndex < Specs[SpecIndex]->Commands.Num(); ++CommandIndex) - { - FAutomationTestFramework::GetInstance().EnqueueLatentCommand(Specs[SpecIndex]->Commands[CommandIndex]); - } - } - } - - TestsRemaining = GetNumTests(); - return true; - } - - inline FString FTestSpecBase::GetTestSourceFileName(const FString& InTestName) const - { - FString TestId = InTestName; - if (TestId.StartsWith(TestName + TEXT(" "))) - { - TestId = InTestName.RightChop(TestName.Len() + 1); - } - - const TSharedRef* Spec = IdToSpecMap.Find(TestId); - if (Spec != nullptr) - { - return (*Spec)->Filename; - } - - return GetTestSourceFileName(); - } - - inline int32 FTestSpecBase::GetTestSourceFileLine(const FString& InTestName) const - { - FString TestId = InTestName; - if (TestId.StartsWith(TestName + TEXT(" "))) - { - TestId = InTestName.RightChop(TestName.Len() + 1); - } - - const TSharedRef* Spec = IdToSpecMap.Find(TestId); - if (Spec != nullptr) - { - return (*Spec)->LineNumber; - } - - return GetTestSourceFileLine(); - } - - inline void FTestSpecBase::GetTests(TArray& OutBeautifiedNames, TArray& OutTestCommands) const - { - EnsureDefinitions(); - - TArray> Specs; - IdToSpecMap.GenerateValueArray(Specs); - - for (int32 Index = 0; Index < Specs.Num(); Index++) - { - OutTestCommands.Push(Specs[Index]->Id); - OutBeautifiedNames.Push(Specs[Index]->Description); - } - } - - inline void FTestSpecBase::Describe(const FString& InDescription, TFunction DoWork) - { - const TSharedRef ParentScope = DefinitionScopeStack.Last(); - const TSharedRef NewScope = MakeShared(); - NewScope->Description = InDescription; - ParentScope->Children.Push(NewScope); - - DefinitionScopeStack.Push(NewScope); - PushDescription(InDescription); - DoWork(); - PopDescription(InDescription); - DefinitionScopeStack.Pop(); - - if (NewScope->It.Num() == 0 && NewScope->Children.Num() == 0) - { - ParentScope->Children.Remove(NewScope); - } - } - - inline void FTestSpecBase::PreDefine() - { - BeforeEach([this]() - { - CurrentContext = CurrentContext.NextContext(); - }); - } - - inline void FTestSpecBase::PostDefine() - { - AfterEach([this]() - { - if (IsLastTest()) - { - CurrentContext = {}; - } - }); - } - - inline void FTestSpecBase::BakeDefinitions() - { - TArray> Stack; - Stack.Push(RootDefinitionScope.ToSharedRef()); - - TArray> BeforeEach; - TArray> AfterEach; - - while (Stack.Num() > 0) - { - const TSharedRef Scope = Stack.Last(); - - BeforeEach.Append(Scope->BeforeEach); - // ScopeAfter each are added reversed - AfterEach.Reserve(AfterEach.Num() + Scope->AfterEach.Num()); - for(int32 i = Scope->AfterEach.Num() - 1; i >= 0; --i) - { - AfterEach.Add(Scope->AfterEach[i]); - } - - for (int32 ItIndex = 0; ItIndex < Scope->It.Num(); ItIndex++) - { - TSharedRef It = Scope->It[ItIndex]; - - TSharedRef Spec = MakeShared(); - Spec->Id = It->Id; - Spec->Description = It->Description; - Spec->Filename = It->Filename; - Spec->LineNumber = It->LineNumber; - Spec->Commands.Append(BeforeEach); - Spec->Commands.Add(It->Command); - - // Add after each reversed - for (int32 i = AfterEach.Num() - 1; i >= 0; --i) - { - Spec->Commands.Add(AfterEach[i]); - } - - check(!IdToSpecMap.Contains(Spec->Id)); - IdToSpecMap.Add(Spec->Id, Spec); - } - Scope->It.Empty(); - - if (Scope->Children.Num() > 0) - { - Stack.Append(Scope->Children); - Scope->Children.Empty(); - } - else - { - while (Stack.Num() > 0 && Stack.Last()->Children.Num() == 0 && Stack.Last()->It.Num() == 0) - { - const TSharedRef PoppedScope = Stack.Pop(); - - if (PoppedScope->BeforeEach.Num() > 0) - { - BeforeEach.RemoveAt(BeforeEach.Num() - PoppedScope->BeforeEach.Num(), PoppedScope->BeforeEach.Num()); - } - - if (PoppedScope->AfterEach.Num() > 0) - { - AfterEach.RemoveAt(AfterEach.Num() - PoppedScope->AfterEach.Num(), PoppedScope->AfterEach.Num()); - } - } - } - } - - RootDefinitionScope.Reset(); - DefinitionScopeStack.Reset(); - bHasBeenDefined = true; - } - - inline void FTestSpecBase::Redefine() - { - Description.Empty(); - IdToSpecMap.Empty(); - RootDefinitionScope.Reset(); - DefinitionScopeStack.Empty(); - bHasBeenDefined = false; - } - - inline FString FTestSpecBase::GetDescription() const - { - FString CompleteDescription; - for (int32 Index = 0; Index < Description.Num(); ++Index) - { - if (Description[Index].IsEmpty()) - { - continue; - } - - if (CompleteDescription.IsEmpty()) - { - CompleteDescription = Description[Index]; - } - else if (FChar::IsWhitespace(CompleteDescription[CompleteDescription.Len() - 1]) || FChar::IsWhitespace(Description[Index][0])) - { - CompleteDescription = CompleteDescription + TEXT(".") + Description[Index]; - } - else - { - CompleteDescription = FString::Printf(TEXT("%s.%s"), *CompleteDescription, *Description[Index]); - } - } - - return CompleteDescription; - } - - inline FString FTestSpecBase::GetId() const - { - if (Description.Last().EndsWith(TEXT("]"))) - { - FString ItDescription = Description.Last(); - ItDescription.RemoveAt(ItDescription.Len() - 1); - - int32 StartingBraceIndex = INDEX_NONE; - if (ItDescription.FindLastChar(TEXT('['), StartingBraceIndex) && StartingBraceIndex != ItDescription.Len() - 1) - { - FString CommandId = ItDescription.RightChop(StartingBraceIndex + 1); - return CommandId; - } - } - - FString CompleteId; - for (int32 Index = 0; Index < Description.Num(); ++Index) - { - if (Description[Index].IsEmpty()) - { - continue; - } - - if (CompleteId.IsEmpty()) - { - CompleteId = Description[Index]; - } - else if (FChar::IsWhitespace(CompleteId[CompleteId.Len() - 1]) || FChar::IsWhitespace(Description[Index][0])) - { - CompleteId = CompleteId + Description[Index]; - } - else - { - CompleteId = FString::Printf(TEXT("%s %s"), *CompleteId, *Description[Index]); - } - } - - return CompleteId; - } - - - inline void FTestSpec::PreDefine() - { - FTestSpecBase::PreDefine(); - - if (!bUseWorld) - { - return; - } - - LatentBeforeEach(EAsyncExecution::ThreadPool, [this](const auto & Done) - { - PrepareTestWorld(FSpecBaseOnWorldReady::CreateLambda([this, &Done](UWorld * InWorld) - { - World = InWorld; - Done.Execute(); - })); - }); - } - - inline void FTestSpec::PostDefine() - { - AfterEach([this]() - { - // If this spec initialized a PIE world, tear it down - if (!bReuseWorldForAllTests || IsLastTest()) - { - ReleaseTestWorld(); - } - }); - - FTestSpecBase::PostDefine(); - } - - inline void FTestSpec::PrepareTestWorld(FSpecBaseOnWorldReady OnWorldReady) - { - checkf(!IsInGameThread(), TEXT("PrepareTestWorld can only be done asynchronously. (LatentBeforeEach with ThreadPool or TaskGraph)")); - - UWorld* SelectedWorld = FindGameWorld(); - - #if WITH_EDITOR - // If there was no PIE world, start it and try again - if (bCanUsePIEWorld && !SelectedWorld && GIsEditor) - { - bool bPIEWorldIsReady = false; - FDelegateHandle PIEStartedHandle; - AsyncTask(ENamedThreads::GameThread, [&]() - { - PIEStartedHandle = FEditorDelegates::PostPIEStarted.AddLambda([&](const bool bIsSimulating) - { - // Notify the thread about the world being ready - bPIEWorldIsReady = true; - FEditorDelegates::PostPIEStarted.Remove(PIEStartedHandle); - }); - FEditorPromotionTestUtilities::StartPIE(false); - }); - - // Wait while PIE initializes - while (!bPIEWorldIsReady) - { - FPlatformProcess::Sleep(0.005f); - } - - SelectedWorld = FindGameWorld(); - bInitializedPIE = SelectedWorld != nullptr; - bInitializedWorld = bInitializedPIE; - } - #endif - - if (!SelectedWorld) - { - SelectedWorld = GWorld; - #if WITH_EDITOR - if (GIsEditor) - { - UE_LOG(LogTemp, Warning, TEXT("Test using GWorld. Not correct for PIE")); - } - #endif - } - - OnWorldReady.ExecuteIfBound(SelectedWorld); - } - - inline void FTestSpec::ReleaseTestWorld() - { - if (!IsInGameThread()) - { - AsyncTask(ENamedThreads::GameThread, [this]() - { - ReleaseTestWorld(); - }); - return; - } - - #if WITH_EDITOR - if (bInitializedPIE) - { - FEditorPromotionTestUtilities::EndPIE(); - bInitializedPIE = false; - return; - } - #endif - - if (!bInitializedWorld) - { - return; - } - - // If world is not PIE, we take care of its teardown - UWorld* WorldPtr = World.Get(); - if(WorldPtr && !WorldPtr->IsPlayInEditor()) - { - WorldPtr->BeginTearingDown(); - - // Cancel any pending connection to a server - GEngine->CancelPending(WorldPtr); - - // Shut down any existing game connections - GEngine->ShutdownWorldNetDriver(WorldPtr); - - for (FActorIterator ActorIt(WorldPtr); ActorIt; ++ActorIt) - { - ActorIt->RouteEndPlay(EEndPlayReason::Quit); - } - - if (WorldPtr->GetGameInstance() != nullptr) - { - WorldPtr->GetGameInstance()->Shutdown(); - } - - World->FlushLevelStreaming(EFlushLevelStreamingType::Visibility); - World->CleanupWorld(); - } - } - - inline UWorld* FTestSpec::FindGameWorld() - { - const TIndirectArray& WorldContexts = GEngine->GetWorldContexts(); - for (const FWorldContext& Context : WorldContexts) - { - if (Context.World() != nullptr) - { - if (Context.WorldType == EWorldType::PIE /*&& Context.PIEInstance == 0*/) - { - return Context.World(); - } - - if (Context.WorldType == EWorldType::Game) - { - return Context.World(); - } - } - } - return nullptr; - } - - template - inline void FTestSpec::Setup(FString&& InName, FString&& InPrettyName, FString&& InFileName, int32 InLineNumber) { - static_assert(TFlags & EAutomationTestFlags::ApplicationContextMask, "AutomationTest has no application flag. It shouldn't run. See AutomationTest.h."); \ - static_assert(((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::SmokeFilter) || - ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::EngineFilter) || - ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::ProductFilter) || - ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::PerfFilter) || - ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::StressFilter) || - ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::NegativeFilter), - "All AutomationTests must have exactly 1 filter type specified. See AutomationTest.h."); + EnsureDefinitions(); + + if (!InParameters.IsEmpty()) + { + const TSharedRef* SpecToRun = IdToSpecMap.Find(InParameters); + if (SpecToRun != nullptr) + { + for (int32 Index = 0; Index < (*SpecToRun)->Commands.Num(); ++Index) + { + FAutomationTestFramework::GetInstance().EnqueueLatentCommand( + (*SpecToRun)->Commands[Index]); + } + } + } + else + { + TArray> Specs; + IdToSpecMap.GenerateValueArray(Specs); + + for (int32 SpecIndex = 0; SpecIndex < Specs.Num(); SpecIndex++) + { + for (int32 CommandIndex = 0; CommandIndex < Specs[SpecIndex]->Commands.Num(); ++CommandIndex) + { + FAutomationTestFramework::GetInstance().EnqueueLatentCommand( + Specs[SpecIndex]->Commands[CommandIndex]); + } + } + } + + TestsRemaining = GetNumTests(); + return true; + } + + inline FString FTestSpecBase::GetTestSourceFileName(const FString& InTestName) const + { + FString TestId = InTestName; + if (TestId.StartsWith(TestName + TEXT(" "))) + { + TestId = InTestName.RightChop(TestName.Len() + 1); + } + + const TSharedRef* Spec = IdToSpecMap.Find(TestId); + if (Spec != nullptr) + { + return (*Spec)->Filename; + } + + return GetTestSourceFileName(); + } + + inline int32 FTestSpecBase::GetTestSourceFileLine(const FString& InTestName) const + { + FString TestId = InTestName; + if (TestId.StartsWith(TestName + TEXT(" "))) + { + TestId = InTestName.RightChop(TestName.Len() + 1); + } + + const TSharedRef* Spec = IdToSpecMap.Find(TestId); + if (Spec != nullptr) + { + return (*Spec)->LineNumber; + } + + return GetTestSourceFileLine(); + } + + inline void FTestSpecBase::GetTests( + TArray& OutBeautifiedNames, TArray& OutTestCommands) const + { + EnsureDefinitions(); + + TArray> Specs; + IdToSpecMap.GenerateValueArray(Specs); + + for (int32 Index = 0; Index < Specs.Num(); Index++) + { + OutTestCommands.Push(Specs[Index]->Id); + OutBeautifiedNames.Push(Specs[Index]->Description); + } + } + + inline void FTestSpecBase::Describe(const FString& InDescription, TFunction DoWork) + { + const TSharedRef ParentScope = DefinitionScopeStack.Last(); + const TSharedRef NewScope = MakeShared(); + NewScope->Description = InDescription; + ParentScope->Children.Push(NewScope); + + DefinitionScopeStack.Push(NewScope); + PushDescription(InDescription); + DoWork(); + PopDescription(InDescription); + DefinitionScopeStack.Pop(); + + if (NewScope->It.Num() == 0 && NewScope->Children.Num() == 0) + { + ParentScope->Children.Remove(NewScope); + } + } + + inline void FTestSpecBase::PreDefine() + { + BeforeEach([this]() { + CurrentContext = CurrentContext.NextContext(); + }); + } + + inline void FTestSpecBase::PostDefine() + { + AfterEach([this]() { + if (IsLastTest()) + { + CurrentContext = {}; + } + }); + } + + inline void FTestSpecBase::BakeDefinitions() + { + TArray> Stack; + Stack.Push(RootDefinitionScope.ToSharedRef()); + + TArray> BeforeEach; + TArray> AfterEach; + + while (Stack.Num() > 0) + { + const TSharedRef Scope = Stack.Last(); + + BeforeEach.Append(Scope->BeforeEach); + // ScopeAfter each are added reversed + AfterEach.Reserve(AfterEach.Num() + Scope->AfterEach.Num()); + for (int32 i = Scope->AfterEach.Num() - 1; i >= 0; --i) + { + AfterEach.Add(Scope->AfterEach[i]); + } + + for (int32 ItIndex = 0; ItIndex < Scope->It.Num(); ItIndex++) + { + TSharedRef It = Scope->It[ItIndex]; + + TSharedRef Spec = MakeShared(); + Spec->Id = It->Id; + Spec->Description = It->Description; + Spec->Filename = It->Filename; + Spec->LineNumber = It->LineNumber; + Spec->Commands.Append(BeforeEach); + Spec->Commands.Add(It->Command); + + // Add after each reversed + for (int32 i = AfterEach.Num() - 1; i >= 0; --i) + { + Spec->Commands.Add(AfterEach[i]); + } + + check(!IdToSpecMap.Contains(Spec->Id)); + IdToSpecMap.Add(Spec->Id, Spec); + } + Scope->It.Empty(); + + if (Scope->Children.Num() > 0) + { + Stack.Append(Scope->Children); + Scope->Children.Empty(); + } + else + { + while (Stack.Num() > 0 && Stack.Last()->Children.Num() == 0 && Stack.Last()->It.Num() == 0) + { + const TSharedRef PoppedScope = Stack.Pop(); + + if (PoppedScope->BeforeEach.Num() > 0) + { + BeforeEach.RemoveAt( + BeforeEach.Num() - PoppedScope->BeforeEach.Num(), PoppedScope->BeforeEach.Num()); + } + + if (PoppedScope->AfterEach.Num() > 0) + { + AfterEach.RemoveAt( + AfterEach.Num() - PoppedScope->AfterEach.Num(), PoppedScope->AfterEach.Num()); + } + } + } + } + + RootDefinitionScope.Reset(); + DefinitionScopeStack.Reset(); + bHasBeenDefined = true; + } + + inline void FTestSpecBase::Redefine() + { + Description.Empty(); + IdToSpecMap.Empty(); + RootDefinitionScope.Reset(); + DefinitionScopeStack.Empty(); + bHasBeenDefined = false; + } + + inline FString FTestSpecBase::GetDescription() const + { + FString CompleteDescription; + for (int32 Index = 0; Index < Description.Num(); ++Index) + { + if (Description[Index].IsEmpty()) + { + continue; + } + + if (CompleteDescription.IsEmpty()) + { + CompleteDescription = Description[Index]; + } + else if (FChar::IsWhitespace(CompleteDescription[CompleteDescription.Len() - 1]) || + FChar::IsWhitespace(Description[Index][0])) + { + CompleteDescription = CompleteDescription + TEXT(".") + Description[Index]; + } + else + { + CompleteDescription = + FString::Printf(TEXT("%s.%s"), *CompleteDescription, *Description[Index]); + } + } + + return CompleteDescription; + } + + inline FString FTestSpecBase::GetId() const + { + if (Description.Last().EndsWith(TEXT("]"))) + { + FString ItDescription = Description.Last(); + ItDescription.RemoveAt(ItDescription.Len() - 1); + + int32 StartingBraceIndex = INDEX_NONE; + if (ItDescription.FindLastChar(TEXT('['), StartingBraceIndex) && + StartingBraceIndex != ItDescription.Len() - 1) + { + FString CommandId = ItDescription.RightChop(StartingBraceIndex + 1); + return CommandId; + } + } + + FString CompleteId; + for (int32 Index = 0; Index < Description.Num(); ++Index) + { + if (Description[Index].IsEmpty()) + { + continue; + } + + if (CompleteId.IsEmpty()) + { + CompleteId = Description[Index]; + } + else if (FChar::IsWhitespace(CompleteId[CompleteId.Len() - 1]) || + FChar::IsWhitespace(Description[Index][0])) + { + CompleteId = CompleteId + Description[Index]; + } + else + { + CompleteId = FString::Printf(TEXT("%s %s"), *CompleteId, *Description[Index]); + } + } + + return CompleteId; + } + + inline void FTestSpec::PreDefine() + { + FTestSpecBase::PreDefine(); + + if (!bUseWorld) + { + return; + } + + LatentBeforeEach(EAsyncExecution::TaskGraphMainThread, [this](const auto Done) { + PrepareTestWorld([this, Done](UWorld* InWorld) { + MainWorld = InWorld; + Done.Execute(); + }); + }); + } + + inline void FTestSpec::PostDefine() + { + AfterEach([this]() { + // If this spec initialized a PIE world, tear it down + if (!bReuseWorldForAllTests || IsLastTest()) + { + ReleaseTestWorld(MainWorld.Get()); + } + }); + + FTestSpecBase::PostDefine(); + } + + inline void FTestSpec::PrepareTestWorld(TFunction OnWorldReady) + { + checkf(IsInGameThread(), TEXT("PrepareTestWorld can only be run on game thread.")); + + UWorld* SelectedWorld = FindGameWorld(); + +# if WITH_EDITOR + // If there was no PIE world, start it and try again + if (bCanUsePIEWorld && !SelectedWorld && GIsEditor) + { + FDelegateHandle PIEStartedHandle = FEditorDelegates::PostPIEStarted.AddLambda( + [this, OnWorldReady, PIEStartedHandle](const bool bIsSimulating) { + FEditorDelegates::PostPIEStarted.Remove(PIEStartedHandle); + + UWorld* SelectedWorld = FindGameWorld(); + bInitializedPIE = SelectedWorld != nullptr; + bInitializedWorld = bInitializedPIE; + + OnWorldReady(SelectedWorld); + }); + FEditorPromotionTestUtilities::StartPIE(false); + return; + } +# endif + + if (!SelectedWorld) + { + SelectedWorld = CreateWorld(DefaultWorldSettings); + bInitializedWorld = true; + } + + OnWorldReady(SelectedWorld); + } + + inline void FTestSpec::ReleaseTestWorld(UWorld* World) + { + if (!IsInGameThread()) + { + AsyncTask(ENamedThreads::GameThread, [this, World]() { + ReleaseTestWorld(World); + }); + return; + } + +# if WITH_EDITOR + if (bInitializedPIE) + { + FEditorPromotionTestUtilities::EndPIE(); + bInitializedPIE = false; + bInitializedWorld = false; + return; + } +# endif + + if (!bInitializedWorld) + { + return; + } + + // If world is not PIE, we take care of its teardown + DestroyWorld(World); + bInitializedWorld = false; + } + + inline UWorld* FTestSpec::CreateWorld(FTestWorldSettings Settings) + { + auto* GameInstance = CreateGameInstance(GEngine); + GameInstance->AddToRoot(); + + GameInstance->InitializeStandalone(TEXT("FAbilitySpec::World"), nullptr); + UWorld* World = GameInstance->GetWorld(); + + const bool bInformEngineOfWorld = true; + if (GEngine && bInformEngineOfWorld) + { + GEngine->WorldAdded(World); + } + + World->SetShouldTick(Settings.bShouldTick); + SetGameMode(World, Settings); + + FURL URL; + World->InitializeActorsForPlay(URL); + World->BeginPlay(); + + World->AddToRoot(); + return World; + } + + inline void FTestSpec::TickWorldUntil(UWorld* World, bool bUseRealtime, TFunction Delegate) + { + check(IsInGameThread()); + + const float Step = 1.f / 60.f; // 60 fps + + if (!bUseRealtime) + { + while (IsValid(World) && Delegate(Step)) + { + World->Tick(ELevelTick::LEVELTICK_All, Step); + + // This is terrible but required for subticking like this. + // we could always cache the real GFrameCounter at the start of our tests + // and restore it when finished. + ++GFrameCounter; + } + return; + } + + float DeltaTime = Step; + while (IsValid(World) && Delegate(DeltaTime)) + { + int32 TickCycles = 0; + CLOCK_CYCLES(TickCycles); + World->Tick(ELevelTick::LEVELTICK_All, DeltaTime); + UNCLOCK_CYCLES(TickCycles); + + // This is terrible but required for subticking like this. + // we could always cache the real GFrameCounter at the start of our tests + // and restore it when finished. + ++GFrameCounter; + + const float TickDuration = FPlatformTime::ToSeconds(TickCycles); + if (TickDuration < Step) + { + FPlatformProcess::Sleep(Step - TickDuration); + } + + DeltaTime = FMath::Max(TickDuration, Step); + } + } + + inline void FTestSpec::TickWorld(UWorld* World, float Duration, bool bUseRealtime) + { + TickWorldUntil(World, bUseRealtime, [&Duration](float DeltaTime) { + Duration -= DeltaTime; + return Duration > 0.f; + }); + } + + inline UGameInstance* FTestSpec::CreateGameInstance(UObject* Context) + { + FSoftClassPath GameInstanceClassName = GetDefault()->GameInstanceClass; + UClass* GameInstanceClass = GameInstanceClassName.TryLoadClass(); + + if (!GameInstanceClass) + { + GameInstanceClass = UGameInstance::StaticClass(); + } + return NewObject(Context, GameInstanceClass); + } + + inline bool FTestSpec::DestroyWorld(UWorld* World) + { + // If world is not PIE, we take care of its teardown + if (World && !World->IsPlayInEditor()) + { + World->BeginTearingDown(); + + // Cancel any pending connection to a server + GEngine->CancelPending(World); + + // Shut down any existing game connections + GEngine->ShutdownWorldNetDriver(World); + + for (FActorIterator ActorIt(World); ActorIt; ++ActorIt) + { + ActorIt->RouteEndPlay(EEndPlayReason::Quit); + } + + if (auto* GameInstance = World->GetGameInstance()) + { + GameInstance->Shutdown(); + GameInstance->RemoveFromRoot(); + } + + GEngine->DestroyWorldContext(World); + World->DestroyWorld(false); + + return true; + } + return false; + } + + inline UWorld* FTestSpec::FindGameWorld() + { + const TIndirectArray& WorldContexts = GEngine->GetWorldContexts(); + for (const FWorldContext& Context : WorldContexts) + { + if (Context.World() != nullptr) + { + if (Context.WorldType == EWorldType::PIE /*&& Context.PIEInstance == 0*/) + { + return Context.World(); + } + + if (Context.WorldType == EWorldType::Game) + { + return Context.World(); + } + } + } + return nullptr; + } + + inline bool FTestSpec::SetGameMode(UWorld* World, FTestWorldSettings& Settings) + { + if (!World->IsServer() || World->GetAuthGameMode()) + { + return false; + } + + if (!Settings.GameMode) + { + Settings.GameMode = AGameModeBase::StaticClass(); + } + + FActorSpawnParameters SpawnInfo; + SpawnInfo.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + SpawnInfo.ObjectFlags |= RF_Transient; // We never want to save game modes into a map + + auto* GameMode = World->SpawnActor(Settings.GameMode, SpawnInfo); + World->CopyGameState(GameMode, nullptr); + return GameMode != nullptr; + } + + template + inline void FTestSpec::Setup( + FString&& InName, FString&& InPrettyName, FString&& InFileName, int32 InLineNumber) + { + static_assert(TFlags & EAutomationTestFlags::ApplicationContextMask, + "AutomationTest has no application flag. It shouldn't run. See " + "AutomationTest.h."); + static_assert( + ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::SmokeFilter) || + ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::EngineFilter) || + ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::ProductFilter) || + ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::PerfFilter) || + ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::StressFilter) || + ((TFlags & EAutomationTestFlags::FilterMask) == EAutomationTestFlags::NegativeFilter), + "All AutomationTests must have exactly 1 filter type " + "specified. See AutomationTest.h."); ClassName = InName; PrettyName = MoveTemp(InPrettyName); @@ -1253,7 +1449,6 @@ namespace Automatron Reregister(InName); } -} - +} // namespace Automatron -#endif //WITH_DEV_AUTOMATION_TESTS +#endif // WITH_DEV_AUTOMATION_TESTS diff --git a/Source/AutomatronTest/AutomatronTest.Build.cs b/Source/AutomatronTest/AutomatronTest.Build.cs index 8462101..a7d58b2 100644 --- a/Source/AutomatronTest/AutomatronTest.Build.cs +++ b/Source/AutomatronTest/AutomatronTest.Build.cs @@ -17,8 +17,9 @@ public AutomatronTest(ReadOnlyTargetRules Target) : base(Target) PrivateDependencyModuleNames.AddRange(new string[] { "CoreUObject", "Engine", - "Automatron" - }); + "Automatron", + "EngineSettings" + }); if (Target.bBuildEditor) { From ca9f56f487ca66d55acf812be2c4031d76a4bf63 Mon Sep 17 00:00:00 2001 From: muit Date: Tue, 8 Sep 2020 21:17:53 +0200 Subject: [PATCH 3/7] Small clang format changes --- .clang-format | 4 +++- Source/Automatron/Public/Automatron.h | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.clang-format b/.clang-format index c6ef6a0..b3d9cd7 100644 --- a/.clang-format +++ b/.clang-format @@ -58,10 +58,12 @@ ForEachMacros: - for IncludeBlocks: Regroup IncludeCategories: + - Regex: '.*\.generated\.h' + Priority: 100 - Regex: '.*(PCH).*' Priority: -1 - Regex: '".*"' - Priority: 1 + Priority: 2 - Regex: '^<.*\.(h)>' Priority: 3 - Regex: '^<.*>' diff --git a/Source/Automatron/Public/Automatron.h b/Source/Automatron/Public/Automatron.h index 9d53339..971c5a6 100644 --- a/Source/Automatron/Public/Automatron.h +++ b/Source/Automatron/Public/Automatron.h @@ -703,6 +703,7 @@ namespace Automatron #endif // WITH_DEV_AUTOMATION_TESTS + //////////////////////////////////////////////////////////////// // GENERATION MACROS From a902c9a41a86b865a6601f19595d425e7e3318e3 Mon Sep 17 00:00:00 2001 From: muit Date: Sun, 13 Dec 2020 22:55:07 +0100 Subject: [PATCH 4/7] 4.26 Update --- Automatron.uplugin | 1 + Source/Automatron/Public/Automatron.h | 90 +++++++++++++++------------ 2 files changed, 52 insertions(+), 39 deletions(-) diff --git a/Automatron.uplugin b/Automatron.uplugin index 98a9d33..75ccfcd 100644 --- a/Automatron.uplugin +++ b/Automatron.uplugin @@ -10,6 +10,7 @@ "DocsURL": "https://splash-damage.github.io/Automatron", "MarketplaceURL": "", "SupportURL": "", + "EngineVersion": "4.26", "CanContainContent": true, "IsBetaVersion": true, "Installed": false, diff --git a/Source/Automatron/Public/Automatron.h b/Source/Automatron/Public/Automatron.h index 971c5a6..8c0eb42 100644 --- a/Source/Automatron/Public/Automatron.h +++ b/Source/Automatron/Public/Automatron.h @@ -39,15 +39,13 @@ #include #include +#include #include #include #include #include #include -#ifndef WITH_DEV_AUTOMATION_TESTS -# define WITH_DEV_AUTOMATION_TESTS 1 -#endif #if WITH_EDITOR # include @@ -57,13 +55,13 @@ //////////////////////////////////////////////////////////////// // DEFINITIONS -#if WITH_DEV_AUTOMATION_TESTS namespace Automatron { class FTestSpecBase; struct FTestWorldSettings { + TSubclassOf GameInstance; TSubclassOf GameMode = AGameModeBase::StaticClass(); bool bShouldTick = false; @@ -71,19 +69,41 @@ namespace Automatron namespace Spec { + class FRegister + { + public: + DECLARE_EVENT(FRegister, FOnSetup); + + static FOnSetup& OnSetup() + { + static FOnSetup Delegate{}; + return Delegate; + } + }; + ///////////////////////////////////////////////////// // Initializes an spec instance at global execution time // and registers it to the system template - struct TRegister + class TRegister : public FRegister { - static TRegister Register; + public: + + // Just by existing, this instance will define the class and register the spec + static TRegister Instance; - T Instance; - TRegister() : Instance{} + TRegister() { - Instance.Setup(); + OnSetup().AddStatic(&TRegister::Setup); + } + + private: + + static void Setup() + { + static T Spec{}; + Spec.Setup(); } }; @@ -311,8 +331,8 @@ namespace Automatron Spec::FContext CurrentContext; public: - FTestSpecBase(const FString& InName, const bool bInComplexTask) - : FAutomationTestBase(InName, bInComplexTask) + FTestSpecBase() + : FAutomationTestBase("", false) , RootDefinitionScope(MakeShared()) { DefinitionScopeStack.Push(RootDefinitionScope.ToSharedRef()); @@ -618,7 +638,7 @@ namespace Automatron TWeakObjectPtr MainWorld; public: - FTestSpec() : FTestSpecBase("", false) {} + FTestSpec() : FTestSpecBase() {} virtual FString GetTestSourceFileName() const override { @@ -671,7 +691,7 @@ namespace Automatron void TickWorldUntil(UWorld* World, bool bUseRealtime, TFunction Delegate); void TickWorld(UWorld* World, float Duration, bool bUseRealtime = false); - UGameInstance* CreateGameInstance(UObject* Context); + UGameInstance* CreateGameInstance(const FTestWorldSettings& Settings, UObject* Context); bool DestroyWorld(UWorld* World); @@ -697,21 +717,23 @@ namespace Automatron namespace Spec { template - TRegister TRegister::Register{}; + TRegister TRegister::Instance{}; } -} // namespace Automatron -#endif // WITH_DEV_AUTOMATION_TESTS + static void RegisterSpecs() + { + Spec::FRegister::OnSetup().Broadcast(); + } +} // namespace Automatron //////////////////////////////////////////////////////////////// // GENERATION MACROS -#if WITH_DEV_AUTOMATION_TESTS -# define GENERATE_SPEC(TClass, PrettyName, TFlags) \ +#define GENERATE_SPEC(TClass, PrettyName, TFlags) \ GENERATE_SPEC_PRIVATE(TClass, PrettyName, TFlags, __FILE__, __LINE__) -# define GENERATE_SPEC_PRIVATE(TClass, PrettyName, TFlags, FileName, LineNumber) \ +#define GENERATE_SPEC_PRIVATE(TClass, PrettyName, TFlags, FileName, LineNumber) \ private: \ void Setup() \ { \ @@ -719,35 +741,23 @@ namespace Automatron } \ static Automatron::Spec::TRegister& __meta_register() \ { \ - return Automatron::Spec::TRegister::Register; \ + return Automatron::Spec::TRegister::Instance; \ } \ friend Automatron::Spec::TRegister; \ \ virtual void Define() override -# define SPEC(TClass, TParent, PrettyName, TFlags) \ +#define SPEC(TClass, TParent, PrettyName, TFlags) \ class TClass : public TParent \ { \ GENERATE_SPEC(TClass, PrettyName, TFlags); \ }; \ void TClass::Define() -#else // WITH_DEV_AUTOMATION_TESTS - -# define GENERATE_SPEC(TClass, PrettyName, TFlags) -# define SPEC(TClass, TParent, PrettyName, TFlags) \ - class TClass \ - { \ - void Define(); \ - }; \ - void TClass::Define() - -#endif // WITH_DEV_AUTOMATION_TESTS //////////////////////////////////////////////////////////////// // DECLARATIONS -#if WITH_DEV_AUTOMATION_TESTS namespace Automatron { namespace Commands @@ -1265,7 +1275,7 @@ namespace Automatron inline UWorld* FTestSpec::CreateWorld(FTestWorldSettings Settings) { - auto* GameInstance = CreateGameInstance(GEngine); + auto* GameInstance = CreateGameInstance(Settings, GEngine); GameInstance->AddToRoot(); GameInstance->InitializeStandalone(TEXT("FAbilitySpec::World"), nullptr); @@ -1339,10 +1349,14 @@ namespace Automatron }); } - inline UGameInstance* FTestSpec::CreateGameInstance(UObject* Context) + inline UGameInstance* FTestSpec::CreateGameInstance(const FTestWorldSettings& Settings, UObject* Context) { - FSoftClassPath GameInstanceClassName = GetDefault()->GameInstanceClass; - UClass* GameInstanceClass = GameInstanceClassName.TryLoadClass(); + UClass* GameInstanceClass = Settings.GameInstance.Get(); + if(!GameInstanceClass) + { + FSoftClassPath GameInstanceClassName = GetDefault()->GameInstanceClass; + GameInstanceClass = GameInstanceClassName.TryLoadClass(); + } if (!GameInstanceClass) { @@ -1451,5 +1465,3 @@ namespace Automatron Reregister(InName); } } // namespace Automatron - -#endif // WITH_DEV_AUTOMATION_TESTS From 1e3286b8948b1529010825595ede28a87d0475d2 Mon Sep 17 00:00:00 2001 From: muit Date: Sat, 12 Mar 2022 20:59:55 +0100 Subject: [PATCH 5/7] Made editor dependency private --- Source/Automatron/Automatron.Build.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Automatron/Automatron.Build.cs b/Source/Automatron/Automatron.Build.cs index b63f050..c7a10f7 100644 --- a/Source/Automatron/Automatron.Build.cs +++ b/Source/Automatron/Automatron.Build.cs @@ -21,7 +21,7 @@ public Automatron(ReadOnlyTargetRules Target) : base(Target) if (Target.bBuildEditor) { - PublicDependencyModuleNames.Add("UnrealEd"); + PrivateDependencyModuleNames.Add("UnrealEd"); } } } From 72f2c8571b489ceecd1a4c1f1cefe617573520ae Mon Sep 17 00:00:00 2001 From: muit Date: Fri, 25 Nov 2022 22:34:23 +0100 Subject: [PATCH 6/7] Updated to 5.1 --- Automatron.uplugin | 2 +- Source/Automatron/Public/Automatron.h | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Automatron.uplugin b/Automatron.uplugin index 75ccfcd..b122a2b 100644 --- a/Automatron.uplugin +++ b/Automatron.uplugin @@ -10,7 +10,7 @@ "DocsURL": "https://splash-damage.github.io/Automatron", "MarketplaceURL": "", "SupportURL": "", - "EngineVersion": "4.26", + "EngineVersion": "5.1", "CanContainContent": true, "IsBetaVersion": true, "Installed": false, diff --git a/Source/Automatron/Public/Automatron.h b/Source/Automatron/Public/Automatron.h index 8c0eb42..5086f3d 100644 --- a/Source/Automatron/Public/Automatron.h +++ b/Source/Automatron/Public/Automatron.h @@ -1420,7 +1420,8 @@ namespace Automatron inline bool FTestSpec::SetGameMode(UWorld* World, FTestWorldSettings& Settings) { - if (!World->IsServer() || World->GetAuthGameMode()) + if ((!World->IsNetMode(NM_DedicatedServer) && !World->IsNetMode(NM_ListenServer)) || + World->GetAuthGameMode()) { return false; } From 0a54b7459428560a2195893d1fe2d332583f30fd Mon Sep 17 00:00:00 2001 From: muit Date: Wed, 11 Oct 2023 22:56:49 +0200 Subject: [PATCH 7/7] Fix for Linux on 5.3 --- Source/Automatron/Public/Automatron.h | 62 +++++++++++++-------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/Source/Automatron/Public/Automatron.h b/Source/Automatron/Public/Automatron.h index 5086f3d..7f2d967 100644 --- a/Source/Automatron/Public/Automatron.h +++ b/Source/Automatron/Public/Automatron.h @@ -38,8 +38,8 @@ #pragma once #include -#include #include +#include #include #include #include @@ -88,7 +88,6 @@ namespace Automatron class TRegister : public FRegister { public: - // Just by existing, this instance will define the class and register the spec static TRegister Instance; @@ -99,7 +98,6 @@ namespace Automatron } private: - static void Setup() { static T Spec{}; @@ -631,9 +629,10 @@ namespace Automatron uint32 Flags = 0; bool bInitializedWorld = false; -# if WITH_EDITOR +#if WITH_EDITOR bool bInitializedPIE = false; -# endif + FDelegateHandle PIEStartedHandle; +#endif TWeakObjectPtr MainWorld; @@ -731,28 +730,28 @@ namespace Automatron // GENERATION MACROS #define GENERATE_SPEC(TClass, PrettyName, TFlags) \ - GENERATE_SPEC_PRIVATE(TClass, PrettyName, TFlags, __FILE__, __LINE__) + GENERATE_SPEC_PRIVATE(TClass, PrettyName, TFlags, __FILE__, __LINE__) #define GENERATE_SPEC_PRIVATE(TClass, PrettyName, TFlags, FileName, LineNumber) \ - private: \ - void Setup() \ - { \ - FTestSpec::Setup(TEXT(#TClass), TEXT(PrettyName), FileName, LineNumber); \ - } \ - static Automatron::Spec::TRegister& __meta_register() \ - { \ - return Automatron::Spec::TRegister::Instance; \ - } \ - friend Automatron::Spec::TRegister; \ - \ - virtual void Define() override +private: \ + void Setup() \ + { \ + FTestSpec::Setup(TEXT(#TClass), TEXT(PrettyName), FileName, LineNumber); \ + } \ + static Automatron::Spec::TRegister& __meta_register() \ + { \ + return Automatron::Spec::TRegister::Instance; \ + } \ + friend Automatron::Spec::TRegister; \ + \ + virtual void Define() override #define SPEC(TClass, TParent, PrettyName, TFlags) \ - class TClass : public TParent \ - { \ - GENERATE_SPEC(TClass, PrettyName, TFlags); \ - }; \ - void TClass::Define() + class TClass : public TParent \ + { \ + GENERATE_SPEC(TClass, PrettyName, TFlags); \ + }; \ + void TClass::Define() //////////////////////////////////////////////////////////////// @@ -1215,14 +1214,12 @@ namespace Automatron UWorld* SelectedWorld = FindGameWorld(); -# if WITH_EDITOR +#if WITH_EDITOR // If there was no PIE world, start it and try again if (bCanUsePIEWorld && !SelectedWorld && GIsEditor) { - FDelegateHandle PIEStartedHandle = FEditorDelegates::PostPIEStarted.AddLambda( - [this, OnWorldReady, PIEStartedHandle](const bool bIsSimulating) { - FEditorDelegates::PostPIEStarted.Remove(PIEStartedHandle); - + PIEStartedHandle = + FEditorDelegates::PostPIEStarted.AddLambda([this, OnWorldReady](const bool bIsSimulating) { UWorld* SelectedWorld = FindGameWorld(); bInitializedPIE = SelectedWorld != nullptr; bInitializedWorld = bInitializedPIE; @@ -1232,7 +1229,7 @@ namespace Automatron FEditorPromotionTestUtilities::StartPIE(false); return; } -# endif +#endif if (!SelectedWorld) { @@ -1253,7 +1250,8 @@ namespace Automatron return; } -# if WITH_EDITOR +#if WITH_EDITOR + FEditorDelegates::PostPIEStarted.Remove(PIEStartedHandle); if (bInitializedPIE) { FEditorPromotionTestUtilities::EndPIE(); @@ -1261,7 +1259,7 @@ namespace Automatron bInitializedWorld = false; return; } -# endif +#endif if (!bInitializedWorld) { @@ -1352,7 +1350,7 @@ namespace Automatron inline UGameInstance* FTestSpec::CreateGameInstance(const FTestWorldSettings& Settings, UObject* Context) { UClass* GameInstanceClass = Settings.GameInstance.Get(); - if(!GameInstanceClass) + if (!GameInstanceClass) { FSoftClassPath GameInstanceClassName = GetDefault()->GameInstanceClass; GameInstanceClass = GameInstanceClassName.TryLoadClass();