diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..b3d9cd7 --- /dev/null +++ b/.clang-format @@ -0,0 +1,144 @@ +--- +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: '.*\.generated\.h' + Priority: 100 + - Regex: '.*(PCH).*' + Priority: -1 + - Regex: '".*"' + Priority: 2 + - 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/Automatron.uplugin b/Automatron.uplugin index 2d6920a..b122a2b 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", @@ -10,6 +10,7 @@ "DocsURL": "https://splash-damage.github.io/Automatron", "MarketplaceURL": "", "SupportURL": "", + "EngineVersion": "5.1", "CanContainContent": true, "IsBetaVersion": true, "Installed": false, diff --git a/Source/Automatron/Automatron.Build.cs b/Source/Automatron/Automatron.Build.cs index 16f4893..c7a10f7 100644 --- a/Source/Automatron/Automatron.Build.cs +++ b/Source/Automatron/Automatron.Build.cs @@ -8,18 +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" - }); - - PrivateDependencyModuleNames.AddRange(new string[] - { + "FunctionalTesting", + "EngineSettings" }); if (Target.bBuildEditor) 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..7f2d967 100644 --- a/Source/Automatron/Public/Automatron.h +++ b/Source/Automatron/Public/Automatron.h @@ -1,37 +1,1466 @@ -// Copyright 2020 Splash Damage, Ltd. - All Rights Reserved. +// 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 +// +// 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 #include -#include -#include "TestSpec.h" +#if WITH_EDITOR +# include +# include +#endif + +//////////////////////////////////////////////////////////////// +// DEFINITIONS + +namespace Automatron +{ + class FTestSpecBase; + + struct FTestWorldSettings + { + TSubclassOf GameInstance; + TSubclassOf GameMode = AGameModeBase::StaticClass(); + + bool bShouldTick = false; + }; + + 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 + class TRegister : public FRegister + { + public: + // Just by existing, this instance will define the class and register the spec + static TRegister Instance; + + + TRegister() + { + OnSetup().AddStatic(&TRegister::Setup); + } + + private: + static void Setup() + { + static T Spec{}; + Spec.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 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) + : 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(); + }; + }; // namespace Commands + + 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() + : FAutomationTestBase("", false) + , 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; + }; + + 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; + + FTestWorldSettings DefaultWorldSettings; + + private: + FString ClassName; + FString PrettyName; + FString FileName; + int32 LineNumber = -1; + uint32 Flags = 0; + + bool bInitializedWorld = false; +#if WITH_EDITOR + bool bInitializedPIE = false; + FDelegateHandle PIEStartedHandle; +#endif + + TWeakObjectPtr MainWorld; + + public: + FTestSpec() : FTestSpecBase() {} + + 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(TFunction OnWorldReady); + + void ReleaseTestWorld(UWorld* World); + + // 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(const FTestWorldSettings& Settings, UObject* Context); + + bool DestroyWorld(UWorld* World); + + UWorld* GetMainWorld() const + { + return MainWorld.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(); + + static bool SetGameMode(UWorld* World, FTestWorldSettings& Settings); + }; + + namespace Spec + { + template + TRegister TRegister::Instance{}; + } + + static void RegisterSpecs() + { + Spec::FRegister::OnSetup().Broadcast(); + } +} // namespace Automatron + + +//////////////////////////////////////////////////////////////// +// GENERATION MACROS #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() \ - { \ +#define GENERATE_SPEC_PRIVATE(TClass, PrettyName, TFlags, FileName, LineNumber) \ +private: \ + void Setup() \ + { \ FTestSpec::Setup(TEXT(#TClass), TEXT(PrettyName), FileName, LineNumber); \ - } \ - static TSpecRegister& __meta_register() \ - { \ - return TSpecRegister::Register; \ - } \ - friend TSpecRegister; \ -\ + } \ + 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() + + +//////////////////////////////////////////////////////////////// +// DECLARATIONS + +namespace Automatron +{ + 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(); + } + } // namespace Commands + + 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::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) + { + PIEStartedHandle = + FEditorDelegates::PostPIEStarted.AddLambda([this, OnWorldReady](const bool bIsSimulating) { + 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 + FEditorDelegates::PostPIEStarted.Remove(PIEStartedHandle); + 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(Settings, 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(const FTestWorldSettings& Settings, UObject* Context) + { + UClass* GameInstanceClass = Settings.GameInstance.Get(); + if (!GameInstanceClass) + { + FSoftClassPath GameInstanceClassName = GetDefault()->GameInstanceClass; + 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->IsNetMode(NM_DedicatedServer) && !World->IsNetMode(NM_ListenServer)) || + 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); + FileName = MoveTemp(InFileName); + LineNumber = InLineNumber; + Flags = TFlags; -#define SPEC(TClass, TParent, PrettyName, TFlags) \ -class TClass : public TParent \ -{ \ - GENERATE_SPEC(TClass, PrettyName, TFlags); \ -}; \ -void TClass::Define() + Reregister(InName); + } +} // namespace Automatron 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..a7d58b2 100644 --- a/Source/AutomatronTest/AutomatronTest.Build.cs +++ b/Source/AutomatronTest/AutomatronTest.Build.cs @@ -12,12 +12,18 @@ public AutomatronTest(ReadOnlyTargetRules Target) : base(Target) PublicDependencyModuleNames.AddRange(new string[] { "Core", - "Automatron", }); PrivateDependencyModuleNames.AddRange(new string[] { "CoreUObject", - "Engine" - }); + "Engine", + "Automatron", + "EngineSettings" + }); + + 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 }); }