// Copyright Epic Games, Inc. All Rights Reserved. #include "Misc/AutomationTest.h" #include "Misc/SecureHash.h" #include "Math/RandomStream.h" #include "Tests/TestHelpers.h" #include "Tests/Mock/VerifierStat.mock.h" #include "Tests/Mock/Manifest.mock.h" #include "Tests/Fake/FileSystem.fake.h" #include "Tests/Fake/InstallerError.fake.h" #include "Installer/Verifier.h" #include "BuildPatchVerify.h" #include "BuildPatchHash.h" #include "IBuildManifestSet.h" #include "BuildPatchSettings.h" #if WITH_DEV_AUTOMATION_TESTS BEGIN_DEFINE_SPEC(FVerifierSpec, "BuildPatchServices.Unit", EAutomationTestFlags::ProductFilter | EAutomationTestFlags_ApplicationContextMask) // Unit. TUniquePtr Verifier; // Mock. TUniquePtr FakeFileSystem; TUniquePtr FakeInstallerError; TUniquePtr MockVerificationStat; BuildPatchServices::FMockManifestPtr MockManifest; // Data. FString VerifyDirectory; FString StagedFileDirectory; TSet AllFiles; TSet SomeFiles; TSet TouchedFiles; TSet Tags; TArray OutDatedFiles; TMap DiskFileToManifestFile; bool bHasPaused; TUniquePtr ManifestSet; // Test helpers. TFuture PauseFor(float Seconds); void MakeFileData(); void TouchAllFiles(); void TouchSomeFiles(); void CorruptSomeFiles(); void ResizeSomeFiles(); void StageSomeFiles(); void MakeUnit(BuildPatchServices::EVerifyMode Mode); TSet LoadedFiles(); TSet FilesSizeCheckedFiles(); END_DEFINE_SPEC(FVerifierSpec) void FVerifierSpec::Define() { using namespace BuildPatchServices; // Data setup. FRollingHashConst::Init(); VerifyDirectory = TEXT("VerDir"); StagedFileDirectory = TEXT("StaFilDir"); bHasPaused = false; // Specs. BeforeEach([this]() { FakeFileSystem.Reset(new FFakeFileSystem()); FakeInstallerError.Reset(new FFakeInstallerError()); MockVerificationStat.Reset(new FMockVerifierStat()); MockManifest = MakeShareable(new FMockManifest()); ManifestSet.Reset(FBuildManifestSetFactory::Create({ BuildPatchServices::FInstallerAction::MakeInstall(MockManifest.ToSharedRef(), Tags) })); MakeFileData(); }); xDescribe("Verifier", [this]() { Describe("Verify", [this]() { BeforeEach([this]() { TouchSomeFiles(); }); Describe("when SHA verifying all files", [this]() { BeforeEach([this]() { MakeUnit(EVerifyMode::ShaVerifyAllFiles); }); It("should load and SHA check all files.", [this]() { Verifier->Verify(OutDatedFiles); TEST_EQUAL(OutDatedFiles.Num(), 0); TEST_TRUE(SetsAreEqual(LoadedFiles(), AllFiles)); }); }); Describe("when SHA verifying only touched files", [this]() { BeforeEach([this]() { MakeUnit(EVerifyMode::ShaVerifyTouchedFiles); }); It("should load and SHA check touched files.", [this]() { Verifier->Verify(OutDatedFiles); TEST_EQUAL(OutDatedFiles.Num(), 0); TEST_TRUE(SetsAreEqual(LoadedFiles(), TouchedFiles)); }); }); Describe("when file size verifying all files", [this]() { BeforeEach([this]() { MakeUnit(EVerifyMode::FileSizeCheckAllFiles); }); It("should check file size of all files.", [this]() { Verifier->Verify(OutDatedFiles); TEST_EQUAL(OutDatedFiles.Num(), 0); TEST_EQUAL(LoadedFiles().Num(), 0); TEST_TRUE(SetsAreEqual(FilesSizeCheckedFiles(), AllFiles)); }); }); Describe("when file size verifying touched files", [this]() { BeforeEach([this]() { MakeUnit(EVerifyMode::FileSizeCheckTouchedFiles); }); It("should check file size of touched files.", [this]() { Verifier->Verify(OutDatedFiles); TEST_EQUAL(OutDatedFiles.Num(), 0); TEST_EQUAL(LoadedFiles().Num(), 0); TEST_TRUE(SetsAreEqual(FilesSizeCheckedFiles(), TouchedFiles)); }); }); Describe("when verifying with SHA and some files are corrupt", [this]() { BeforeEach([this]() { TouchAllFiles(); CorruptSomeFiles(); MakeUnit(EVerifyMode::ShaVerifyAllFiles); }); It("should provide some files.", [this]() { Verifier->Verify(OutDatedFiles); TEST_TRUE(SetsAreEqual(TSet(OutDatedFiles), SomeFiles)); }); }); Describe("when verifying by size and some files are corrupt", [this]() { BeforeEach([this]() { TouchAllFiles(); ResizeSomeFiles(); MakeUnit(EVerifyMode::FileSizeCheckAllFiles); }); It("should provide some files.", [this]() { Verifier->Verify(OutDatedFiles); TEST_TRUE(SetsAreEqual(TSet(OutDatedFiles), SomeFiles)); }); }); Describe("when some files were only staged", [this]() { BeforeEach([this]() { TouchAllFiles(); StageSomeFiles(); MakeUnit(EVerifyMode::ShaVerifyAllFiles); }); It("load staged files instead of installed files.", [this]() { Verifier->Verify(OutDatedFiles); TEST_TRUE(SetsAreEqual(LoadedFiles(), AllFiles)); }); }); Describe("when there are multiple types of file errors", [this]() { Describe("when we are using SHA verify mode", [this]() { Describe("when the first error is a corruption", [this]() { BeforeEach([this]() { CorruptSomeFiles(); const FString LastFile = VerifyDirectory / SomeFiles.Array().Last(); FakeFileSystem->DiskData.Remove(LastFile); MakeUnit(EVerifyMode::ShaVerifyAllFiles); }); It("will return HashCheckFailed.", [this]() { EVerifyResult VerifyResult = Verifier->Verify(OutDatedFiles); TEST_EQUAL(VerifyResult, EVerifyResult::HashCheckFailed); }); }); Describe("the first error is missing file", [this]() { BeforeEach([this]() { CorruptSomeFiles(); const FString FirstFile = VerifyDirectory / *SomeFiles.CreateIterator(); FakeFileSystem->DiskData.Remove(FirstFile); MakeUnit(EVerifyMode::ShaVerifyAllFiles); }); It("will return FileMissing.", [this]() { EVerifyResult VerifyResult = Verifier->Verify(OutDatedFiles); TEST_EQUAL(VerifyResult, EVerifyResult::FileMissing); }); }); Describe("the first error is a file failed to open", [this]() { BeforeEach([this]() { CorruptSomeFiles(); const FString FirstFile = VerifyDirectory / *SomeFiles.CreateIterator(); FakeFileSystem->DiskDataOpenFailure.Add(FirstFile); MakeUnit(EVerifyMode::ShaVerifyAllFiles); }); It("will return OpenFileFailed.", [this]() { EVerifyResult VerifyResult = Verifier->Verify(OutDatedFiles); TEST_EQUAL(VerifyResult, EVerifyResult::OpenFileFailed); }); }); Describe("the first error is file was the wrong size", [this]() { BeforeEach([this]() { ResizeSomeFiles(); const FString LastFile = VerifyDirectory / SomeFiles.Array().Last(); FakeFileSystem->DiskData.Remove(LastFile); MakeUnit(EVerifyMode::ShaVerifyAllFiles); }); It("will return FileSizeFailed.", [this]() { EVerifyResult VerifyResult = Verifier->Verify(OutDatedFiles); TEST_EQUAL(VerifyResult, EVerifyResult::FileSizeFailed); }); }); }); Describe("when we are using file size verify mode", [this]() { Describe("the first error is missing file", [this]() { BeforeEach([this]() { ResizeSomeFiles(); const FString FirstFile = VerifyDirectory / *SomeFiles.CreateIterator(); FakeFileSystem->DiskData.Remove(FirstFile); MakeUnit(EVerifyMode::FileSizeCheckAllFiles); }); It("will return FileMissing.", [this]() { EVerifyResult VerifyResult = Verifier->Verify(OutDatedFiles); TEST_EQUAL(VerifyResult, EVerifyResult::FileMissing); }); }); Describe("the first error is file was the wrong size", [this]() { BeforeEach([this]() { ResizeSomeFiles(); const FString LastFile = VerifyDirectory / SomeFiles.Array().Last(); FakeFileSystem->DiskData.Remove(LastFile); MakeUnit(EVerifyMode::FileSizeCheckAllFiles); }); It("will return FileSizeFailed.", [this]() { EVerifyResult VerifyResult = Verifier->Verify(OutDatedFiles); TEST_EQUAL(VerifyResult, EVerifyResult::FileSizeFailed); }); }); }); }); }); Describe("SetPaused", [this]() { Describe("when verifying with SHA", [this]() { BeforeEach([this]() { TouchAllFiles(); MakeUnit(EVerifyMode::ShaVerifyAllFiles); }); It("should delay the verification process.", [this]() { const float PauseTime = 0.15f; MockVerificationStat->OnFileCompletedFunc = [this, PauseTime](const FString&, EVerifyResult) { if (!bHasPaused) { bHasPaused = true; PauseFor(PauseTime); } }; Verifier->Verify(OutDatedFiles); double LongestDelay = 0.0f; for (int32 Idx = 1; Idx < MockVerificationStat->RxOnFileStarted.Num(); ++Idx) { double ThisDelay = MockVerificationStat->RxOnFileStarted[Idx].Get<0>() - MockVerificationStat->RxOnFileStarted[Idx - 1].Get<0>(); if (ThisDelay > LongestDelay) { LongestDelay = ThisDelay; } } TEST_TRUE(LongestDelay >= PauseTime); }); }); Describe("when verifying with size", [this]() { BeforeEach([this]() { TouchAllFiles(); MakeUnit(EVerifyMode::FileSizeCheckAllFiles); }); It("should delay the verification process.", [this]() { const float PauseTime = 0.15f; MockVerificationStat->OnFileCompletedFunc = [this, PauseTime](const FString&, EVerifyResult) { if (!bHasPaused) { bHasPaused = true; PauseFor(PauseTime); } }; Verifier->Verify(OutDatedFiles); double LongestDelay = 0.0f; for (int32 Idx = 1; Idx < MockVerificationStat->RxOnFileStarted.Num(); ++Idx) { double ThisDelay = MockVerificationStat->RxOnFileStarted[Idx].Get<0>() - MockVerificationStat->RxOnFileStarted[Idx - 1].Get<0>(); if (ThisDelay > LongestDelay) { LongestDelay = ThisDelay; } } TEST_TRUE(LongestDelay >= PauseTime); }); }); }); Describe("Abort", [this]() { Describe("when verifying with SHA", [this]() { BeforeEach([this]() { TouchAllFiles(); MakeUnit(EVerifyMode::ShaVerifyAllFiles); }); It("should halt process and stop.", [this]() { MockVerificationStat->OnFileCompletedFunc = [this](const FString&, EVerifyResult) { Verifier->Abort(); }; Verifier->Verify(OutDatedFiles); TEST_TRUE(LoadedFiles().Num() < MockManifest->BuildFileList.Num()); }); It("should return aborted.", [this]() { MockVerificationStat->OnFileCompletedFunc = [this](const FString&, EVerifyResult) { Verifier->Abort(); }; EVerifyResult VerifyResult = Verifier->Verify(OutDatedFiles); TEST_EQUAL(VerifyResult, EVerifyResult::Aborted); }); }); Describe("when verifying by size", [this]() { BeforeEach([this]() { TouchAllFiles(); MakeUnit(EVerifyMode::FileSizeCheckAllFiles); }); It("should halt process and stop.", [this]() { MockVerificationStat->OnFileCompletedFunc = [this](const FString&, EVerifyResult) { Verifier->Abort(); }; Verifier->Verify(OutDatedFiles); TEST_TRUE(LoadedFiles().Num() < MockManifest->BuildFileList.Num()); }); It("should return aborted.", [this]() { MockVerificationStat->OnFileCompletedFunc = [this](const FString&, EVerifyResult) { Verifier->Abort(); }; EVerifyResult VerifyResult = Verifier->Verify(OutDatedFiles); TEST_EQUAL(VerifyResult, EVerifyResult::Aborted); }); }); }); }); AfterEach([this]() { Verifier.Reset(); ManifestSet.Reset(); MockManifest.Reset(); MockVerificationStat.Reset(); FakeFileSystem.Reset(); AllFiles.Reset(); TouchedFiles.Reset(); Tags.Reset(); OutDatedFiles.Reset(); DiskFileToManifestFile.Reset(); bHasPaused = false; }); } TFuture FVerifierSpec::PauseFor(float Seconds) { using namespace BuildPatchServices; double PauseAt = FStatsCollector::GetSeconds(); Verifier->SetPaused(true); TFunction Task = [this, PauseAt, Seconds]() { while ((FStatsCollector::GetSeconds() - PauseAt) < Seconds) { FPlatformProcess::Sleep(0.01f); } Verifier->SetPaused(false); }; return Async(EAsyncExecution::Thread, MoveTemp(Task)); } void FVerifierSpec::MakeFileData() { using namespace BuildPatchServices; for (int32 Idx = 0; Idx < 25; ++Idx) { MockManifest->BuildFileList.Add(FString::Printf(TEXT("Some/Install/File%d.exe"), Idx)); DiskFileToManifestFile.Add(VerifyDirectory / MockManifest->BuildFileList.Last(), MockManifest->BuildFileList.Last()); DiskFileToManifestFile.Add(StagedFileDirectory / MockManifest->BuildFileList.Last(), MockManifest->BuildFileList.Last()); SomeFiles.Add(MockManifest->BuildFileList.Last()); MockManifest->BuildFileList.Add(FString::Printf(TEXT("Other/Install/File%d.exe"), Idx)); DiskFileToManifestFile.Add(VerifyDirectory / MockManifest->BuildFileList.Last(), MockManifest->BuildFileList.Last()); DiskFileToManifestFile.Add(StagedFileDirectory / MockManifest->BuildFileList.Last(), MockManifest->BuildFileList.Last()); } AllFiles = TSet(MockManifest->BuildFileList); MockManifest->TaggedFileList = AllFiles; FRandomStream RandomData(0); FSHAHash SHAHashData; for (const FString& Filename : MockManifest->BuildFileList) { FString FullFilename = VerifyDirectory / Filename; TArray& FileData = FakeFileSystem->DiskData.Add(FullFilename); FileData.AddUninitialized(100); uint8* Data = FileData.GetData(); for (int32 DataIdx = 0; DataIdx <= (FileData.Num() - 4); DataIdx += 4) { *((uint32*)(Data + DataIdx)) = RandomData.GetUnsignedInt(); } FSHA1::HashBuffer(Data, FileData.Num(), SHAHashData.Hash); MockManifest->FileNameToHashes.Add(Filename, SHAHashData); MockManifest->FileNameToFileSize.Add(Filename, FileData.Num()); } } void FVerifierSpec::TouchAllFiles() { TouchedFiles = TSet(MockManifest->BuildFileList); } void FVerifierSpec::TouchSomeFiles() { TouchedFiles.Reset(); for (const FString& Filename : SomeFiles) { TouchedFiles.Add(Filename); } } void FVerifierSpec::CorruptSomeFiles() { for (const FString& Filename : SomeFiles) { FString FullFilename = VerifyDirectory / Filename; TArray& FileData = FakeFileSystem->DiskData[FullFilename]; for (int32 DataIdx = 0; DataIdx < 10; ++DataIdx) { FileData[DataIdx] = FileData[DataIdx + 1]; } } } void FVerifierSpec::ResizeSomeFiles() { for (const FString& Filename : SomeFiles) { FString FullFilename = VerifyDirectory / Filename; TArray& FileData = FakeFileSystem->DiskData[FullFilename]; FileData.Add(123); FSHA1::HashBuffer(FileData.GetData(), FileData.Num(), MockManifest->FileNameToHashes[Filename].Hash); } } void FVerifierSpec::StageSomeFiles() { for (const FString& Filename : SomeFiles) { FString OldFilename = VerifyDirectory / Filename; FString NewFilename = StagedFileDirectory / Filename; FakeFileSystem->DiskData.Add(NewFilename, FakeFileSystem->DiskData[OldFilename]); FakeFileSystem->DiskData.Remove(OldFilename); DiskFileToManifestFile[OldFilename] = TEXT("Break"); } } void FVerifierSpec::MakeUnit(BuildPatchServices::EVerifyMode Mode) { using namespace BuildPatchServices; // TODO: Verifier behavioural change needs unit test update. Verifier.Reset(FVerifierFactory::Create( FakeFileSystem.Get(), MockVerificationStat.Get(), Mode, nullptr, /*TouchedFiles,*/ ManifestSet.Get(), VerifyDirectory, StagedFileDirectory)); if (EVerifyMode::FileSizeCheckTouchedFiles == Mode || EVerifyMode::ShaVerifyTouchedFiles == Mode) { Verifier->AddTouchedFiles(TouchedFiles); } } TSet FVerifierSpec::LoadedFiles() { using namespace BuildPatchServices; TSet Result; for (const FMockFileSystem::FCreateFileReader& Call : FakeFileSystem->RxCreateFileReader) { Result.Add(DiskFileToManifestFile[Call.Get<2>()]); } return Result; } TSet FVerifierSpec::FilesSizeCheckedFiles() { using namespace BuildPatchServices; TSet Result; for (const FMockFileSystem::FGetFileSize& Call : FakeFileSystem->RxGetFileSize) { Result.Add(DiskFileToManifestFile[Call.Get<1>()]); } return Result; } #endif //WITH_DEV_AUTOMATION_TESTS