// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. #include "CoreMinimal.h" #include "Misc/AutomationTest.h" #include "UObject/ObjectMacros.h" #include "UObject/UObjectGlobals.h" #include "UObject/UObjectHash.h" #include "UObject/Object.h" #include "UObject/Package.h" #include "UObject/UnrealType.h" #include "Serialization/ArchiveObjectCrc32.h" #include "Engine/Blueprint.h" #include "GameFramework/Actor.h" #include "Engine/World.h" class FArchiveSkipTransientObjectCRC32 : public FArchiveObjectCrc32 { public: static bool CanPropertyBeDifferentInConvertedCDO(const UProperty* InProperty) { check(InProperty); return InProperty->HasAllPropertyFlags(CPF_Transient) || InProperty->HasAllPropertyFlags(CPF_EditorOnly) || InProperty->GetName() == TEXT("BlueprintCreatedComponents") || InProperty->GetName() == TEXT("CreationMethod") || InProperty->GetName() == TEXT("InstanceComponents") || InProperty->GetName() == TEXT("bNetAddressable") || InProperty->GetName() == TEXT("OwnedComponents"); } // Begin FArchive Interface virtual bool ShouldSkipProperty(const UProperty* InProperty) const override { return FArchiveObjectCrc32::ShouldSkipProperty(InProperty) || CanPropertyBeDifferentInConvertedCDO(InProperty); } // End FArchive Interface }; void ClearNativeRecursive(UObject* OnObject) { OnObject->ClearInternalFlags(EInternalObjectFlags::Native); TArray Children; GetObjectsWithOuter(OnObject, Children, false); for (UObject* Entry : Children) { ClearNativeRecursive(Entry); } } // We mess with the rootset flag instead of using FGCObject because it's an error for any test data to remain in the rootset after the test runs struct FOwnedObjectsHelper { ~FOwnedObjectsHelper() { for (UObject* Entry : OwnedObjects) { Entry->RemoveFromRoot(); Entry->ClearFlags(RF_Standalone); if (Entry->IsNative()) { ClearNativeRecursive(Entry); } // Actors need to be explicitly destroyed, probably just to remove them from their owning // level: if (AActor* AsActor = Cast(Entry)) { AsActor->Destroy(); } } CollectGarbage(RF_NoFlags); } void Push(UObject* Obj) { Obj->AddToRoot(); OwnedObjects.Push(Obj); } private: TArray OwnedObjects; }; // Helper functions introduced to load classes (generated or native: static UClass* GetGeneratedClass(const TCHAR* TestFolder, const TCHAR* ClassName, FAutomationTestBase* Context, FOwnedObjectsHelper &OwnedObjects) { FString FullName = FString::Printf(TEXT("/RuntimeTests/CompilerTests/%s/%s.%s"), TestFolder, ClassName, ClassName); UBlueprint* Blueprint = LoadObject(nullptr, *FullName); if (!Blueprint) { Context->AddWarning(FString::Printf(TEXT("Missing blueprint for test: '%s'"), *FullName)); return nullptr; } TArray Objects; GetObjectsWithOuter(Blueprint->GetOuter(), Objects, false); for (UObject* Entry : Objects) { OwnedObjects.Push(Entry); } CollectGarbage(RF_NoFlags); return Blueprint->GeneratedClass; } static UClass* GetNativeClass(const TCHAR* TestFolder, const TCHAR* ClassName, FAutomationTestBase* Context, FOwnedObjectsHelper &OwnedObjects) { CollectGarbage(RF_NoFlags); FString FullName = FString::Printf(TEXT("/RuntimeTests/CompilerTests/%s/%s"), TestFolder, ClassName); UPackage* NativePackage = CreatePackage(nullptr, *FullName); check(NativePackage); const FString FStringFullPathName = FString::Printf(TEXT("%s.%s_C"), *FullName, ClassName); UClass* Ret = (UClass*)ConstructDynamicType(*FStringFullPathName, EConstructDynamicType::CallZConstructor); //FindObjectFast(NativePackage, *(FString(ClassName) + TEXT("_C"))); if (!Ret) { Context->AddWarning(FString::Printf(TEXT("Missing native type for test: '%s'"), ClassName)); return nullptr; } TArray Objects; GetObjectsWithOuter(NativePackage, Objects, false); for (UObject* Entry : Objects) { OwnedObjects.Push(Entry); } CollectGarbage(RF_NoFlags); return Ret; } typedef UClass* (*ClassAccessor)(const TCHAR*, const TCHAR*, FAutomationTestBase*, FOwnedObjectsHelper&); typedef uint32(*TestImpl)(ClassAccessor F, FAutomationTestBase* Context); // This pattern is repeated in each test, so I'm just writing a helper function rather than copy/pasting it: static bool RunTestHelper(TestImpl T, FAutomationTestBase* Context) { uint32 ResultsGenerated = T(&GetGeneratedClass, Context); uint32 ResultsNative = T(&GetNativeClass, Context); if (ResultsGenerated == 0) { Context->AddError(TEXT("Test failed to run!")); } else if (ResultsGenerated != ResultsNative) { Context->AddError(TEXT("Native differs from generated!")); } return true; } // Helper functions introduced to avoid crashing if classes are missing: static UObject* NewTestObject(UClass* Class, FOwnedObjectsHelper &OwnedObjects) { if (Class) { UObject* Result = NewObject(GetTransientPackage(), Class, NAME_None); if (Result) { OwnedObjects.Push(Result); } return Result; } return nullptr; } static UObject* NewTestActor(UClass* ActorClass, FOwnedObjectsHelper &OwnedObjects) { if (ActorClass) { AActor* Actor = GWorld->SpawnActor(ActorClass); #if WITH_EDITORONLY_DATA if (Actor) { OwnedObjects.Push(Actor); Actor->GetRootComponent()->bVisualizeComponent = true; } #endif return Actor; } return nullptr; } static void Call(UObject* Target, const TCHAR* FunctionName, void* Args = nullptr) { if (!Target) { return; } UFunction* Fn = Target->FindFunction(FunctionName); if (Fn) { Target->ProcessEvent(Fn, Args); } } // Remove EAutomationTestFlags::Disabled to enable these tests, note that these will need to be moved into the ClientContext because we can only test nativized cooked content: static const uint32 CompilerTestFlags = EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter | EAutomationTestFlags::Disabled; // Tests: IMPLEMENT_SIMPLE_AUTOMATION_TEST(FBPCompilerArrayTest, "Project.Blueprints.NativeBackend.ArrayTest", CompilerTestFlags) bool FBPCompilerArrayTest::RunTest(const FString& Parameters) { auto TestBody = [](ClassAccessor F, FAutomationTestBase* Context) { FOwnedObjectsHelper OwnedObjects; TArray Input; Input.Push(TEXT("addedString")); UObject* TestInstance = NewTestObject(F(TEXT("Array"), TEXT("BP_Array_Basic"), Context, OwnedObjects), OwnedObjects); if (!TestInstance) { return 0u; } Call(TestInstance, TEXT("RunArrayTest"), &Input); FArchiveSkipTransientObjectCRC32 Results; return Results.Crc32(TestInstance); }; return RunTestHelper(TestBody, this); } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FBPCompilerCDOTest, "Project.Blueprints.NativeBackend.CDOTest", CompilerTestFlags) bool FBPCompilerCDOTest::RunTest(const FString& Parameters) { FOwnedObjectsHelper OwnedObjects; UObject* GeneratedTestInstance = NewTestObject(GetGeneratedClass(TEXT("CDO"), TEXT("BP_CDO_Basic"), this, OwnedObjects), OwnedObjects); UObject* NativeTestInstance = NewTestObject(GetNativeClass(TEXT("CDO"), TEXT("BP_CDO_Basic"), this, OwnedObjects), OwnedObjects); if (!GeneratedTestInstance || !NativeTestInstance) { AddError(TEXT("Test failed to run!")); return true; } for (UProperty* NativeProperty : TFieldRange(NativeTestInstance->GetClass())) { if ((NativeProperty->GetOwnerClass() == UObject::StaticClass()) || (FArchiveSkipTransientObjectCRC32::CanPropertyBeDifferentInConvertedCDO(NativeProperty))) { continue; } UProperty* BPProperty = FindField(GeneratedTestInstance->GetClass(), *NativeProperty->GetName()); if (!BPProperty) { AddError(*FString::Printf(TEXT("Cannot find property %s in BPGC"), *NativeProperty->GetName())); return true; } uint8* NativeValue = NativeProperty->ContainerPtrToValuePtr(NativeTestInstance->GetClass()->GetDefaultObject()); uint8* BPGCValue = BPProperty->ContainerPtrToValuePtr(GeneratedTestInstance->GetClass()->GetDefaultObject()); if (!NativeProperty->Identical(NativeValue, BPGCValue)) { AddError(*FString::Printf(TEXT("Different value of property %s"), *NativeProperty->GetName())); return true; } } return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FBPCompilerCommunicationTest, "Project.Blueprints.NativeBackend.CommunicationTest", CompilerTestFlags) bool FBPCompilerCommunicationTest::RunTest(const FString& Parameters) { auto TestBody = [](ClassAccessor F, FAutomationTestBase* Context) { FOwnedObjectsHelper OwnedObjects; UObject* A = NewTestObject(F(TEXT("Communication"), TEXT("BP_Comm_Test_A"), Context, OwnedObjects), OwnedObjects); UObject* B = NewTestObject(F(TEXT("Communication"), TEXT("BP_Comm_Test_B"), Context, OwnedObjects), OwnedObjects); if (!A || !B) { return 0u; } Call(A, TEXT("Flop")); Call(B, TEXT("Flip")); FArchiveSkipTransientObjectCRC32 Results; return Results.Crc32(B, Results.Crc32(A)); }; return RunTestHelper(TestBody, this); } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FBPCompilerConstructionScriptTest, "Project.Blueprints.NativeBackend.ConstructionScriptTest", CompilerTestFlags) bool FBPCompilerConstructionScriptTest::RunTest(const FString& Parameters) { auto TestBody = [](ClassAccessor F, FAutomationTestBase* Context) { FOwnedObjectsHelper OwnedObjects; UObject* TestInstance = NewTestActor(F(TEXT("ConstructionScript"), TEXT("BP_ConstructionScript_Test"), Context, OwnedObjects), OwnedObjects); if (!TestInstance) { return 0u; } FArchiveSkipTransientObjectCRC32 Results; return Results.Crc32(TestInstance); }; return RunTestHelper(TestBody, this); } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FBPCompilerControlFlowTest, "Project.Blueprints.NativeBackend.ControlFlow", CompilerTestFlags) bool FBPCompilerControlFlowTest::RunTest(const FString& Parameters) { auto TestBody = [](ClassAccessor F, FAutomationTestBase* Context) { FOwnedObjectsHelper OwnedObjects; UObject* TestInstance = NewTestObject(F(TEXT("ControlFlow"), TEXT("BP_ControlFlow_Basic"), Context, OwnedObjects), OwnedObjects); if (!TestInstance) { return 0u; } Call(TestInstance, TEXT("RunControlFlowTest")); FArchiveSkipTransientObjectCRC32 Results; return Results.Crc32(TestInstance); }; return RunTestHelper(TestBody, this); } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FBPCompilerEnumTest, "Project.Blueprints.NativeBackend.EnumTest", CompilerTestFlags) bool FBPCompilerEnumTest::RunTest(const FString& Parameters) { auto TestBody = [](ClassAccessor F, FAutomationTestBase* Context) { FOwnedObjectsHelper OwnedObjects; UObject* TestInstance = NewTestObject(F(TEXT("Enum"), TEXT("BP_Enum_Reader_Writer"), Context, OwnedObjects), OwnedObjects); if (!TestInstance) { return 0u; } Call(TestInstance, TEXT("UpdateEnum")); FArchiveSkipTransientObjectCRC32 Results; return Results.Crc32(TestInstance); }; return RunTestHelper(TestBody, this); } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FBPCompilerEventTest, "Project.Blueprints.NativeBackend.Event", CompilerTestFlags) bool FBPCompilerEventTest::RunTest(const FString& Parameters) { auto TestBody = [](ClassAccessor F, FAutomationTestBase* Context) { FOwnedObjectsHelper OwnedObjects; UObject* TestInstance = NewTestObject(F(TEXT("Event"), TEXT("BP_Event_Basic"), Context, OwnedObjects), OwnedObjects); if (!TestInstance) { return 0u; } Call(TestInstance, TEXT("BeginEventChain")); FArchiveSkipTransientObjectCRC32 Results; return Results.Crc32(TestInstance); }; return RunTestHelper(TestBody, this); } struct FInheritenceTestParams { bool bFlag; TArray Strings; TArray Result; }; inline bool operator==(const FInheritenceTestParams& LHS, const FInheritenceTestParams& RHS) { return LHS.bFlag == RHS.bFlag && LHS.Strings == RHS.Strings && LHS.Result == RHS.Result; } inline bool operator!=(const FInheritenceTestParams& LHS, const FInheritenceTestParams& RHS) { return !(LHS == RHS); } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FBPCompilerInheritenceTest, "Project.Blueprints.NativeBackend.Inheritence", CompilerTestFlags) bool FBPCompilerInheritenceTest::RunTest(const FString& Parameters) { auto TestBody = [](ClassAccessor F, FAutomationTestBase* Context) { FOwnedObjectsHelper OwnedObjects; UObject* TestInstance = NewTestObject(F(TEXT("Inheritence"), TEXT("BP_Child_Basic"), Context, OwnedObjects), OwnedObjects); if (!TestInstance) { return 0u; } FInheritenceTestParams Params; Params.bFlag = true; Call(TestInstance, TEXT("VirtualFunction"), &Params); FArchiveSkipTransientObjectCRC32 Results; return Results.Crc32(TestInstance); }; return RunTestHelper(TestBody, this); } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FBPCompilerStructureTest, "Project.Blueprints.NativeBackend.Structure", CompilerTestFlags) bool FBPCompilerStructureTest::RunTest(const FString& Parameters) { auto TestBody = [](ClassAccessor F, FAutomationTestBase* Context) { FOwnedObjectsHelper OwnedObjects; UObject* TestInstance = NewTestObject(F(TEXT("Structure"), TEXT("BP_Structure_Driver"), Context, OwnedObjects), OwnedObjects); if (!TestInstance) { return 0u; } Call(TestInstance, TEXT("RunStructTest")); FArchiveSkipTransientObjectCRC32 Results; return Results.Crc32(TestInstance); }; return RunTestHelper(TestBody, this); } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FBPCompilerNodeTest, "Project.Blueprints.NativeBackend.Node", CompilerTestFlags) bool FBPCompilerNodeTest::RunTest(const FString& Parameters) { auto TestBody = [](ClassAccessor F, FAutomationTestBase* Context) { FOwnedObjectsHelper OwnedObjects; UObject* TestInstance = NewTestObject(F(TEXT("Node"), TEXT("BP_Node_Basic"), Context, OwnedObjects), OwnedObjects); if (!TestInstance) { return 0u; } Call(TestInstance, TEXT("RunNodes")); FArchiveSkipTransientObjectCRC32 Results; return Results.Crc32(TestInstance); }; return RunTestHelper(TestBody, this); }