Files
UnrealEngineUWP/Engine/Source/Runtime/CoreUObject/Tests/ObjectPropertyTest.cpp
joe pribele 627db198b3 change to FObjectPtrProperty::Identical to avoid loading lazy loaded objects
https://p4-swarm.epicgames.net/reviews/25288349
#rb zousar.shaker
#preflight 64598eb5023fe5d3ad6df74d

[CL 25381637 by joe pribele in ue5-main branch]
2023-05-08 20:13:30 -04:00

444 lines
15 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#if WITH_LOW_LEVEL_TESTS
#include "TestHarness.h"
#include "TestMacros/Assertions.h"
#include "ObjectPtrTestClass.h"
#include "UObject/Package.h"
#include "Misc/ScopeExit.h"
#include "UObject/UnrealType.h"
#include "UObject/LinkerPlaceholderExportObject.h"
#include "UObject/LinkerPlaceholderClass.h"
#include "UObject/ObjectHandleTracking.h"
#include "LowLevelTestsRunner/WarnFilterScope.h"
#include "Misc/AssetRegistryInterface.h"
#include "AssetRegistry/AssetData.h"
#include "ObjectRefTrackingTestBase.h"
TEST_CASE("UE::CoreUObject::FObjectProperty::CheckValidAddress")
{
#if UE_WITH_OBJECT_HANDLE_TRACKING
auto CallbackHandle = UE::CoreUObject::AddObjectHandleReferenceResolvedCallback([](const FObjectRef&, UPackage*, UObject*)
{
FAIL("Unexpected resolve during CheckValidObject");
});
ON_SCOPE_EXIT
{
UE::CoreUObject::RemoveObjectHandleReferenceResolvedCallback(CallbackHandle);
};
#endif
UClass* Class = UObjectPtrTestClassWithRef::StaticClass();
FObjectProperty* Property = CastField<FObjectProperty>(Class->FindPropertyByName(TEXT("ObjectPtr")));
REQUIRE(Property != nullptr);
UPackage* TestPackage = NewObject<UPackage>(nullptr, TEXT("TestPackageName"), RF_Transient);
TestPackage->AddToRoot();
ON_SCOPE_EXIT
{
TestPackage->RemoveFromRoot();
};
UObjectPtrTestClassWithRef* Obj = NewObject<UObjectPtrTestClassWithRef>(TestPackage, TEXT("Object"));
UObjectPtrTestClass* Other = NewObject<UObjectPtrTestClass>(Obj, TEXT("Other"));
#if UE_WITH_OBJECT_HANDLE_TRACKING
FObjectHandle Handle = MakeUnresolvedHandle(Other);
TObjectPtr<UObjectPtrTestClass> ObjectPtr = *reinterpret_cast<TObjectPtr<UObjectPtrTestClass>*>(&Handle);
#else
TObjectPtr<UObjectPtrTestClass> ObjectPtr = Other;
#endif
//verify nothing happens by default
CHECK(!Obj->ObjectPtr);
Property->CheckValidObject(&Obj->ObjectPtr, nullptr);
CHECK(!Obj->ObjectPtr);
//valid assignment
Obj->ObjectPtr = ObjectPtr;
Property->CheckValidObject(&Obj->ObjectPtr, nullptr);
CHECK(Obj->ObjectPtr == ObjectPtr);
//assign a bad value to the pointer
Obj->ObjectPtr = reinterpret_cast<UObjectPtrTestClass*>(Obj);
CHECK(Obj->ObjectPtr != nullptr);
UE::Testing::FWarnFilterScope _([](const TCHAR* Message, ELogVerbosity::Type Verbosity, const FName& Category)
{
if (Category == TEXT("LogProperty") && FCString::Strstr(Message, TEXT("Reference will be nullptred")) && Verbosity == ELogVerbosity::Type::Warning)
{
return true;
}
return false;
});
Property->CheckValidObject(&Obj->ObjectPtr, ObjectPtr);
CHECK(!Obj->ObjectPtr); //value should be nulled since the type was not compatible
}
TEST_CASE("UE::CoreUObject::FObjectProperty::CheckValidAddressNonNullable")
{
bool bAllowRead = false;
#if UE_WITH_OBJECT_HANDLE_TRACKING
auto CallbackHandle = UE::CoreUObject::AddObjectHandleReferenceResolvedCallback([&bAllowRead](const FObjectRef & SourceRef, UPackage * ClassPackage, UObject* Object)
{
if (!bAllowRead)
{
FAIL("Unexpected resolve during CheckValidObject");
}
});
ON_SCOPE_EXIT
{
UE::CoreUObject::RemoveObjectHandleReferenceResolvedCallback(CallbackHandle);
};
#endif
UClass* Class = UObjectPtrTestClassWithRef::StaticClass();
FObjectProperty* Property = CastField<FObjectProperty>(Class->FindPropertyByName(TEXT("ObjectPtrNonNullable")));
REQUIRE(Property != nullptr);
UPackage* TestPackage = NewObject<UPackage>(nullptr, TEXT("TestPackageName"), RF_Transient);
TestPackage->AddToRoot();
ON_SCOPE_EXIT
{
TestPackage->RemoveFromRoot();
};
UObjectPtrTestClassWithRef* Obj = NewObject<UObjectPtrTestClassWithRef>(TestPackage, TEXT("Object"));
UObjectPtrTestClass* Other = NewObject<UObjectPtrTestClass>(Obj, TEXT("Other"));
#if UE_WITH_OBJECT_HANDLE_TRACKING
FObjectHandle Handle = MakeUnresolvedHandle(Other);
TObjectPtr<UObjectPtrTestClass> ObjectPtr = *reinterpret_cast<TObjectPtr<UObjectPtrTestClass>*>(&Handle);
#else
TObjectPtr<UObjectPtrTestClass> ObjectPtr = Other;
#endif
//property is already null should stay null
CHECK(!Obj->ObjectPtrNonNullable);
Property->CheckValidObject(&Obj->ObjectPtrNonNullable, ObjectPtr);
CHECK(!Obj->ObjectPtrNonNullable);
//valid assignment
Obj->ObjectPtrNonNullable = ObjectPtr;
Property->CheckValidObject(&Obj->ObjectPtrNonNullable, nullptr);
CHECK(Obj->ObjectPtrNonNullable == ObjectPtr);
bAllowRead = true; //has resolve the old value to construct a new default value for the property
//assign a bad value to the pointer
Obj->ObjectPtrNonNullable = reinterpret_cast<UObjectPtrTestClass*>(Obj);
CHECK(Obj->ObjectPtrNonNullable != nullptr);
UE::Testing::FWarnFilterScope _([](const TCHAR* Message, ELogVerbosity::Type Verbosity, const FName& Category)
{
if (Category == TEXT("LogProperty") && FCString::Strstr(Message, TEXT("Reference will be defaulted to")) && Verbosity == ELogVerbosity::Type::Warning)
{
return true;
}
return false;
});
Property->CheckValidObject(&Obj->ObjectPtrNonNullable, ObjectPtr);
CHECK(Obj->ObjectPtrNonNullable == ObjectPtr); //non nullable properties should be assigned the old value
//assign a bad value to the pointer
Obj->ObjectPtrNonNullable = reinterpret_cast<UObjectPtrTestClass*>(Obj);
CHECK(Obj->ObjectPtrNonNullable != nullptr);
//new value is required for non nullable properties
Property->CheckValidObject(&Obj->ObjectPtrNonNullable, nullptr);
CHECK(Obj->ObjectPtrNonNullable != nullptr);
CHECK(Obj->ObjectPtrNonNullable->IsA(UObjectPtrTestClass::StaticClass()));
}
class FMockArchive : public FArchive
{
public:
TFunction<void(FObjectPtr& Value)> OnFObjectPtr;
virtual FArchive& operator<<(FObjectPtr& Value)
{
if (OnFObjectPtr)
OnFObjectPtr(Value);
return *this;
}
};
template<typename T>
static void TestSerializeItem(FName ObjectName)
{
UClass* Class = T::StaticClass();
FObjectProperty* Property = CastField<FObjectProperty>(Class->FindPropertyByName(TEXT("ObjectPtr")));
REQUIRE(Property != nullptr);
UPackage* TestPackage = NewObject<UPackage>(nullptr, TEXT("TestPackageName"), RF_Transient);
TestPackage->AddToRoot();
ON_SCOPE_EXIT
{
TestPackage->RemoveFromRoot();
};
T* Obj = NewObject<T>(TestPackage, ObjectName);
UObjectPtrTestClass* Other = NewObject<UObjectPtrTestClass>(Obj, TEXT("Other"));
ULinkerPlaceholderExportObject* PlaceHolderExport = NewObject<ULinkerPlaceholderExportObject>(TestPackage, TEXT("PlaceHolderExport"));
ULinkerPlaceholderClass* PlaceHolderClass = NewObject<ULinkerPlaceholderClass>(TestPackage, TEXT("PlaceHolderClass"));
PlaceHolderClass->Bind(); //must call bind or crashes on shutdown
int ResolveCount = 0;
#if UE_WITH_OBJECT_HANDLE_TRACKING
auto CallbackId = UE::CoreUObject::AddObjectHandleReferenceResolvedCallback([&ResolveCount](const FObjectRef&, UPackage*, UObject*)
{
++ResolveCount;
});
ON_SCOPE_EXIT
{
UE::CoreUObject::RemoveObjectHandleReferenceResolvedCallback(CallbackId);
};
#endif
{
//verify that if the property is null no reads are triggered
FMockArchive MockArchive;
MockArchive.OnFObjectPtr = [&](FObjectPtr& Value)
{
Value = PlaceHolderExport;
};
FBinaryArchiveFormatter Formatter(MockArchive);
FStructuredArchive Ar(Formatter);
FStructuredArchiveSlot Slot = Ar.Open();
FObjectPtrProperty::StaticSerializeItem(Property, Slot, &Obj->ObjectPtr, nullptr);
CHECK(ResolveCount == 0);
CHECK(*reinterpret_cast<ULinkerPlaceholderExportObject**>(&Obj->ObjectPtr) == PlaceHolderExport);
}
{
//verify that if the property is not null no reads are triggered
FMockArchive MockArchive;
MockArchive.OnFObjectPtr = [&](FObjectPtr& Value)
{
Value = PlaceHolderClass;
};
FBinaryArchiveFormatter Formatter(MockArchive);
FStructuredArchive Ar(Formatter);
FStructuredArchiveSlot Slot = Ar.Open();
Obj->ObjectPtr = Other;
FObjectPtrProperty::StaticSerializeItem(Property, Slot, &Obj->ObjectPtr, nullptr);
CHECK(ResolveCount == 0);
CHECK(*reinterpret_cast<ULinkerPlaceholderClass**>(&Obj->ObjectPtr) == PlaceHolderClass);
}
}
TEST_CASE("UE::CoreUObject::FObjectPtrProperty::StaticSerializeItem")
{
TestSerializeItem<UObjectPtrTestClassWithRef>(TEXT("Object1"));
}
TEST_CASE("UE::CoreUObject::FObjectProperty::StaticSerializeItem")
{
TestSerializeItem<UObjectWithRawProperty>(TEXT("Object2"));
}
class MockAssetRegistryInterface : public IAssetRegistryInterface
{
public:
MockAssetRegistryInterface()
: Old(IAssetRegistryInterface::Default)
{
IAssetRegistryInterface::Default = this;
}
virtual ~MockAssetRegistryInterface()
{
IAssetRegistryInterface::Default = Old;
}
virtual void GetDependencies(FName InPackageName, TArray<FName>& OutDependencies, UE::AssetRegistry::EDependencyCategory Category = UE::AssetRegistry::EDependencyCategory::Package, const UE::AssetRegistry::FDependencyQuery& Flags = UE::AssetRegistry::FDependencyQuery()) override
{
}
virtual UE::AssetRegistry::EExists TryGetAssetByObjectPath(const FSoftObjectPath& ObjectPath, FAssetData& OutAssetData) const override
{
const FAssetData* Found = AssetData.Find(ObjectPath);
if (Found)
{
OutAssetData = *Found;
return UE::AssetRegistry::EExists::Exists;
}
return UE::AssetRegistry::EExists::DoesNotExist;
}
virtual UE::AssetRegistry::EExists TryGetAssetPackageData(FName PackageName, FAssetPackageData& OutPackageData) const override
{
return UE::AssetRegistry::EExists::Exists;
}
IAssetRegistryInterface* Old;
TMap<FSoftObjectPath, FAssetData> AssetData;
};
#if UE_WITH_OBJECT_HANDLE_LATE_RESOLVE
TEST_CASE("UE::CoreUObject::FObjectProperty::ParseObjectPropertyValue")
{
UClass* Class = UObjectPtrTestClassWithRef::StaticClass();
FObjectProperty* Property = CastField<FObjectProperty>(Class->FindPropertyByName(TEXT("ObjectPtr")));
//make a fake entry for the asset registry
//this allows lazy load to return an unresolved handle
MockAssetRegistryInterface MockAssetRegistry;
const TCHAR* Text = TEXT("/TestPackageName.Other");
FSoftObjectPath Path(Text);
FAssetData AssetData;
AssetData.PackageName = "/TestPackageName";
AssetData.PackagePath = "/";
AssetData.AssetName = "Other";
AssetData.AssetClassPath.TrySetPath(UObjectPtrTestClass::StaticClass());
MockAssetRegistry.AssetData.Add(Path, AssetData);
UPackage* TestPackage = NewObject<UPackage>(nullptr, TEXT("TestPackageName"), RF_Transient);
TestPackage->AddToRoot();
ON_SCOPE_EXIT
{
TestPackage->RemoveFromRoot();
};
UObjectPtrTestClassWithRef* Obj = NewObject<UObjectPtrTestClassWithRef>(TestPackage, "ObjectName");
TObjectPtr<UObject> Result;
FObjectPropertyBase::ParseObjectPropertyValue(Property, Obj, UObjectPtrTestClass::StaticClass(), 0, Text, Result);
CHECK(Result != nullptr);
CHECK(!Result.IsResolved());
{
const TCHAR * Buffer = TEXT("ObjectPtr=/TestPackageName.Other");
TArray<FDefinedProperty> DefinedProperties;
Buffer = FProperty::ImportSingleProperty(Buffer, Obj, Class, Obj, PPF_Delimited, nullptr, DefinedProperties);
CHECK(Obj->ObjectPtr != nullptr);
CHECK(!Obj->ObjectPtr.IsResolved());
}
{
const TCHAR* Buffer = TEXT("ObjectPtr=/TestPackageName.Other");
FProperty* ObjProperty = Class->FindPropertyByName(TEXT("ObjectPtr"));
ObjProperty->ImportText_InContainer(Buffer, Obj, Obj, 0);
CHECK(Obj->ObjectPtr == nullptr); //TODO this should not resolve and not be null
CHECK(Obj->ObjectPtr.IsResolved());
}
{
const TCHAR * Buffer = TEXT("ArrayObjPtr=(/TestPackageName.Other)");
TArray<FDefinedProperty> DefinedProperties;
Buffer = FProperty::ImportSingleProperty(Buffer, Obj, Class, Obj, PPF_Delimited, nullptr, DefinedProperties);
CHECK(Obj->ArrayObjPtr.Num() == 1);
CHECK(!Obj->ArrayObjPtr[0].IsResolved());
}
{
const TCHAR* Buffer = TEXT("ArrayObjPtr=(/TestPackageName.Other)");
FProperty* ArrayProperty = Class->FindPropertyByName(TEXT("ArrayObjPtr"));
ArrayProperty->ImportText_InContainer(Buffer, Obj, Obj, 0);
CHECK(Obj->ArrayObjPtr.Num() == 1);
CHECK(!Obj->ArrayObjPtr[0].IsResolved());
}
{
const TCHAR * Buffer = TEXT("ObjectPtr=/TestPackageName.Other");
TArray<FDefinedProperty> DefinedProperties;
Buffer = FProperty::ImportSingleProperty(Buffer, Obj, Class, Obj, PPF_Delimited, nullptr, DefinedProperties);
CHECK(Obj->ObjectPtr != nullptr);
CHECK(!Obj->ObjectPtr.IsResolved());
}
{
const TCHAR* Buffer = TEXT("ObjectPtr=/TestPackageName.Other");
FProperty* ObjProperty = Class->FindPropertyByName(TEXT("ObjectPtr"));
ObjProperty->ImportText_InContainer(Buffer, Obj, Obj, 0);
CHECK(Obj->ObjectPtr == nullptr); //TODO this should not resolve and not be null
CHECK(Obj->ObjectPtr.IsResolved());
}
{
const TCHAR * Buffer = TEXT("ArrayObjPtr=(/TestPackageName.Other)");
TArray<FDefinedProperty> DefinedProperties;
Buffer = FProperty::ImportSingleProperty(Buffer, Obj, Class, Obj, PPF_Delimited, nullptr, DefinedProperties);
CHECK(Obj->ArrayObjPtr.Num() == 1);
CHECK(!Obj->ArrayObjPtr[0].IsResolved());
}
{
const TCHAR* Buffer = TEXT("ArrayObjPtr=(/TestPackageName.Other)");
FProperty* ArrayProperty = Class->FindPropertyByName(TEXT("ArrayObjPtr"));
ArrayProperty->ImportText_InContainer(Buffer, Obj, Obj, 0);
CHECK(Obj->ArrayObjPtr.Num() == 1);
CHECK(!Obj->ArrayObjPtr[0].IsResolved());
}
}
#endif
TEST_CASE("UE::FObjectPtrProperty::Identical")
{
int ResolveCount = 0;
#if UE_WITH_OBJECT_HANDLE_TRACKING
auto CallbackHandle = UE::CoreUObject::AddObjectHandleReferenceResolvedCallback([&ResolveCount](const FObjectRef&, UPackage*, UObject*)
{
++ResolveCount;
});
ON_SCOPE_EXIT
{
UE::CoreUObject::RemoveObjectHandleReferenceResolvedCallback(CallbackHandle);
};
#endif
UClass* Class = UObjectPtrTestClassWithRef::StaticClass();
FObjectProperty* Property = CastField<FObjectProperty>(Class->FindPropertyByName(TEXT("ObjectPtr")));
REQUIRE(Property != nullptr);
UPackage* TestPackage = NewObject<UPackage>(nullptr, TEXT("Test/TestPackageName"), RF_Transient);
TestPackage->AddToRoot();
UPackage* TestPackage2 = NewObject<UPackage>(nullptr, TEXT("Test/TestPackageName2"), RF_Transient);
TestPackage2->AddToRoot();
ON_SCOPE_EXIT
{
TestPackage->RemoveFromRoot();
TestPackage2->RemoveFromRoot();
};
TObjectPtr<UObject> ObjWithRef = NewObject<UObjectPtrTestClassWithRef>(TestPackage, TEXT("UObjectWithClassProperty"));
TObjectPtr<UObject> Obj1 = NewObject<UObjectPtrTestClass>(TestPackage, TEXT("UObjectPtrTestClass"));
TObjectPtr<UObject> Obj2 = NewObject<UObjectPtrTestClass>(TestPackage2, TEXT("UObjectPtrTestClass"));
#if UE_WITH_OBJECT_HANDLE_TRACKING
FObjectHandle Handle1 = MakeUnresolvedHandle(ObjWithRef.Get());
FObjectHandle Handle2 = MakeUnresolvedHandle(Obj1.Get());
FObjectHandle Handle3 = MakeUnresolvedHandle(Obj2.Get());
TObjectPtr<UObjectPtrTestClass> ObjectPtr = *reinterpret_cast<TObjectPtr<UObjectPtrTestClass>*>(&Handle1);
ObjWithRef = *reinterpret_cast<TObjectPtr<UObject>*>(&Handle1);
Obj1 = *reinterpret_cast<TObjectPtr<UObject>*>(&Handle2);
Obj2 = *reinterpret_cast<TObjectPtr<UObject>*>(&Handle3);
#endif
CHECK(Property->Identical(&Obj1, &Obj1, 0u));
CHECK(!Property->Identical(&Obj1, nullptr, 0u));
CHECK(!Property->Identical(nullptr, &Obj1, 0u));
CHECK(!Property->Identical(&ObjWithRef, &Obj2, 0u));
CHECK(!Property->Identical(&ObjWithRef, &Obj2, 0u));
CHECK(!Property->Identical(&Obj1, &ObjWithRef, PPF_DeepComparison));
CHECK(ResolveCount == 0);
CHECK(Property->Identical(&Obj1, &Obj2, PPF_DeepComparison));
#if UE_WITH_OBJECT_HANDLE_TRACKING
CHECK(ResolveCount == 2);
#endif
}
#endif