// Copyright Epic Games, Inc. All Rights Reserved. #include "Misc/AutomationTest.h" #include "BuildPatchSettings.h" #include "Tests/TestHelpers.h" #include "Tests/Mock/Manifest.mock.h" #include "Installer/ChunkReferenceTracker.h" #if WITH_DEV_AUTOMATION_TESTS BEGIN_DEFINE_SPEC(FChunkReferenceTrackerSpec, "BuildPatchServices.Unit", EAutomationTestFlags::ProductFilter | EAutomationTestFlags_ApplicationContextMask) const uint32 TestChunkSize = 128 * 1024; // Unit TUniquePtr ChunkReferenceTracker; // Mock BuildPatchServices::FMockManifestPtr MockManifest; TUniquePtr ManifestSet; // Data TArray FileList; TArray SubsetFileList; TSet AllChunks; TSet SubsetReferencedChunks; TMap FileManifests; TMap ChunkRefCounts; TArray UseOrderForwardSortedSubsetArray; TArray UseOrderReverseSortedSubsetArray; TArray UseOrderForwardSortedSubsetUniqueArray; END_DEFINE_SPEC(FChunkReferenceTrackerSpec) void FChunkReferenceTrackerSpec::Define() { using namespace BuildPatchServices; // Data setup. for (int32 Idx = 0; Idx < 50; ++Idx) { FileList.Add(FString::Printf(TEXT("Some/Install/File%d.exe"), Idx)); FileList.Add(FString::Printf(TEXT("Other/Install/File%d.exe"), Idx)); } FileList.Sort(TLess()); SubsetFileList = FileList.FilterByPredicate([](const FString& Element) { return Element.StartsWith(TEXT("Some")); }); for (const FString& Filename : FileList) { FFileManifest& FileManifest = FileManifests.Add(Filename, FFileManifest()); FileManifest.Filename = Filename; FChunkPart ChunkPartData; // New chunk. ChunkPartData.Guid = FGuid::NewGuid(); AllChunks.Add(ChunkPartData.Guid); ChunkPartData.Offset = 0; ChunkPartData.Size = TestChunkSize; FileManifest.ChunkParts.Add(ChunkPartData); // New chunk. ChunkPartData.Guid = FGuid::NewGuid(); AllChunks.Add(ChunkPartData.Guid); ChunkPartData.Offset += ChunkPartData.Size; ChunkPartData.Size = TestChunkSize; FileManifest.ChunkParts.Add(ChunkPartData); // Dupe chunk. ChunkPartData.Offset += ChunkPartData.Size; ChunkPartData.Size = TestChunkSize; FileManifest.ChunkParts.Add(ChunkPartData); } for (const FString& File : SubsetFileList) { for (const FChunkPart& FileChunkPart : FileManifests[File].ChunkParts) { SubsetReferencedChunks.Add(FileChunkPart.Guid); UseOrderForwardSortedSubsetArray.Add(FileChunkPart.Guid); UseOrderForwardSortedSubsetUniqueArray.AddUnique(FileChunkPart.Guid); UseOrderReverseSortedSubsetArray.Insert(FileChunkPart.Guid, 0); } } for (const FString& File : FileList) { for (const FChunkPart& FileChunkPart : FileManifests[File].ChunkParts) { ++ChunkRefCounts.FindOrAdd(FileChunkPart.Guid); } } // Specs. Describe("ChunkReferenceTracker", [this]() { Describe("GetReferencedChunks", [this]() { Describe("when constructing all files in the manifest", [this]() { BeforeEach([this]() { MockManifest = MakeShareable(new FMockManifest()); MockManifest->FileManifests = FileManifests; MockManifest->BuildFileList = FileList; MockManifest->TaggedFileList = TSet(FileList); MockManifest->SyncInternalManifestStructures(); TSet FilesToConstruct(FileList); FilesToConstruct.Sort(TLess()); ManifestSet.Reset(FBuildManifestSetFactory::Create({ BuildPatchServices::FInstallerAction::MakeInstall(MockManifest.ToSharedRef()) })); ChunkReferenceTracker.Reset(FChunkReferenceTrackerFactory::Create(ManifestSet.Get(), MoveTemp(FilesToConstruct))); }); It("should return all chunks before any are popped.", [this]() { TSet ReferencedChunks = ChunkReferenceTracker->GetReferencedChunks(); TEST_EQUAL(AllChunks.Difference(ReferencedChunks).Num(), 0); TEST_EQUAL(AllChunks.Num(), ReferencedChunks.Num()); }); It("should return all chunks that are still referenced.", [this]() { FGuid FirstChunk = FileManifests[FileList[0]].ChunkParts[0].Guid; TEST_EQUAL(ChunkRefCounts[FirstChunk], 1); ChunkReferenceTracker->PopReference(FirstChunk); TSet ReferencedChunks = ChunkReferenceTracker->GetReferencedChunks(); TSet UnreferencedChunks = AllChunks.Difference(ReferencedChunks); TEST_EQUAL(UnreferencedChunks.Num(), 1); TEST_TRUE(UnreferencedChunks.Contains(FirstChunk)); }); It("should return no chunks if all references are popped.", [this]() { for (const FString& File : FileList) { for (const FChunkPart& FileChunkPart : FileManifests[File].ChunkParts) { ChunkReferenceTracker->PopReference(FileChunkPart.Guid); } } TSet ReferencedChunks = ChunkReferenceTracker->GetReferencedChunks(); TEST_EQUAL(ReferencedChunks.Num(), 0); }); }); Describe("when constructing a subset of files in the manifest", [this]() { BeforeEach([this]() { MockManifest = MakeShareable(new FMockManifest()); MockManifest->FileManifests = FileManifests; MockManifest->BuildFileList = FileList; MockManifest->TaggedFileList = TSet(FileList); MockManifest->SyncInternalManifestStructures(); TSet FilesToConstruct(SubsetFileList); FilesToConstruct.Sort(TLess()); ManifestSet.Reset(FBuildManifestSetFactory::Create({ BuildPatchServices::FInstallerAction::MakeInstall(MockManifest.ToSharedRef()) })); ChunkReferenceTracker.Reset(FChunkReferenceTrackerFactory::Create(ManifestSet.Get(), MoveTemp(FilesToConstruct))); }); It("should return only chunks referenced by subset of files.", [this]() { TSet ReferencedChunks = ChunkReferenceTracker->GetReferencedChunks(); TEST_EQUAL(SubsetReferencedChunks.Difference(ReferencedChunks).Num(), 0); TEST_EQUAL(SubsetReferencedChunks.Num(), ReferencedChunks.Num()); }); It("should return all chunks that are still referenced.", [this]() { FGuid FirstChunk = FileManifests[SubsetFileList[0]].ChunkParts[0].Guid; TEST_EQUAL(ChunkRefCounts[FirstChunk], 1); ChunkReferenceTracker->PopReference(FirstChunk); TSet ReferencedChunks = ChunkReferenceTracker->GetReferencedChunks(); TSet UnreferencedChunks = SubsetReferencedChunks.Difference(ReferencedChunks); TEST_EQUAL(UnreferencedChunks.Num(), 1); TEST_TRUE(UnreferencedChunks.Contains(FirstChunk)); }); It("should return no chunks if all references are popped.", [this]() { for (const FString& File : SubsetFileList) { for (const FChunkPart& FileChunkPart : FileManifests[File].ChunkParts) { ChunkReferenceTracker->PopReference(FileChunkPart.Guid); } } TSet ReferencedChunks = ChunkReferenceTracker->GetReferencedChunks(); TEST_EQUAL(ReferencedChunks.Num(), 0); }); }); }); Describe("GetReferenceCount", [this]() { BeforeEach([this]() { MockManifest = MakeShareable(new FMockManifest()); MockManifest->FileManifests = FileManifests; MockManifest->BuildFileList = FileList; MockManifest->TaggedFileList = TSet(FileList); MockManifest->SyncInternalManifestStructures(); TSet FilesToConstruct(FileList); FilesToConstruct.Sort(TLess()); ManifestSet.Reset(FBuildManifestSetFactory::Create({ BuildPatchServices::FInstallerAction::MakeInstall(MockManifest.ToSharedRef()) })); ChunkReferenceTracker.Reset(FChunkReferenceTrackerFactory::Create(ManifestSet.Get(), MoveTemp(FilesToConstruct))); }); It("should return original counts before anything is popped.", [this]() { for (const FGuid& ChunkId : AllChunks) { TEST_EQUAL(ChunkReferenceTracker->GetReferenceCount(ChunkId), ChunkRefCounts[ChunkId]); } }); It("should return 0 for unknown chunks.", [this]() { TEST_EQUAL(ChunkReferenceTracker->GetReferenceCount(FGuid::NewGuid()), 0); }); It("should return adjusted count for popped references.", [this]() { FGuid FirstChunk = FileManifests[FileList[0]].ChunkParts[0].Guid; TEST_EQUAL(ChunkReferenceTracker->GetReferenceCount(FirstChunk), ChunkRefCounts[FirstChunk]); ChunkReferenceTracker->PopReference(FirstChunk); TEST_EQUAL(ChunkReferenceTracker->GetReferenceCount(FirstChunk), ChunkRefCounts[FirstChunk]-1); }); It("should return 0 for all chunks once all references popped.", [this]() { for (const FString& File : FileList) { for (const FChunkPart& FileChunkPart : FileManifests[File].ChunkParts) { ChunkReferenceTracker->PopReference(FileChunkPart.Guid); } } for (const FGuid& ChunkId : AllChunks) { TEST_EQUAL(ChunkReferenceTracker->GetReferenceCount(ChunkId), 0); } }); }); Describe("SortByUseOrder", [this]() { BeforeEach([this]() { MockManifest = MakeShareable(new FMockManifest()); MockManifest->FileManifests = FileManifests; MockManifest->BuildFileList = FileList; MockManifest->TaggedFileList = TSet(FileList); MockManifest->SyncInternalManifestStructures(); TSet FilesToConstruct(FileList); FilesToConstruct.Sort(TLess()); ManifestSet.Reset(FBuildManifestSetFactory::Create({ BuildPatchServices::FInstallerAction::MakeInstall(MockManifest.ToSharedRef()) })); ChunkReferenceTracker.Reset(FChunkReferenceTrackerFactory::Create(ManifestSet.Get(), MoveTemp(FilesToConstruct))); }); Describe("when used with ESortDirection::Ascending", [this]() { It("should place soonest required chunks first.", [this]() { TArray SortedArray = UseOrderForwardSortedSubsetArray; TArray ArrayToSort = UseOrderReverseSortedSubsetArray; ChunkReferenceTracker->SortByUseOrder(ArrayToSort, IChunkReferenceTracker::ESortDirection::Ascending); TEST_EQUAL(SortedArray, ArrayToSort); }); It("should place unused chunks last.", [this]() { TArray SortedArray = UseOrderForwardSortedSubsetArray; TArray ArrayToSort = UseOrderReverseSortedSubsetArray; SortedArray.Add(FGuid::NewGuid()); ArrayToSort.Insert(SortedArray.Last(), ArrayToSort.Num() / 2); ChunkReferenceTracker->SortByUseOrder(ArrayToSort, IChunkReferenceTracker::ESortDirection::Ascending); TEST_EQUAL(SortedArray, ArrayToSort); }); }); Describe("when used with ESortDirection::Descending", [this]() { It("should place soonest required chunks last.", [this]() { TArray SortedArray = UseOrderReverseSortedSubsetArray; TArray ArrayToSort = UseOrderForwardSortedSubsetArray; ChunkReferenceTracker->SortByUseOrder(ArrayToSort, IChunkReferenceTracker::ESortDirection::Descending); TEST_EQUAL(SortedArray, ArrayToSort); }); It("should place unused chunks first.", [this]() { TArray SortedArray = UseOrderReverseSortedSubsetArray; TArray ArrayToSort = UseOrderForwardSortedSubsetArray; SortedArray.Insert(FGuid::NewGuid(), 0); ArrayToSort.Insert(SortedArray[0], ArrayToSort.Num() / 2); ChunkReferenceTracker->SortByUseOrder(ArrayToSort, IChunkReferenceTracker::ESortDirection::Descending); TEST_EQUAL(SortedArray, ArrayToSort); }); }); Describe("when used with an invalid ESortDirection", [this]() { It("should leave the array unchanged.", [this]() { TArray ArrayToSort, ArrayToSortDupe; ArrayToSort = UseOrderForwardSortedSubsetArray; ArrayToSort.Insert(FGuid::NewGuid(), ArrayToSort.Num() / 2); ArrayToSortDupe = ArrayToSort; ChunkReferenceTracker->SortByUseOrder(ArrayToSort, static_cast(255)); TEST_EQUAL(ArrayToSortDupe, ArrayToSort); }); }); }); Describe("GetNextReferences", [this]() { BeforeEach([this]() { MockManifest = MakeShareable(new FMockManifest()); MockManifest->FileManifests = FileManifests; MockManifest->BuildFileList = FileList; MockManifest->TaggedFileList = TSet(FileList); MockManifest->SyncInternalManifestStructures(); TSet FilesToConstruct(FileList); FilesToConstruct.Sort(TLess()); ManifestSet.Reset(FBuildManifestSetFactory::Create({ BuildPatchServices::FInstallerAction::MakeInstall(MockManifest.ToSharedRef()) })); ChunkReferenceTracker.Reset(FChunkReferenceTrackerFactory::Create(ManifestSet.Get(), MoveTemp(FilesToConstruct))); }); It("should return the correct number of selected references.", [this]() { int32 NumChunks = SubsetReferencedChunks.Num() / 2; TArray SelectedChunks = ChunkReferenceTracker->GetNextReferences(NumChunks, [this](const FGuid& ChunkID) { return SubsetReferencedChunks.Contains(ChunkID); }); TEST_EQUAL(SelectedChunks.Num(), NumChunks); TEST_EQUAL(TSet(SelectedChunks).Difference(SubsetReferencedChunks).Num(), 0); }); It("should return the selected references in the correct order.", [this]() { TArray SortedArray = UseOrderForwardSortedSubsetUniqueArray; int32 NumChunks = SortedArray.Num() / 2; SortedArray.SetNum(NumChunks); TArray SelectedChunks = ChunkReferenceTracker->GetNextReferences(NumChunks, [this](const FGuid& ChunkID) { return SubsetReferencedChunks.Contains(ChunkID); }); TEST_EQUAL(SelectedChunks, SortedArray); }); It("should return up to the given count if there are less available.", [this]() { TArray SelectedChunks = ChunkReferenceTracker->GetNextReferences(MAX_int32, [this](const FGuid& ChunkID) { return SubsetReferencedChunks.Contains(ChunkID); }); TEST_EQUAL(SelectedChunks.Num(), SubsetReferencedChunks.Num()); TEST_EQUAL(TSet(SelectedChunks).Difference(SubsetReferencedChunks).Num(), 0); }); It("should return no ids if none were selected.", [this]() { TArray SelectedChunks = ChunkReferenceTracker->GetNextReferences(MAX_int32, [this](const FGuid& ChunkID) { return false; }); TEST_EQUAL(SelectedChunks.Num(), 0); }); It("should not return duplicates.", [this]() { TArray SelectedChunks = ChunkReferenceTracker->GetNextReferences(MAX_int32, [this](const FGuid& ChunkID) { return true; }); TSet SelectedChunksSet(SelectedChunks); TEST_EQUAL(SelectedChunks.Num(), SelectedChunksSet.Num()); }); }); Describe("PopReference", [this]() { BeforeEach([this]() { MockManifest = MakeShareable(new FMockManifest()); MockManifest->FileManifests = FileManifests; MockManifest->BuildFileList = FileList; MockManifest->TaggedFileList = TSet(FileList); MockManifest->SyncInternalManifestStructures(); TSet FilesToConstruct(FileList); FilesToConstruct.Sort(TLess()); ManifestSet.Reset(FBuildManifestSetFactory::Create({ BuildPatchServices::FInstallerAction::MakeInstall(MockManifest.ToSharedRef()) })); ChunkReferenceTracker.Reset(FChunkReferenceTrackerFactory::Create(ManifestSet.Get(), MoveTemp(FilesToConstruct))); }); It("should return true when popping the top chunk", [this]() { FGuid TopChunk = FileManifests[FileList[0]].ChunkParts[0].Guid; TEST_TRUE(ChunkReferenceTracker->PopReference(TopChunk)); }); It("should return false when popping unknown chunk", [this]() { TEST_FALSE(ChunkReferenceTracker->PopReference(FGuid::NewGuid())); }); It("should always return true for popping the chunks in order", [this]() { for (const FString& File : FileList) { for (const FChunkPart& FileChunkPart : FileManifests[File].ChunkParts) { TEST_TRUE(ChunkReferenceTracker->PopReference(FileChunkPart.Guid)); } } }); It("should return false for all except the top chunk", [this]() { FGuid FirstChunk = FileManifests[FileList[0]].ChunkParts[0].Guid; for (const FGuid& ChunkId : AllChunks) { if (ChunkId != FirstChunk) { TEST_FALSE(ChunkReferenceTracker->PopReference(ChunkId)); } } }); It("should return true for correct pop following many incorrect pops", [this]() { FGuid FirstChunk = FileManifests[FileList[0]].ChunkParts[0].Guid; for (const FGuid& ChunkId : AllChunks) { if (ChunkId != FirstChunk) { ChunkReferenceTracker->PopReference(ChunkId); } } TEST_TRUE(ChunkReferenceTracker->PopReference(FirstChunk)); }); }); }); AfterEach([this]() { ChunkReferenceTracker.Reset(); MockManifest.Reset(); ManifestSet.Reset(); }); } #endif //WITH_DEV_AUTOMATION_TESTS