// Copyright Epic Games, Inc. All Rights Reserved. #include "AutoRTFM.h" #include "AutoRTFMTesting.h" #include "ScopedGuard.h" #include "GenericPlatform/GenericPlatformMisc.h" #include "HAL/MallocLeakDetection.h" #include "MyAutoRTFMTestObject.h" #include "UObject/GCObject.h" #include "UObject/ReachabilityAnalysis.h" #include "UObject/UObjectAnnotation.h" #include "UObject/UObjectThreadContext.h" THIRD_PARTY_INCLUDES_START #include "Catch2Includes.h" THIRD_PARTY_INCLUDES_END TEST_CASE("UObject.NewObject") { SECTION("Create") { UMyAutoRTFMTestObject* Object = nullptr; AutoRTFM::Testing::Commit([&] { Object = NewObject(); }); REQUIRE(nullptr != Object); REQUIRE(42 == Object->Value); } SECTION("Abort") { UMyAutoRTFMTestObject* Object = nullptr; AutoRTFM::Testing::Abort([&] { Object = NewObject(); AutoRTFM::AbortTransaction(); }); REQUIRE(nullptr == Object); } } TEST_CASE("UObject.NewObjectWithOuter") { SECTION("Create") { UMyAutoRTFMTestObject* Outer = NewObject(); UMyAutoRTFMTestObject* Object = nullptr; AutoRTFM::Testing::Commit([&] { Object = NewObject(Outer); }); REQUIRE(nullptr != Object); REQUIRE(42 == Object->Value); REQUIRE(Object->IsInOuter(Outer)); REQUIRE(55 == Outer->Value); } SECTION("Abort") { UMyAutoRTFMTestObject* Outer = NewObject(); UMyAutoRTFMTestObject* Object = nullptr; AutoRTFM::Testing::Abort([&] { Object = NewObject(Outer); AutoRTFM::AbortTransaction(); }); REQUIRE(nullptr == Object); REQUIRE(42 == Outer->Value); } } TEST_CASE("UObject.Rename") { const TCHAR* Cat = TEXT("Cat"); const TCHAR* Dog = TEXT("Dog"); const TCHAR* Bat = TEXT("Bat"); UMyAutoRTFMTestObject* OuterA = NewObject(); UMyAutoRTFMTestObject* OuterB = NewObject(); UMyAutoRTFMTestObject* OuterC = NewObject(); UMyAutoRTFMTestObject* Object = NewObject(OuterA, Cat); REQUIRE(Object->GetOuter() == OuterA); SECTION("Commit(Rename(Name))") { AutoRTFM::Testing::Commit([&] { REQUIRE(Object->Rename(Dog, OuterA)); }); REQUIRE(Object->GetName() == Dog); REQUIRE(Object->GetOuter() == OuterA); } SECTION("Abort(Rename(Name))") { AutoRTFM::Testing::Abort([&] { REQUIRE(Object->Rename(Dog, OuterA)); AutoRTFM::AbortTransaction(); }); REQUIRE(Object->GetName() == Cat); REQUIRE(Object->GetOuter() == OuterA); } SECTION("Commit(Rename(Name), Rename(Name))") { AutoRTFM::Testing::Commit([&] { REQUIRE(Object->Rename(Dog, OuterA)); REQUIRE(Object->Rename(Bat, OuterA)); }); REQUIRE(Object->GetName() == Bat); REQUIRE(Object->GetOuter() == OuterA); } SECTION("Abort(Rename(Name), Rename(Name))") { AutoRTFM::Testing::Abort([&] { REQUIRE(Object->Rename(Dog, OuterA)); REQUIRE(Object->Rename(Bat, OuterA)); AutoRTFM::AbortTransaction(); }); REQUIRE(Object->GetName() == Cat); REQUIRE(Object->GetOuter() == OuterA); } SECTION("Commit(Rename(Name), Commit(Rename(Name)))") { AutoRTFM::Testing::Commit([&] { REQUIRE(Object->Rename(Dog, OuterA)); AutoRTFM::Testing::Commit([&] { REQUIRE(Object->Rename(Bat, OuterA)); }); }); REQUIRE(Object->GetName() == Bat); REQUIRE(Object->GetOuter() == OuterA); } SECTION("Commit(Rename(Name), Abort(Rename(Name)))") { AutoRTFM::Testing::Commit([&] { REQUIRE(Object->Rename(Dog, OuterA)); AutoRTFM::Testing::Abort([&] { REQUIRE(Object->Rename(Bat, OuterA)); AutoRTFM::AbortTransaction(); }); }); REQUIRE(Object->GetName() == Dog); REQUIRE(Object->GetOuter() == OuterA); } SECTION("Abort(Rename(Name), Commit(Rename(Name)))") { AutoRTFM::Testing::Abort([&] { REQUIRE(Object->Rename(Bat, OuterA)); AutoRTFM::Testing::Commit([&] { REQUIRE(Object->Rename(Dog, OuterA)); }); AutoRTFM::AbortTransaction(); }); REQUIRE(Object->GetName() == Cat); REQUIRE(Object->GetOuter() == OuterA); } SECTION("Abort(Rename(Name), Commit(Rename(Name)))") { AutoRTFM::Testing::Abort([&] { REQUIRE(Object->Rename(Bat, OuterA)); AutoRTFM::Testing::Abort([&] { REQUIRE(Object->Rename(Dog, OuterA)); AutoRTFM::AbortTransaction(); }); AutoRTFM::AbortTransaction(); }); REQUIRE(Object->GetName() == Cat); REQUIRE(Object->GetOuter() == OuterA); } SECTION("Commit(Rename(Object))") { AutoRTFM::Testing::Commit([&] { REQUIRE(Object->Rename(nullptr, OuterB)); }); REQUIRE(Object->GetName() == Cat); REQUIRE(Object->GetOuter() == OuterB); } SECTION("Abort(Rename(Object))") { AutoRTFM::Testing::Abort([&] { REQUIRE(Object->Rename(nullptr, OuterB)); AutoRTFM::AbortTransaction(); }); REQUIRE(Object->GetName() == Cat); REQUIRE(Object->GetOuter() == OuterA); } SECTION("Commit(Rename(Object), Rename(Object))") { AutoRTFM::Testing::Commit([&] { REQUIRE(Object->Rename(nullptr, OuterB)); REQUIRE(Object->Rename(nullptr, OuterC)); }); REQUIRE(Object->GetName() == Cat); REQUIRE(Object->GetOuter() == OuterC); } SECTION("Abort(Rename(Object), Rename(Object))") { AutoRTFM::Testing::Abort([&] { REQUIRE(Object->Rename(nullptr, OuterB)); REQUIRE(Object->Rename(nullptr, OuterC)); AutoRTFM::AbortTransaction(); }); REQUIRE(Object->GetName() == Cat); REQUIRE(Object->GetOuter() == OuterA); } SECTION("Commit(Rename(Object), Commit(Rename(Object)))") { AutoRTFM::Testing::Commit([&] { REQUIRE(Object->Rename(nullptr, OuterB)); AutoRTFM::Testing::Commit([&] { REQUIRE(Object->Rename(nullptr, OuterC)); }); }); REQUIRE(Object->GetName() == Cat); REQUIRE(Object->GetOuter() == OuterC); } SECTION("Commit(Rename(Object), Abort(Rename(Object)))") { AutoRTFM::Testing::Commit([&] { REQUIRE(Object->Rename(nullptr, OuterB)); AutoRTFM::Testing::Abort([&] { REQUIRE(Object->Rename(nullptr, OuterC)); AutoRTFM::AbortTransaction(); }); }); REQUIRE(Object->GetName() == Cat); REQUIRE(Object->GetOuter() == OuterB); } SECTION("Abort(Rename(Object), Commit(Rename(Object)))") { AutoRTFM::Testing::Abort([&] { REQUIRE(Object->Rename(nullptr, OuterB)); AutoRTFM::Testing::Commit([&] { REQUIRE(Object->Rename(nullptr, OuterC)); }); AutoRTFM::AbortTransaction(); }); REQUIRE(Object->GetName() == Cat); REQUIRE(Object->GetOuter() == OuterA); } SECTION("Abort(Rename(Object), Commit(Rename(Object)))") { AutoRTFM::Testing::Abort([&] { REQUIRE(Object->Rename(nullptr, OuterB)); AutoRTFM::Testing::Abort([&] { REQUIRE(Object->Rename(nullptr, OuterC)); AutoRTFM::AbortTransaction(); }); AutoRTFM::AbortTransaction(); }); REQUIRE(Object->GetName() == Cat); REQUIRE(Object->GetOuter() == OuterA); } } // This is a copy of the helper function in TestGarbageCollector.cpp. int32 PerformGarbageCollectionWithIncrementalReachabilityAnalysis(TFunctionRef ReachabilityIterationCallback) { int32 ReachabilityIterationIndex = 0; CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS, false); while (IsIncrementalReachabilityAnalysisPending()) { if (ReachabilityIterationCallback(ReachabilityIterationIndex)) { break; } // Re-check if incremental rachability is still pending because the callback above could've triggered GC which would complete all iterations if (IsIncrementalReachabilityAnalysisPending()) { PerformIncrementalReachabilityAnalysis(GetReachabilityAnalysisTimeLimit()); ReachabilityIterationIndex++; } } if (IsIncrementalPurgePending()) { IncrementalPurgeGarbage(false); } check(IsIncrementalPurgePending() == false); return ReachabilityIterationIndex + 1; } TEST_CASE("UObject.MarkAsReachable") { // We need incremental reachability to be on. SetIncrementalReachabilityAnalysisEnabled(true); // Cache the original time limit. const float Original = GetReachabilityAnalysisTimeLimit(); // And we need a super small time limit s that reachability analysis will definitely have started. SetReachabilityAnalysisTimeLimit(FLT_MIN); // We need to be sure we've done the static GC initialization before we start doing a garbage // collection. FGCObject::StaticInit(); UMyAutoRTFMTestObject* const Object = NewObject(); // Somewhat ironically, garbage collection can leak memory. MALLOCLEAK_IGNORE_SCOPE(); PerformGarbageCollectionWithIncrementalReachabilityAnalysis([Object](int32 index) { if (0 != index) { return true; } AutoRTFM::Testing::Commit([&] { Object->MarkAsReachable(); }); return false; }); // Reset it back just incase another test required the original time limit. SetReachabilityAnalysisTimeLimit(Original); } TEST_CASE("FUObjectAnnotationSparse.AddAnnotation") { struct FTestAnnotation { FTestAnnotation() : TestAnnotationNumber(42) { } int TestAnnotationNumber; bool IsDefault() const { return TestAnnotationNumber == 42; } bool operator==(const FTestAnnotation& Other) const { return Other.TestAnnotationNumber == TestAnnotationNumber; } }; FUObjectAnnotationSparse AnnotationMap; UMyAutoRTFMTestObject* Key = NewObject(); UMyAutoRTFMTestObject* Key2 = NewObject(); FTestAnnotation ValueA; ValueA.TestAnnotationNumber = 10; FTestAnnotation ValueB; ValueB.TestAnnotationNumber = 20; FTestAnnotation ValueC; ValueC.TestAnnotationNumber = 30; SECTION("Add") { SECTION("Commit") { AutoRTFM::Testing::Commit([&] { REQUIRE(FTestAnnotation() == AnnotationMap.GetAnnotation(Key)); AnnotationMap.AddAnnotation(Key, ValueA); REQUIRE(ValueA == AnnotationMap.GetAnnotation(Key)); }); REQUIRE(ValueA == AnnotationMap.GetAnnotation(Key)); } SECTION("Abort") { AutoRTFM::Testing::Abort([&] { AnnotationMap.AddAnnotation(Key, ValueA); AutoRTFM::AbortTransaction(); }); REQUIRE(FTestAnnotation() == AnnotationMap.GetAnnotation(Key)); } } SECTION("Replace") { AnnotationMap.AddAnnotation(Key, ValueB); SECTION("Commit") { AutoRTFM::Testing::Commit([&] { REQUIRE(ValueB == AnnotationMap.GetAnnotation(Key)); AnnotationMap.AddAnnotation(Key, ValueA); REQUIRE(ValueA == AnnotationMap.GetAnnotation(Key)); }); REQUIRE(ValueA == AnnotationMap.GetAnnotation(Key)); } SECTION("Abort") { AutoRTFM::Testing::Abort([&] { AnnotationMap.AddAnnotation(Key, ValueA); AutoRTFM::AbortTransaction(); }); REQUIRE(ValueB == AnnotationMap.GetAnnotation(Key)); } } SECTION("Add, Commit(Remove), Get") { AnnotationMap.AddAnnotation(Key, ValueC); AutoRTFM::Testing::Commit([&] { AnnotationMap.RemoveAnnotation(Key); REQUIRE(AnnotationMap.GetAnnotation(Key) == FTestAnnotation{}); }); REQUIRE(AnnotationMap.GetAnnotation(Key) == FTestAnnotation{}); } SECTION("Add, Abort(Remove), Get") { AnnotationMap.AddAnnotation(Key, ValueC); AutoRTFM::Testing::Abort([&] { AnnotationMap.RemoveAnnotation(Key); REQUIRE(AnnotationMap.GetAnnotation(Key) == FTestAnnotation{}); AutoRTFM::AbortTransaction(); }); REQUIRE(AnnotationMap.GetAnnotation(Key) == ValueC); } SECTION("Add 1, Add 2, Commit(Get 1), Get 2") { AnnotationMap.AddAnnotation(Key, ValueA); AnnotationMap.AddAnnotation(Key2, ValueB); AutoRTFM::Testing::Commit([&] { REQUIRE(AnnotationMap.GetAnnotation(Key) == ValueA); }); REQUIRE(AnnotationMap.GetAnnotation(Key2) == ValueB); } SECTION("Add 1, Add 2, Abort(Get 1), Get 2") { AnnotationMap.AddAnnotation(Key, ValueA); AnnotationMap.AddAnnotation(Key2, ValueB); AutoRTFM::Testing::Abort([&] { REQUIRE(AnnotationMap.GetAnnotation(Key) == ValueA); AutoRTFM::AbortTransaction(); }); REQUIRE(AnnotationMap.GetAnnotation(Key2) == ValueB); } SECTION("Add 1, Add 2, Open(Get 1), Get 2") { AnnotationMap.AddAnnotation(Key, ValueA); AnnotationMap.AddAnnotation(Key2, ValueB); AutoRTFM::Testing::Commit([&] { AutoRTFM::Open([&] { REQUIRE(AnnotationMap.GetAnnotation(Key) == ValueA); }); }); REQUIRE(AnnotationMap.GetAnnotation(Key2) == ValueB); } } struct FAnnotationObject { UObject* Object = nullptr; FAnnotationObject() {} FAnnotationObject(UObject* InObject) : Object(InObject) {} bool IsDefault() { return !Object; } }; template <> struct TIsPODType { enum { Value = true }; }; TEST_CASE("UObject.AnnotationMap") { FUObjectAnnotationSparse AnnotationMap; UObject* Key = NewObject(); AutoRTFM::Testing::Commit([&] { UObject* Value = NewObject(); AnnotationMap.GetAnnotation(Key); AnnotationMap.AddAnnotation(Key, Value); }); REQUIRE(!AnnotationMap.GetAnnotation(Key).IsDefault()); } TEST_CASE("UObject.AtomicallySetFlags") { UObject* const Object = NewObject(); constexpr EObjectFlags OldFlags = EObjectFlags::RF_Public | EObjectFlags::RF_Transient; constexpr EObjectFlags FlagsToAdd = EObjectFlags::RF_Transient | EObjectFlags::RF_AllocatedInSharedPage; // We need to ensure we cover the case where we are adding a flag that is already there // and thus cannot just wipe that out if we abort! Object->AtomicallyClearFlags(FlagsToAdd); Object->AtomicallySetFlags(OldFlags); REQUIRE(Object->HasAllFlags(OldFlags) & !Object->HasAllFlags(FlagsToAdd)); AutoRTFM::Testing::Abort([&] { Object->AtomicallySetFlags(FlagsToAdd); AutoRTFM::AbortTransaction(); }); REQUIRE(Object->HasAllFlags(OldFlags) & !Object->HasAllFlags(FlagsToAdd)); AutoRTFM::Testing::Commit([&] { Object->AtomicallySetFlags(FlagsToAdd); }); REQUIRE(Object->HasAllFlags(OldFlags) & Object->HasAllFlags(FlagsToAdd)); } TEST_CASE("UObject.AtomicallyClearFlags") { UObject* const Object = NewObject(); constexpr EObjectFlags OldFlags = EObjectFlags::RF_Public | EObjectFlags::RF_Transient; constexpr EObjectFlags FlagsToClear = EObjectFlags::RF_Transient | EObjectFlags::RF_AllocatedInSharedPage; // We need to ensure we cover the case where we are adding a flag that is already there // and thus cannot just wipe that out if we abort! Object->AtomicallyClearFlags(FlagsToClear); Object->AtomicallySetFlags(OldFlags); REQUIRE(Object->HasAllFlags(OldFlags) & !Object->HasAllFlags(FlagsToClear)); AutoRTFM::Testing::Abort([&] { Object->AtomicallyClearFlags(FlagsToClear); AutoRTFM::AbortTransaction(); }); REQUIRE(Object->HasAllFlags(OldFlags) & !Object->HasAllFlags(FlagsToClear)); AutoRTFM::Testing::Commit([&] { Object->AtomicallyClearFlags(FlagsToClear); }); REQUIRE(Object->HasAnyFlags(OldFlags) & !Object->HasAllFlags(FlagsToClear)); } // Tests that constructing a UObject in both the open and closed doesn't result // in a corrupt FUObjectThreadContext. See SOL-7131. TEST_CASE("UObject.FUObjectThreadContext") { AutoRTFM::TScopedGuard CallbackScope(UMyAutoRTFMTestObject::ConstructorCallback, nullptr); struct Fns { static void CreateObjectWithCtor(UMyAutoRTFMTestObject::FConstructorCallback* Ctor) { AutoRTFM::Open([&] { UMyAutoRTFMTestObject::ConstructorCallback = Ctor; }); NewObject(); } static void CtorCreateInnerClosed(const FObjectInitializer& ObjectInitializer, UMyAutoRTFMTestObject& Object) { AutoRTFM::EContextStatus Status = AutoRTFM::Close([&] { REQUIRE(1 == FUObjectThreadContext::Get().IsInConstructor); CreateObjectWithCtor(nullptr); REQUIRE(1 == FUObjectThreadContext::Get().IsInConstructor); }); REQUIRE(AutoRTFM::EContextStatus::OnTrack == Status); } static void CtorCreateInnerTransact(const FObjectInitializer& ObjectInitializer, UMyAutoRTFMTestObject& Object) { AutoRTFM::Testing::Commit([&] { REQUIRE(1 == FUObjectThreadContext::Get().IsInConstructor); CreateObjectWithCtor(nullptr); REQUIRE(1 == FUObjectThreadContext::Get().IsInConstructor); }); } static void CtorAbort(const FObjectInitializer& ObjectInitializer, UMyAutoRTFMTestObject& Object) { REQUIRE(1 == FUObjectThreadContext::Get().IsInConstructor); AutoRTFM::EContextStatus Status = AutoRTFM::Close([&] { AutoRTFM::AbortTransaction(); FAIL(/* unreachable */); }); REQUIRE(AutoRTFM::EContextStatus::AbortedByRequest == Status); } }; SECTION("Transact(UObjectCtor(Abort))") { AutoRTFM::Testing::Abort([&] { REQUIRE(AutoRTFM::IsClosed()); REQUIRE(0 == FUObjectThreadContext::Get().IsInConstructor); Fns::CreateObjectWithCtor(Fns::CtorAbort); FAIL(/* unreachable */); }); REQUIRE(0 == FUObjectThreadContext::Get().IsInConstructor); } SECTION("Transact(UObjectCtor(), Abort)") { AutoRTFM::Testing::Abort([&] { REQUIRE(AutoRTFM::IsClosed()); REQUIRE(0 == FUObjectThreadContext::Get().IsInConstructor); NewObject(); REQUIRE(0 == FUObjectThreadContext::Get().IsInConstructor); AutoRTFM::AbortTransaction(); FAIL(/* unreachable */); }); REQUIRE(0 == FUObjectThreadContext::Get().IsInConstructor); } SECTION("Transact(Open(UObjectCtor(Rollback)), UObjectCtor(Abort))") { AutoRTFM::Testing::Abort([&] { REQUIRE(AutoRTFM::IsClosed()); REQUIRE(0 == FUObjectThreadContext::Get().IsInConstructor); AutoRTFM::Open([&] { AutoRTFM::ForTheRuntime::RollbackTransaction(); }); REQUIRE(0 == FUObjectThreadContext::Get().IsInConstructor); Fns::CreateObjectWithCtor(Fns::CtorAbort); FAIL(/* unreachable */); }); REQUIRE(0 == FUObjectThreadContext::Get().IsInConstructor); } SECTION("Transact(Open(UObjectCtor(Transact(UObjectCtor))), UObjectCtor(Abort))") { AutoRTFM::Testing::Abort([&] { REQUIRE(AutoRTFM::IsClosed()); REQUIRE(0 == FUObjectThreadContext::Get().IsInConstructor); AutoRTFM::Open([&] { Fns::CreateObjectWithCtor(Fns::CtorCreateInnerTransact); }); REQUIRE(0 == FUObjectThreadContext::Get().IsInConstructor); Fns::CreateObjectWithCtor(Fns::CtorAbort); FAIL(/* unreachable */); }); REQUIRE(0 == FUObjectThreadContext::Get().IsInConstructor); } SECTION("Transact(Open(UObjectCtor(Transact(UObjectCtor))), Abort)") { AutoRTFM::Testing::Abort([&] { REQUIRE(AutoRTFM::IsClosed()); REQUIRE(0 == FUObjectThreadContext::Get().IsInConstructor); AutoRTFM::Open([&] { Fns::CreateObjectWithCtor(Fns::CtorCreateInnerTransact); }); REQUIRE(0 == FUObjectThreadContext::Get().IsInConstructor); AutoRTFM::AbortTransaction(); FAIL(/* unreachable */); }); REQUIRE(0 == FUObjectThreadContext::Get().IsInConstructor); } SECTION("Transact(Open(UObjectCtor(Close(UObjectCtor))), UObjectCtor(Abort))") { AutoRTFM::Testing::Abort([&] { REQUIRE(AutoRTFM::IsClosed()); REQUIRE(0 == FUObjectThreadContext::Get().IsInConstructor); AutoRTFM::Open([&] { Fns::CreateObjectWithCtor(Fns::CtorCreateInnerClosed); }); REQUIRE(0 == FUObjectThreadContext::Get().IsInConstructor); Fns::CreateObjectWithCtor(Fns::CtorAbort); FAIL(/* unreachable */); }); REQUIRE(0 == FUObjectThreadContext::Get().IsInConstructor); } SECTION("Transact(Open(UObjectCtor(Close(UObjectCtor))), Abort)") { AutoRTFM::Testing::Abort([&] { REQUIRE(AutoRTFM::IsClosed()); REQUIRE(0 == FUObjectThreadContext::Get().IsInConstructor); AutoRTFM::Open([&] { Fns::CreateObjectWithCtor(Fns::CtorCreateInnerClosed); }); REQUIRE(0 == FUObjectThreadContext::Get().IsInConstructor); AutoRTFM::AbortTransaction(); FAIL(/* unreachable */); }); REQUIRE(0 == FUObjectThreadContext::Get().IsInConstructor); } } TEST_CASE("UObject.AddRef") { SECTION("Default") { UMyAutoRTFMTestObject* Object = NewObject(); AutoRTFM::Testing::Commit([&] { Object->AddRef(); }); Object->ReleaseRef(); } } TEST_CASE("UObject.ReleaseRef") { SECTION("Default") { UMyAutoRTFMTestObject* Object = NewObject(); Object->AddRef(); AutoRTFM::Testing::Commit([&] { Object->ReleaseRef(); }); } SECTION("With Cascading Abort") { UMyAutoRTFMTestObject* Object = NewObject(); Object->AddRef(); bool bFirst = true; AutoRTFM::Testing::Commit([&] { Object->ReleaseRef(); if (bFirst) { AutoRTFM::CascadingRetryTransaction([&] { bFirst = false; }); } }); } } TEST_CASE("TObjectPtr") { UMyAutoRTFMTestObject* Object = NewObject(); SECTION("Construct") { SECTION("Commit") { AutoRTFM::Testing::Commit([&] { TObjectPtr ObjectPtr(Object); }); } SECTION("Abort") { AutoRTFM::Testing::Abort([&] { TObjectPtr ObjectPtr(Object); AutoRTFM::AbortTransaction(); }); } } SECTION("Destruct") { std::optional> ObjectPtr = TObjectPtr(Object); SECTION("Commit") { AutoRTFM::Testing::Commit([&] { ObjectPtr.reset(); }); } SECTION("Abort") { AutoRTFM::Testing::Abort([&] { ObjectPtr.reset(); AutoRTFM::AbortTransaction(); }); } } } TEST_CASE("FGCObject") { struct FMyGCObject : FGCObject { bool bAlive = true; ~FMyGCObject() { REQUIRE(bAlive); bAlive = false; } void AddReferencedObjects([[maybe_unused]] FReferenceCollector& Collector) override { REQUIRE(bAlive); } FString GetReferencerName() const override { REQUIRE(bAlive); return {}; } }; auto Test = [&](auto&& Callback) { SECTION("NonTransactional") { Callback(); } SECTION("Commit") { AutoRTFM::Testing::Commit([&] { Callback(); }); } SECTION("Abort") { AutoRTFM::Testing::Abort([&] { Callback(); AutoRTFM::AbortTransaction(); }); } }; // Tests for FGCObject that is allocated on the heap. SECTION("Heap") { SECTION("Transact(Construct, Destruct)") { Test([&] { TUniquePtr Object = MakeUnique(); }); } SECTION("Transact(Construct, Unregister, Register, Destruct)") { Test([&] { TUniquePtr Object = MakeUnique(); Object->UnregisterGCObject(); Object->RegisterGCObject(); }); } SECTION("Transact(Construct, Unregister, Destruct)") { Test([&] { TUniquePtr Object = MakeUnique(); Object->UnregisterGCObject(); }); } SECTION("Transact(Construct, Register, Destruct)") { Test([&] { TUniquePtr Object = MakeUnique(); Object->RegisterGCObject(); }); } SECTION("Transact(Construct, Register, Unregister, Destruct)") { Test([&] { TUniquePtr Object = MakeUnique(); Object->RegisterGCObject(); Object->UnregisterGCObject(); }); } SECTION("Transact(Construct, Unregister, Unregister, Register, Register, Destruct)") { Test([&] { TUniquePtr Object = MakeUnique(); Object->UnregisterGCObject(); Object->UnregisterGCObject(); Object->RegisterGCObject(); Object->RegisterGCObject(); }); } SECTION("Transact(Construct), Destruct") { TUniquePtr Object; Test([&] { Object = MakeUnique(); }); } SECTION("Construct, Transact(Destruct)") { TUniquePtr Object = MakeUnique(); Test([&] { Object.Reset(); }); } SECTION("Construct, Transact(Unregister), Destruct") { TUniquePtr Object = MakeUnique(); Test([&] { Object->UnregisterGCObject(); }); } SECTION("Construct, Transact(Unregister, Register), Destruct") { TUniquePtr Object = MakeUnique(); Test([&] { Object->UnregisterGCObject(); Object->RegisterGCObject(); }); } SECTION("Construct, Transact(Unregister, Destruct)") { TUniquePtr Object = MakeUnique(); Test([&] { Object->UnregisterGCObject(); Object.Reset(); }); } SECTION("Construct, Transact(Unregister, Register, Destruct)") { TUniquePtr Object = MakeUnique(); Test([&] { Object->UnregisterGCObject(); Object->RegisterGCObject(); Object.Reset(); }); } SECTION("Construct, Transact(Unregister, Transact(Register)), Destruct") { TUniquePtr Object = MakeUnique(); Test([&] { Object->UnregisterGCObject(); Test([&] { Object->RegisterGCObject(); }); }); } SECTION("Construct, Transact(Unregister, Register, Transact(Unregister)), Destruct") { TUniquePtr Object = MakeUnique(); Test([&] { Object->UnregisterGCObject(); Object->RegisterGCObject(); Test([&] { Object->UnregisterGCObject(); }); }); } SECTION("Construct, Transact(Unregister, Transact(Register), Destruct)") { TUniquePtr Object = MakeUnique(); Test([&] { Object->UnregisterGCObject(); Test([&] { Object->RegisterGCObject(); }); Object.Reset(); }); } SECTION("Construct, Transact(Unregister, Register, Transact(Unregister), Destruct)") { TUniquePtr Object = MakeUnique(); Test([&] { Object->UnregisterGCObject(); Object->RegisterGCObject(); Test([&] { Object->UnregisterGCObject(); }); Object.Reset(); }); } } // Tests for FGCObject that is allocated within the transaction's stack. SECTION("Stack") { SECTION("Transact(Construct, Destruct)") { Test([&] { FMyGCObject MyGCObject; }); } SECTION("Transact(Construct, Unregister, Destruct)") { Test([&] { FMyGCObject MyGCObject; MyGCObject.UnregisterGCObject(); }); } SECTION("Transact(Construct, Unregister, Unregister, Destruct)") { Test([&] { FMyGCObject MyGCObject; MyGCObject.UnregisterGCObject(); MyGCObject.UnregisterGCObject(); MyGCObject.RegisterGCObject(); }); } SECTION("Transact(Construct, Unregister, Register, Destruct)") { Test([&] { FMyGCObject MyGCObject; MyGCObject.UnregisterGCObject(); MyGCObject.RegisterGCObject(); }); } SECTION("Transact(Construct, Register, Register, Destruct)") { Test([&] { FMyGCObject MyGCObject; MyGCObject.RegisterGCObject(); MyGCObject.RegisterGCObject(); }); } SECTION("Transact(Construct, Register, Unregister, Register, Destruct)") { Test([&] { FMyGCObject MyGCObject; MyGCObject.RegisterGCObject(); MyGCObject.UnregisterGCObject(); MyGCObject.RegisterGCObject(); }); } SECTION("Transact(Construct, Transact(Unregister, Register), Destruct)") { Test([&] { FMyGCObject MyGCObject; Test([&] { MyGCObject.UnregisterGCObject(); MyGCObject.RegisterGCObject(); }); }); } SECTION("Transact(Construct, Unregister, Transact(Register), Destruct)") { Test([&] { FMyGCObject MyGCObject; MyGCObject.UnregisterGCObject(); Test([&] { MyGCObject.RegisterGCObject(); }); }); } SECTION("Transact(Construct, Transact(Unregister), Register, Destruct)") { Test([&] { FMyGCObject MyGCObject; Test([&] { MyGCObject.UnregisterGCObject(); }); MyGCObject.RegisterGCObject(); }); } SECTION("Construct, Transact(Destruct)") { std::optional MyGCObject{FMyGCObject{}}; Test([&] { MyGCObject.reset(); }); } SECTION("Transact(Construct), Destruct") { std::optional MyGCObject; Test([&] { MyGCObject = FMyGCObject{}; }); } SECTION("Construct, Transact(Unregister, Destruct)") { std::optional MyGCObject{FMyGCObject{}}; Test([&] { MyGCObject->UnregisterGCObject(); MyGCObject.reset(); }); } SECTION("Transact(Construct, Unregister), Destruct") { std::optional MyGCObject; Test([&] { MyGCObject = FMyGCObject{}; MyGCObject->UnregisterGCObject(); }); } } }