|  | //===----------------------------------------------------------------------===// | 
|  | // | 
|  | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | 
|  | // See https://llvm.org/LICENSE.txt for license information. | 
|  | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | 
|  | // | 
|  | //===----------------------------------------------------------------------===// | 
|  |  | 
|  | #include "llvm/Support/VirtualOutputBackends.h" | 
|  | #include "llvm/ADT/StringMap.h" | 
|  | #include "llvm/Support/BLAKE3.h" | 
|  | #include "llvm/Support/HashingOutputBackend.h" | 
|  | #include "llvm/Support/MemoryBuffer.h" | 
|  | #include "llvm/Support/ThreadPool.h" | 
|  | #include "llvm/Support/raw_ostream.h" | 
|  | #include "llvm/Testing/Support/Error.h" | 
|  | #include "gtest/gtest.h" | 
|  |  | 
|  | using namespace llvm; | 
|  | using namespace llvm::vfs; | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | class OutputBackendProvider { | 
|  | public: | 
|  | virtual bool rejectsMissingDirectories() = 0; | 
|  |  | 
|  | virtual IntrusiveRefCntPtr<OutputBackend> createBackend() = 0; | 
|  | virtual std::string getFilePathToCreate() = 0; | 
|  | virtual std::string getFilePathToCreateUnder(StringRef Parent1, | 
|  | StringRef Parent2 = "") = 0; | 
|  | virtual Error checkCreated(StringRef FilePath, | 
|  | OutputConfig Config = OutputConfig()) = 0; | 
|  | virtual Error checkWrote(StringRef FilePath, StringRef Data) = 0; | 
|  | virtual Error checkFlushed(StringRef FilePath, StringRef Data) = 0; | 
|  | virtual Error checkKept(StringRef FilePath, StringRef Data) = 0; | 
|  | virtual Error checkDiscarded(StringRef FilePath) = 0; | 
|  |  | 
|  | virtual ~OutputBackendProvider() = default; | 
|  |  | 
|  | struct Generator { | 
|  | std::string Name; | 
|  | std::function<std::unique_ptr<OutputBackendProvider>()> Generate; | 
|  |  | 
|  | std::unique_ptr<OutputBackendProvider> operator()() const { | 
|  | return Generate(); | 
|  | } | 
|  | }; | 
|  | }; | 
|  |  | 
|  | struct BackendTest | 
|  | : public ::testing::TestWithParam<OutputBackendProvider::Generator> { | 
|  | std::unique_ptr<OutputBackendProvider> Provider; | 
|  |  | 
|  | void SetUp() override { Provider = GetParam()(); } | 
|  | void TearDown() override { Provider = nullptr; } | 
|  |  | 
|  | IntrusiveRefCntPtr<OutputBackend> createBackend() { | 
|  | return Provider->createBackend(); | 
|  | } | 
|  | }; | 
|  |  | 
|  | TEST_P(BackendTest, Discard) { | 
|  | auto Backend = createBackend(); | 
|  | std::string FilePath = Provider->getFilePathToCreate(); | 
|  | StringRef Data = "some data"; | 
|  | OutputFile O; | 
|  | EXPECT_THAT_ERROR(Backend->createFile(FilePath).moveInto(O), Succeeded()); | 
|  | consumeDiscardOnDestroy(O); | 
|  | ASSERT_THAT_ERROR(Provider->checkCreated(FilePath), Succeeded()); | 
|  |  | 
|  | O << Data; | 
|  | EXPECT_THAT_ERROR(O.discard(), Succeeded()); | 
|  | EXPECT_THAT_ERROR(Provider->checkDiscarded(FilePath), Succeeded()); | 
|  | EXPECT_FALSE(O.isOpen()); | 
|  | } | 
|  |  | 
|  | TEST_P(BackendTest, DiscardNoAtomicWrite) { | 
|  | auto Backend = createBackend(); | 
|  | std::string FilePath = Provider->getFilePathToCreate(); | 
|  | StringRef Data = "some data"; | 
|  | OutputConfig Config = OutputConfig().setNoAtomicWrite(); | 
|  |  | 
|  | OutputFile O; | 
|  | EXPECT_THAT_ERROR(Backend->createFile(FilePath, Config).moveInto(O), | 
|  | Succeeded()); | 
|  | consumeDiscardOnDestroy(O); | 
|  | ASSERT_THAT_ERROR(Provider->checkCreated(FilePath, Config), Succeeded()); | 
|  |  | 
|  | O << Data; | 
|  | EXPECT_THAT_ERROR(O.discard(), Succeeded()); | 
|  | EXPECT_THAT_ERROR(Provider->checkDiscarded(FilePath), Succeeded()); | 
|  | EXPECT_FALSE(O.isOpen()); | 
|  | } | 
|  |  | 
|  | TEST_P(BackendTest, Keep) { | 
|  | auto Backend = createBackend(); | 
|  | std::string FilePath = Provider->getFilePathToCreate(); | 
|  | StringRef Data = "some data"; | 
|  |  | 
|  | OutputFile O; | 
|  | EXPECT_THAT_ERROR(Backend->createFile(FilePath).moveInto(O), Succeeded()); | 
|  | consumeDiscardOnDestroy(O); | 
|  | ASSERT_THAT_ERROR(Provider->checkCreated(FilePath), Succeeded()); | 
|  | ASSERT_TRUE(O.isOpen()); | 
|  |  | 
|  | O << Data; | 
|  | EXPECT_THAT_ERROR(Provider->checkWrote(FilePath, Data), Succeeded()); | 
|  |  | 
|  | EXPECT_THAT_ERROR(O.keep(), Succeeded()); | 
|  | EXPECT_THAT_ERROR(Provider->checkKept(FilePath, Data), Succeeded()); | 
|  | EXPECT_FALSE(O.isOpen()); | 
|  | } | 
|  |  | 
|  | TEST_P(BackendTest, KeepFlush) { | 
|  | auto Backend = createBackend(); | 
|  | std::string FilePath = Provider->getFilePathToCreate(); | 
|  | StringRef Data = "some data"; | 
|  | OutputFile O; | 
|  | EXPECT_THAT_ERROR(Backend->createFile(FilePath).moveInto(O), Succeeded()); | 
|  | consumeDiscardOnDestroy(O); | 
|  | ASSERT_THAT_ERROR(Provider->checkCreated(FilePath), Succeeded()); | 
|  |  | 
|  | O << Data; | 
|  | EXPECT_THAT_ERROR(Provider->checkWrote(FilePath, Data), Succeeded()); | 
|  |  | 
|  | O.getOS().flush(); | 
|  | EXPECT_THAT_ERROR(Provider->checkFlushed(FilePath, Data), Succeeded()); | 
|  |  | 
|  | EXPECT_THAT_ERROR(O.keep(), Succeeded()); | 
|  | EXPECT_THAT_ERROR(Provider->checkKept(FilePath, Data), Succeeded()); | 
|  | } | 
|  |  | 
|  | TEST_P(BackendTest, KeepFlushProxy) { | 
|  | auto Backend = createBackend(); | 
|  | std::string FilePath = Provider->getFilePathToCreate(); | 
|  | StringRef Data = "some data"; | 
|  | OutputFile O; | 
|  | EXPECT_THAT_ERROR(Backend->createFile(FilePath).moveInto(O), Succeeded()); | 
|  | consumeDiscardOnDestroy(O); | 
|  | ASSERT_THAT_ERROR(Provider->checkCreated(FilePath), Succeeded()); | 
|  | { | 
|  | std::unique_ptr<raw_pwrite_stream> Proxy; | 
|  | EXPECT_THAT_ERROR(O.createProxy().moveInto(Proxy), Succeeded()); | 
|  | *Proxy << Data; | 
|  | EXPECT_THAT_ERROR(Provider->checkWrote(FilePath, Data), Succeeded()); | 
|  |  | 
|  | Proxy->flush(); | 
|  | EXPECT_THAT_ERROR(Provider->checkFlushed(FilePath, Data), Succeeded()); | 
|  | } | 
|  | EXPECT_THAT_ERROR(O.keep(), Succeeded()); | 
|  | EXPECT_THAT_ERROR(Provider->checkKept(FilePath, Data), Succeeded()); | 
|  | } | 
|  |  | 
|  | TEST_P(BackendTest, KeepEmpty) { | 
|  | auto Backend = createBackend(); | 
|  | std::string FilePath = Provider->getFilePathToCreate(); | 
|  | OutputFile O; | 
|  | EXPECT_THAT_ERROR(Backend->createFile(FilePath).moveInto(O), Succeeded()); | 
|  | consumeDiscardOnDestroy(O); | 
|  | ASSERT_THAT_ERROR(Provider->checkCreated(FilePath), Succeeded()); | 
|  | EXPECT_THAT_ERROR(O.keep(), Succeeded()); | 
|  | EXPECT_THAT_ERROR(Provider->checkKept(FilePath, ""), Succeeded()); | 
|  | } | 
|  |  | 
|  | TEST_P(BackendTest, KeepMissingDirectory) { | 
|  | auto Backend = createBackend(); | 
|  | std::string FilePath = Provider->getFilePathToCreateUnder("missing"); | 
|  | StringRef Data = "some data"; | 
|  |  | 
|  | OutputFile O; | 
|  | EXPECT_THAT_ERROR(Backend->createFile(FilePath).moveInto(O), Succeeded()); | 
|  | consumeDiscardOnDestroy(O); | 
|  | ASSERT_THAT_ERROR(Provider->checkCreated(FilePath), Succeeded()); | 
|  |  | 
|  | O << Data; | 
|  | EXPECT_THAT_ERROR(O.keep(), Succeeded()); | 
|  | EXPECT_THAT_ERROR(Provider->checkKept(FilePath, Data), Succeeded()); | 
|  | } | 
|  |  | 
|  | TEST_P(BackendTest, KeepMissingDirectoryNested) { | 
|  | auto Backend = createBackend(); | 
|  | std::string FilePath = | 
|  | Provider->getFilePathToCreateUnder("missing", "nested"); | 
|  | StringRef Data = "some data"; | 
|  |  | 
|  | OutputFile O; | 
|  | EXPECT_THAT_ERROR(Backend->createFile(FilePath).moveInto(O), Succeeded()); | 
|  | consumeDiscardOnDestroy(O); | 
|  | ASSERT_THAT_ERROR(Provider->checkCreated(FilePath), Succeeded()); | 
|  |  | 
|  | O << Data; | 
|  | EXPECT_THAT_ERROR(O.keep(), Succeeded()); | 
|  | EXPECT_THAT_ERROR(Provider->checkKept(FilePath, Data), Succeeded()); | 
|  | } | 
|  |  | 
|  | TEST_P(BackendTest, KeepNoAtomicWrite) { | 
|  | auto Backend = createBackend(); | 
|  | std::string FilePath = Provider->getFilePathToCreate(); | 
|  | StringRef Data = "some data"; | 
|  | OutputConfig Config = OutputConfig().setNoAtomicWrite(); | 
|  |  | 
|  | OutputFile O; | 
|  | EXPECT_THAT_ERROR(Backend->createFile(FilePath, Config).moveInto(O), | 
|  | Succeeded()); | 
|  | consumeDiscardOnDestroy(O); | 
|  | ASSERT_THAT_ERROR(Provider->checkCreated(FilePath, Config), Succeeded()); | 
|  | O << Data; | 
|  | EXPECT_THAT_ERROR(Provider->checkWrote(FilePath, Data), Succeeded()); | 
|  |  | 
|  | EXPECT_THAT_ERROR(O.keep(), Succeeded()); | 
|  | EXPECT_THAT_ERROR(Provider->checkKept(FilePath, Data), Succeeded()); | 
|  | EXPECT_FALSE(O.isOpen()); | 
|  | } | 
|  |  | 
|  | TEST_P(BackendTest, KeepNoAtomicWriteMissingDirectory) { | 
|  | auto Backend = createBackend(); | 
|  | std::string FilePath = Provider->getFilePathToCreate(); | 
|  | StringRef Data = "some data"; | 
|  | OutputConfig Config = OutputConfig().setNoAtomicWrite(); | 
|  |  | 
|  | OutputFile O; | 
|  | EXPECT_THAT_ERROR(Backend->createFile(FilePath, Config).moveInto(O), | 
|  | Succeeded()); | 
|  | consumeDiscardOnDestroy(O); | 
|  | ASSERT_THAT_ERROR(Provider->checkCreated(FilePath, Config), Succeeded()); | 
|  |  | 
|  | O << Data; | 
|  | EXPECT_THAT_ERROR(Provider->checkWrote(FilePath, Data), Succeeded()); | 
|  |  | 
|  | EXPECT_THAT_ERROR(O.keep(), Succeeded()); | 
|  | EXPECT_THAT_ERROR(Provider->checkKept(FilePath, Data), Succeeded()); | 
|  | EXPECT_FALSE(O.isOpen()); | 
|  | } | 
|  |  | 
|  | TEST_P(BackendTest, KeepMissingDirectoryNoImply) { | 
|  | // Skip this test if the backend doesn't have a concept of missing | 
|  | // directories. | 
|  | if (!Provider->rejectsMissingDirectories()) | 
|  | return; | 
|  |  | 
|  | auto Backend = createBackend(); | 
|  | std::string FilePath = Provider->getFilePathToCreateUnder("missing"); | 
|  | std::error_code EC = errorToErrorCode( | 
|  | consumeDiscardOnDestroy( | 
|  | Backend->createFile(FilePath, | 
|  | OutputConfig().setNoImplyCreateDirectories())) | 
|  | .takeError()); | 
|  | EXPECT_EQ(int(std::errc::no_such_file_or_directory), EC.value()); | 
|  | } | 
|  |  | 
|  | class NullOutputBackendProvider : public OutputBackendProvider { | 
|  | public: | 
|  | bool rejectsMissingDirectories() override { return false; } | 
|  |  | 
|  | IntrusiveRefCntPtr<OutputBackend> createBackend() override { | 
|  | return makeNullOutputBackend(); | 
|  | } | 
|  | std::string getFilePathToCreate() override { return "ignored.data"; } | 
|  | std::string getFilePathToCreateUnder(StringRef Parent1, | 
|  | StringRef Parent2) override { | 
|  | SmallString<128> Path; | 
|  | sys::path::append(Path, Parent1, Parent2, getFilePathToCreate()); | 
|  | return Path.str().str(); | 
|  | } | 
|  | Error checkCreated(StringRef, OutputConfig) override { | 
|  | return Error::success(); | 
|  | } | 
|  | Error checkWrote(StringRef, StringRef) override { return Error::success(); } | 
|  | Error checkFlushed(StringRef, StringRef) override { return Error::success(); } | 
|  | Error checkKept(StringRef, StringRef) override { return Error::success(); } | 
|  | Error checkDiscarded(StringRef) override { return Error::success(); } | 
|  | }; | 
|  |  | 
|  | struct OnDiskFile { | 
|  | const unittest::TempDir &D; | 
|  | SmallString<128> Path; | 
|  | StringRef ParentPath; | 
|  | StringRef Filename; | 
|  | StringRef Stem; | 
|  | StringRef Extension; | 
|  | std::unique_ptr<MemoryBuffer> LastBuffer; | 
|  |  | 
|  | OnDiskFile(const unittest::TempDir &D, const Twine &InputPath) : D(D) { | 
|  | if (sys::path::is_absolute(InputPath)) | 
|  | InputPath.toVector(Path); | 
|  | else | 
|  | sys::path::append(Path, D.path(), InputPath); | 
|  | ParentPath = sys::path::parent_path(Path); | 
|  | Filename = sys::path::filename(Path); | 
|  | Stem = sys::path::stem(Filename); | 
|  | Extension = sys::path::extension(Filename); | 
|  | } | 
|  |  | 
|  | std::optional<OnDiskFile> findTemp() const; | 
|  |  | 
|  | std::optional<sys::fs::UniqueID> getCurrentUniqueID(); | 
|  |  | 
|  | bool hasUniqueID(sys::fs::UniqueID ID) { | 
|  | auto CurrentID = getCurrentUniqueID(); | 
|  | if (!CurrentID) | 
|  | return false; | 
|  | return *CurrentID == ID; | 
|  | } | 
|  |  | 
|  | std::optional<StringRef> getCurrentContent() { | 
|  | auto OnDiskOrErr = MemoryBuffer::getFile(Path); | 
|  | if (!OnDiskOrErr) | 
|  | return std::nullopt; | 
|  | LastBuffer = std::move(*OnDiskOrErr); | 
|  | return LastBuffer->getBuffer(); | 
|  | } | 
|  |  | 
|  | bool equalsCurrentContent(StringRef Data) { | 
|  | auto CurrentContent = getCurrentContent(); | 
|  | if (!CurrentContent) | 
|  | return false; | 
|  | return *CurrentContent == Data; | 
|  | } | 
|  |  | 
|  | bool equalsCurrentContent(std::nullopt_t) { | 
|  | return getCurrentContent() == std::nullopt; | 
|  | } | 
|  | }; | 
|  |  | 
|  | class OnDiskOutputBackendProvider : public OutputBackendProvider { | 
|  | public: | 
|  | bool rejectsMissingDirectories() override { return true; } | 
|  |  | 
|  | std::optional<unittest::TempDir> D; | 
|  |  | 
|  | IntrusiveRefCntPtr<OutputBackend> createBackend() override { | 
|  | auto Backend = makeIntrusiveRefCnt<OnDiskOutputBackend>(); | 
|  | Backend->Settings = Settings; | 
|  | return Backend; | 
|  | } | 
|  | void init() { | 
|  | if (!D) | 
|  | D.emplace("OutputBackendTest.d", /*Unique=*/true); | 
|  | } | 
|  | std::string getFilePathToCreate() override { | 
|  | init(); | 
|  | return OnDiskFile(*D, "file.data").Path.str().str(); | 
|  | } | 
|  | std::string getFilePathToCreateUnder(StringRef Parent1, | 
|  | StringRef Parent2) override { | 
|  | init(); | 
|  | SmallString<128> Path; | 
|  | sys::path::append(Path, D->path(), Parent1, Parent2, "file.data"); | 
|  | return Path.str().str(); | 
|  | } | 
|  |  | 
|  | Error checkCreated(StringRef FilePath, OutputConfig Config) override; | 
|  | Error checkWrote(StringRef FilePath, StringRef Data) override; | 
|  | Error checkFlushed(StringRef FilePath, StringRef Data) override; | 
|  | Error checkKept(StringRef FilePath, StringRef Data) override; | 
|  | Error checkDiscarded(StringRef FilePath) override; | 
|  |  | 
|  | struct FileInfo { | 
|  | OutputConfig Config; | 
|  | std::optional<OnDiskFile> F; | 
|  | std::optional<OnDiskFile> Temp; | 
|  | std::optional<sys::fs::UniqueID> UID; | 
|  | std::optional<sys::fs::UniqueID> TempUID; | 
|  | }; | 
|  | Error checkOpen(FileInfo &Info); | 
|  | bool shouldUseTemporaries(const FileInfo &Info) const; | 
|  |  | 
|  | OnDiskOutputBackendProvider() = default; | 
|  | explicit OnDiskOutputBackendProvider( | 
|  | const OnDiskOutputBackend::OutputSettings &Settings) | 
|  | : Settings(Settings) {} | 
|  | OnDiskOutputBackend::OutputSettings Settings; | 
|  |  | 
|  | StringMap<FileInfo> Files; | 
|  | Error lookupFileInfo(StringRef FilePath, FileInfo *&Info); | 
|  | }; | 
|  |  | 
|  | bool OnDiskOutputBackendProvider::shouldUseTemporaries( | 
|  | const FileInfo &Info) const { | 
|  | return Info.Config.getAtomicWrite() && Settings.UseTemporaries; | 
|  | } | 
|  |  | 
|  | struct ProviderGeneratorList { | 
|  | std::vector<OutputBackendProvider::Generator> Generators; | 
|  | ProviderGeneratorList( | 
|  | std::initializer_list<OutputBackendProvider::Generator> IL) | 
|  | : Generators(IL) {} | 
|  |  | 
|  | std::string operator()( | 
|  | const ::testing::TestParamInfo<OutputBackendProvider::Generator> &Info) { | 
|  | return Info.param.Name; | 
|  | } | 
|  | }; | 
|  |  | 
|  | ProviderGeneratorList BackendGenerators = { | 
|  | {"Null", []() { return std::make_unique<NullOutputBackendProvider>(); }}, | 
|  | {"OnDisk", | 
|  | []() { return std::make_unique<OnDiskOutputBackendProvider>(); }}, | 
|  | {"OnDisk_DisableRemoveOnSignal", | 
|  | []() { | 
|  | OnDiskOutputBackend::OutputSettings Settings; | 
|  | Settings.RemoveOnSignal = false; | 
|  | return std::make_unique<OnDiskOutputBackendProvider>(Settings); | 
|  | }}, | 
|  | {"OnDisk_DisableTemporaries", | 
|  | []() { | 
|  | OnDiskOutputBackend::OutputSettings Settings; | 
|  | Settings.UseTemporaries = false; | 
|  | return std::make_unique<OnDiskOutputBackendProvider>(Settings); | 
|  | }}, | 
|  | }; | 
|  |  | 
|  | INSTANTIATE_TEST_SUITE_P(VirtualOutput, BackendTest, | 
|  | ::testing::ValuesIn(BackendGenerators.Generators), | 
|  | BackendGenerators); | 
|  |  | 
|  | std::optional<sys::fs::UniqueID> OnDiskFile::getCurrentUniqueID() { | 
|  | sys::fs::file_status Status; | 
|  | sys::fs::status(Path, Status, /*follow=*/false); | 
|  | if (!sys::fs::is_regular_file(Status)) | 
|  | return std::nullopt; | 
|  | return Status.getUniqueID(); | 
|  | } | 
|  |  | 
|  | std::optional<OnDiskFile> OnDiskFile::findTemp() const { | 
|  | std::error_code EC; | 
|  | for (sys::fs::directory_iterator I(ParentPath, EC), E; !EC && I != E; | 
|  | I.increment(EC)) { | 
|  | StringRef TempPath = I->path(); | 
|  | if (!TempPath.starts_with(D.path())) | 
|  | continue; | 
|  |  | 
|  | // Look for "<stem>-*.<extension>.tmp". | 
|  | if (sys::path::extension(TempPath) != ".tmp") | 
|  | continue; | 
|  |  | 
|  | // Drop the ".tmp" and check the extension and stem. | 
|  | StringRef TempStem = sys::path::stem(TempPath); | 
|  | if (sys::path::extension(TempStem) != Extension) | 
|  | continue; | 
|  | StringRef OriginalStem = sys::path::stem(TempStem); | 
|  | if (!OriginalStem.starts_with(Stem)) | 
|  | continue; | 
|  | if (!OriginalStem.drop_front(Stem.size()).starts_with("-")) | 
|  | continue; | 
|  |  | 
|  | // Found it. | 
|  | return OnDiskFile(D, TempPath.drop_front(D.path().size() + 1)); | 
|  | } | 
|  | return std::nullopt; | 
|  | } | 
|  |  | 
|  | Error OnDiskOutputBackendProvider::lookupFileInfo(StringRef FilePath, | 
|  | FileInfo *&Info) { | 
|  | auto I = Files.find(FilePath); | 
|  | if (Files.find(FilePath) == Files.end()) | 
|  | return createStringError(inconvertibleErrorCode(), | 
|  | "Missing call to checkCreated()"); | 
|  | Info = &I->second; | 
|  | assert(Info->F && "Expected OnDiskFile to be initialized"); | 
|  | return Error::success(); | 
|  | } | 
|  |  | 
|  | Error OnDiskOutputBackendProvider::checkOpen(FileInfo &Info) { | 
|  | // Collect info about filesystem state. | 
|  | assert(Info.F); | 
|  | std::optional<sys::fs::UniqueID> UID = Info.F->getCurrentUniqueID(); | 
|  | std::optional<OnDiskFile> Temp = Info.F->findTemp(); | 
|  | std::optional<sys::fs::UniqueID> TempUID; | 
|  | if (Temp) | 
|  | TempUID = Temp->getCurrentUniqueID(); | 
|  |  | 
|  | // Check if it's correct. | 
|  | if (shouldUseTemporaries(Info)) { | 
|  | if (!Temp) | 
|  | return createStringError(inconvertibleErrorCode(), | 
|  | "Missing temporary file"); | 
|  | if (!TempUID) | 
|  | return createStringError(inconvertibleErrorCode(), | 
|  | "Missing UID for temporary"); | 
|  | if (UID) | 
|  | return createStringError( | 
|  | inconvertibleErrorCode(), | 
|  | "Unexpected final UID when temporaries should be used"); | 
|  |  | 
|  | // Check previous data. | 
|  | if (Info.Temp) | 
|  | if (Temp->Path != Info.Temp->Path) | 
|  | return createStringError(inconvertibleErrorCode(), | 
|  | "Temporary path changed"); | 
|  | if (Info.TempUID) | 
|  | if (*TempUID != *Info.TempUID) | 
|  | return createStringError(inconvertibleErrorCode(), | 
|  | "Temporary UID changed"); | 
|  | } else { | 
|  | if (Temp) | 
|  | return createStringError(inconvertibleErrorCode(), | 
|  | "Unexpected temporary file"); | 
|  | if (!UID) | 
|  | return createStringError(inconvertibleErrorCode(), | 
|  | "Missing UID for temporary"); | 
|  |  | 
|  | // Check previous data. | 
|  | if (Info.UID) | 
|  | if (*UID != *Info.UID) | 
|  | return createStringError(inconvertibleErrorCode(), "UID changed"); | 
|  | } | 
|  |  | 
|  | Info.UID = UID; | 
|  | if (Temp) | 
|  | Info.Temp.emplace(*D, Temp->Path); | 
|  | else | 
|  | Info.Temp.reset(); | 
|  | Info.TempUID = TempUID; | 
|  | return Error::success(); | 
|  | } | 
|  |  | 
|  | Error OnDiskOutputBackendProvider::checkCreated(StringRef FilePath, | 
|  | OutputConfig Config) { | 
|  | auto &Info = Files[FilePath]; | 
|  | if (Info.F) { | 
|  | assert(OnDiskFile(*D, FilePath).Path == Info.F->Path); | 
|  | Info.UID = std::nullopt; | 
|  | Info.Temp.reset(); | 
|  | Info.TempUID = std::nullopt; | 
|  | } else { | 
|  | Info.F.emplace(*D, FilePath); | 
|  | } | 
|  | Info.Config = Config; | 
|  | return checkOpen(Info); | 
|  | } | 
|  |  | 
|  | Error OnDiskOutputBackendProvider::checkWrote(StringRef FilePath, | 
|  | StringRef Data) { | 
|  | FileInfo *Info = nullptr; | 
|  | if (Error E = lookupFileInfo(FilePath, Info)) | 
|  | return E; | 
|  | return checkOpen(*Info); | 
|  | } | 
|  |  | 
|  | Error OnDiskOutputBackendProvider::checkFlushed(StringRef FilePath, | 
|  | StringRef Data) { | 
|  | FileInfo *Info = nullptr; | 
|  | if (Error E = lookupFileInfo(FilePath, Info)) | 
|  | return E; | 
|  | if (Error E = checkOpen(*Info)) | 
|  | return E; | 
|  |  | 
|  | OnDiskFile &F = shouldUseTemporaries(*Info) ? *Info->Temp : *Info->F; | 
|  | if (!F.equalsCurrentContent(Data)) | 
|  | return createStringError(inconvertibleErrorCode(), "content not flushed"); | 
|  | return Error::success(); | 
|  | } | 
|  |  | 
|  | Error OnDiskOutputBackendProvider::checkKept(StringRef FilePath, | 
|  | StringRef Data) { | 
|  | FileInfo *Info = nullptr; | 
|  | if (Error E = lookupFileInfo(FilePath, Info)) | 
|  | return E; | 
|  |  | 
|  | #ifndef _WIN32 | 
|  | sys::fs::UniqueID UID = | 
|  | shouldUseTemporaries(*Info) ? *Info->TempUID : *Info->UID; | 
|  | if (!Info->F->hasUniqueID(UID)) | 
|  | return createStringError(inconvertibleErrorCode(), | 
|  | "File not created by keep or changed UID"); | 
|  | #else | 
|  | // On Windows, the UID changes in a rename that happens in keep() | 
|  | // because it's based on hash of file paths. Instead check that the | 
|  | // file contents are the same. | 
|  | if (!Info->F->equalsCurrentContent(Data)) | 
|  | return createStringError(inconvertibleErrorCode(), | 
|  | "File not created by keep"); | 
|  | #endif | 
|  |  | 
|  | if (std::optional<OnDiskFile> Temp = Info->F->findTemp()) | 
|  | return createStringError(inconvertibleErrorCode(), | 
|  | "Temporary not removed by keep"); | 
|  |  | 
|  | return Error::success(); | 
|  | } | 
|  |  | 
|  | Error OnDiskOutputBackendProvider::checkDiscarded(StringRef FilePath) { | 
|  | FileInfo *Info = nullptr; | 
|  | if (Error E = lookupFileInfo(FilePath, Info)) | 
|  | return E; | 
|  |  | 
|  | if (std::optional<sys::fs::UniqueID> UID = Info->F->getCurrentUniqueID()) | 
|  | return createStringError(inconvertibleErrorCode(), | 
|  | "File not removed by discard"); | 
|  |  | 
|  | if (std::optional<OnDiskFile> Temp = Info->F->findTemp()) | 
|  | return createStringError(inconvertibleErrorCode(), | 
|  | "Temporary not removed by discard"); | 
|  |  | 
|  | return Error::success(); | 
|  | } | 
|  |  | 
|  | TEST(VirtualOutputBackendAdaptors, makeFilteringOutputBackend) { | 
|  | bool ShouldCreate = false; | 
|  | auto Backend = makeFilteringOutputBackend( | 
|  | makeIntrusiveRefCnt<OnDiskOutputBackend>(), | 
|  | [&ShouldCreate](StringRef, std::optional<OutputConfig>) { | 
|  | return ShouldCreate; | 
|  | }); | 
|  |  | 
|  | int Count = 0; | 
|  | unittest::TempDir D("FilteringOutputBackendTest.d", /*Unique=*/true); | 
|  | for (bool ShouldCreateVal : {false, true, true, false}) { | 
|  | ShouldCreate = ShouldCreateVal; | 
|  | OnDiskFile OnDisk(D, "file." + Twine(Count++) + "." + Twine(ShouldCreate)); | 
|  | OutputFile Output; | 
|  | ASSERT_THAT_ERROR(consumeDiscardOnDestroy(Backend->createFile(OnDisk.Path)) | 
|  | .moveInto(Output), | 
|  | Succeeded()); | 
|  | EXPECT_NE(ShouldCreate, Output.isNull()); | 
|  | Output << "content"; | 
|  | EXPECT_THAT_ERROR(Output.keep(), Succeeded()); | 
|  |  | 
|  | if (ShouldCreate) { | 
|  | EXPECT_EQ(StringRef("content"), OnDisk.getCurrentContent()); | 
|  | } else { | 
|  | EXPECT_FALSE(OnDisk.getCurrentUniqueID()); | 
|  | } | 
|  | } | 
|  | SmallString<128> Path; | 
|  | } | 
|  |  | 
|  | class AbsolutePathBackend : public ProxyOutputBackend { | 
|  | IntrusiveRefCntPtr<OutputBackend> cloneImpl() const override { | 
|  | llvm_unreachable("unimplemented"); | 
|  | } | 
|  |  | 
|  | Expected<std::unique_ptr<OutputFileImpl>> | 
|  | createFileImpl(StringRef Path, std::optional<OutputConfig> Config) override { | 
|  | assert(!sys::path::is_absolute(Path) && | 
|  | "Expected tests to pass all relative paths"); | 
|  | SmallString<256> AbsPath; | 
|  | sys::path::append(AbsPath, CWD, Path); | 
|  | return ProxyOutputBackend::createFileImpl(AbsPath, Config); | 
|  | } | 
|  |  | 
|  | public: | 
|  | AbsolutePathBackend(const Twine &CWD, | 
|  | IntrusiveRefCntPtr<OutputBackend> Backend) | 
|  | : ProxyOutputBackend(std::move(Backend)), CWD(CWD.str()) { | 
|  | assert(sys::path::is_absolute(this->CWD) && | 
|  | "Expected tests to pass a relative path"); | 
|  | } | 
|  |  | 
|  | private: | 
|  | std::string CWD; | 
|  | }; | 
|  |  | 
|  | TEST(VirtualOutputBackendAdaptors, makeMirroringOutputBackend) { | 
|  | unittest::TempDir D1("MirroringOutputBackendTest.1.d", /*Unique=*/true); | 
|  | unittest::TempDir D2("MirroringOutputBackendTest.2.d", /*Unique=*/true); | 
|  |  | 
|  | IntrusiveRefCntPtr<OutputBackend> Backend; | 
|  | { | 
|  | auto OnDisk = makeIntrusiveRefCnt<OnDiskOutputBackend>(); | 
|  | Backend = makeMirroringOutputBackend( | 
|  | makeIntrusiveRefCnt<AbsolutePathBackend>(D1.path(), OnDisk), | 
|  | makeIntrusiveRefCnt<AbsolutePathBackend>(D2.path(), OnDisk)); | 
|  | } | 
|  |  | 
|  | OnDiskFile OnDisk1(D1, "file"); | 
|  | OnDiskFile OnDisk2(D2, "file"); | 
|  | OutputFile Output; | 
|  | ASSERT_THAT_ERROR( | 
|  | consumeDiscardOnDestroy(Backend->createFile("file")).moveInto(Output), | 
|  | Succeeded()); | 
|  | EXPECT_TRUE(OnDisk1.findTemp()); | 
|  | EXPECT_TRUE(OnDisk2.findTemp()); | 
|  |  | 
|  | Output << "content"; | 
|  | Output.getOS().pwrite("ON", /*Size=*/2, /*Offset=*/1); | 
|  | EXPECT_THAT_ERROR(Output.keep(), Succeeded()); | 
|  | EXPECT_EQ(StringRef("cONtent"), OnDisk1.getCurrentContent()); | 
|  | EXPECT_EQ(StringRef("cONtent"), OnDisk2.getCurrentContent()); | 
|  | EXPECT_NE(OnDisk1.getCurrentUniqueID(), OnDisk2.getCurrentUniqueID()); | 
|  | } | 
|  |  | 
|  | /// Behaves like NullOutputFileImpl, but doesn't match the RTTI (so OutputFile | 
|  | /// cannot tell). | 
|  | class LikeNullOutputFile final : public OutputFileImpl { | 
|  | Error keep() final { return Error::success(); } | 
|  | Error discard() final { return Error::success(); } | 
|  | raw_pwrite_stream &getOS() final { return OS; } | 
|  |  | 
|  | public: | 
|  | LikeNullOutputFile(raw_null_ostream &OS) : OS(OS) {} | 
|  | raw_null_ostream &OS; | 
|  | }; | 
|  | class LikeNullOutputBackend final : public OutputBackend { | 
|  | IntrusiveRefCntPtr<OutputBackend> cloneImpl() const override { | 
|  | llvm_unreachable("not implemented"); | 
|  | } | 
|  |  | 
|  | Expected<std::unique_ptr<OutputFileImpl>> | 
|  | createFileImpl(StringRef Path, std::optional<OutputConfig> Config) override { | 
|  | return std::make_unique<LikeNullOutputFile>(OS); | 
|  | } | 
|  |  | 
|  | public: | 
|  | raw_null_ostream OS; | 
|  | }; | 
|  |  | 
|  | TEST(VirtualOutputBackendAdaptors, makeMirroringOutputBackendNull) { | 
|  | // Check that null outputs are skipped by seeing that LikeNull->OS is passed | 
|  | // through directly (without a mirroring proxy stream) to Output. | 
|  | auto LikeNull = makeIntrusiveRefCnt<LikeNullOutputBackend>(); | 
|  | auto Null1 = makeNullOutputBackend(); | 
|  | auto Mirror = makeMirroringOutputBackend(Null1, LikeNull); | 
|  | OutputFile Output; | 
|  | ASSERT_THAT_ERROR( | 
|  | consumeDiscardOnDestroy(Mirror->createFile("file")).moveInto(Output), | 
|  | Succeeded()); | 
|  | EXPECT_TRUE(!Output.isNull()); | 
|  | EXPECT_EQ(&Output.getOS(), &LikeNull->OS); | 
|  |  | 
|  | // Check the other direction. | 
|  | Mirror = makeMirroringOutputBackend(LikeNull, Null1); | 
|  | ASSERT_THAT_ERROR( | 
|  | consumeDiscardOnDestroy(Mirror->createFile("file")).moveInto(Output), | 
|  | Succeeded()); | 
|  | EXPECT_TRUE(!Output.isNull()); | 
|  | EXPECT_EQ(&Output.getOS(), &LikeNull->OS); | 
|  |  | 
|  | // Same null backend, twice. | 
|  | Mirror = makeMirroringOutputBackend(Null1, Null1); | 
|  | ASSERT_THAT_ERROR( | 
|  | consumeDiscardOnDestroy(Mirror->createFile("file")).moveInto(Output), | 
|  | Succeeded()); | 
|  | EXPECT_TRUE(Output.isNull()); | 
|  |  | 
|  | // Two null backends. | 
|  | auto Null2 = makeNullOutputBackend(); | 
|  | Mirror = makeMirroringOutputBackend(Null1, Null2); | 
|  | ASSERT_THAT_ERROR( | 
|  | consumeDiscardOnDestroy(Mirror->createFile("file")).moveInto(Output), | 
|  | Succeeded()); | 
|  | EXPECT_TRUE(Output.isNull()); | 
|  | } | 
|  |  | 
|  | class StringErrorBackend final : public OutputBackend { | 
|  | IntrusiveRefCntPtr<OutputBackend> cloneImpl() const override { | 
|  | llvm_unreachable("not implemented"); | 
|  | } | 
|  |  | 
|  | Expected<std::unique_ptr<OutputFileImpl>> | 
|  | createFileImpl(StringRef Path, std::optional<OutputConfig> Config) override { | 
|  | return createStringError(inconvertibleErrorCode(), Msg); | 
|  | } | 
|  |  | 
|  | public: | 
|  | StringErrorBackend(const Twine &Msg) : Msg(Msg.str()) {} | 
|  | std::string Msg; | 
|  | }; | 
|  |  | 
|  | TEST(VirtualOutputBackendAdaptors, makeMirroringOutputBackendCreateError) { | 
|  | auto Error1 = makeIntrusiveRefCnt<StringErrorBackend>("error-backend-1"); | 
|  | auto Null = makeNullOutputBackend(); | 
|  |  | 
|  | auto Mirror = makeMirroringOutputBackend(Null, Error1); | 
|  | EXPECT_THAT_ERROR( | 
|  | consumeDiscardOnDestroy(Mirror->createFile("file")).takeError(), | 
|  | FailedWithMessage(Error1->Msg)); | 
|  |  | 
|  | Mirror = makeMirroringOutputBackend(Error1, Null); | 
|  | EXPECT_THAT_ERROR( | 
|  | consumeDiscardOnDestroy(Mirror->createFile("file")).takeError(), | 
|  | FailedWithMessage(Error1->Msg)); | 
|  |  | 
|  | auto Error2 = makeIntrusiveRefCnt<StringErrorBackend>("error-backend-2"); | 
|  | Mirror = makeMirroringOutputBackend(Error1, Error2); | 
|  | EXPECT_THAT_ERROR( | 
|  | consumeDiscardOnDestroy(Mirror->createFile("file")).takeError(), | 
|  | FailedWithMessage(Error1->Msg)); | 
|  | } | 
|  |  | 
|  | TEST(OnDiskBackendTest, OnlyIfDifferent) { | 
|  | OnDiskOutputBackendProvider Provider; | 
|  | auto Backend = Provider.createBackend(); | 
|  | std::string FilePath = Provider.getFilePathToCreate(); | 
|  | StringRef Data = "some data"; | 
|  | OutputConfig Config = OutputConfig().setOnlyIfDifferent(); | 
|  |  | 
|  | OutputFile O1, O2, O3; | 
|  | sys::fs::file_status Status1, Status2, Status3; | 
|  | // Write first file. | 
|  | EXPECT_THAT_ERROR(Backend->createFile(FilePath, Config).moveInto(O1), | 
|  | Succeeded()); | 
|  | O1 << Data; | 
|  | EXPECT_THAT_ERROR(O1.keep(), Succeeded()); | 
|  | EXPECT_FALSE(O1.isOpen()); | 
|  | EXPECT_FALSE(sys::fs::status(FilePath, Status1, /*follow=*/false)); | 
|  |  | 
|  | // Write second with same content. | 
|  | EXPECT_THAT_ERROR(Backend->createFile(FilePath, Config).moveInto(O2), | 
|  | Succeeded()); | 
|  | O2 << Data; | 
|  | EXPECT_THAT_ERROR(O2.keep(), Succeeded()); | 
|  | EXPECT_FALSE(O2.isOpen()); | 
|  | EXPECT_FALSE(sys::fs::status(FilePath, Status2, /*follow=*/false)); | 
|  |  | 
|  | #ifndef _WIN32 | 
|  | // Make sure the output path file is not modified with same content. | 
|  | EXPECT_EQ(Status1.getUniqueID(), Status2.getUniqueID()); | 
|  | #else | 
|  | // On Windows, UniqueIDs are currently hash of file paths and don't | 
|  | // change on overwrites or depend on the file content. Check that | 
|  | // the file content is what is expected instead. | 
|  | auto EqualsCurrentContent = [](StringRef FilePath, StringRef Data) -> bool { | 
|  | auto BufOrErr = MemoryBuffer::getFile(FilePath); | 
|  | if (!BufOrErr) | 
|  | return false; | 
|  | return (*BufOrErr)->getBuffer() == Data; | 
|  | }; | 
|  | EXPECT_TRUE(EqualsCurrentContent(FilePath, Data)); | 
|  | #endif | 
|  |  | 
|  | // Write third with different content. | 
|  | EXPECT_THAT_ERROR(Backend->createFile(FilePath, Config).moveInto(O3), | 
|  | Succeeded()); | 
|  | O3 << Data << "\n"; | 
|  | EXPECT_THAT_ERROR(O3.keep(), Succeeded()); | 
|  | EXPECT_FALSE(O3.isOpen()); | 
|  | EXPECT_FALSE(sys::fs::status(FilePath, Status3, /*follow=*/false)); | 
|  |  | 
|  | #ifndef _WIN32 | 
|  | // This should overwrite the file and create a different UniqueID. | 
|  | EXPECT_NE(Status1.getUniqueID(), Status3.getUniqueID()); | 
|  | #else | 
|  | // On Windows, UniqueIDs are currently hash of file paths and don't | 
|  | // change on overwrites or depend on the file content. Check that | 
|  | // the file content is what is expected instead. | 
|  | EXPECT_TRUE(EqualsCurrentContent(FilePath, (Data + "\n").str())); | 
|  | #endif | 
|  | } | 
|  |  | 
|  | TEST(OnDiskBackendTest, Append) { | 
|  | OnDiskOutputBackendProvider Provider; | 
|  | auto Backend = Provider.createBackend(); | 
|  | std::string FilePath = Provider.getFilePathToCreate(); | 
|  | OutputConfig Config = OutputConfig().setAppend(); | 
|  |  | 
|  | OutputFile O1, O2, O3; | 
|  | // Write first file. | 
|  | EXPECT_THAT_ERROR(Backend->createFile(FilePath, Config).moveInto(O1), | 
|  | Succeeded()); | 
|  | O1 << "some data\n"; | 
|  | EXPECT_THAT_ERROR(O1.keep(), Succeeded()); | 
|  | EXPECT_FALSE(O1.isOpen()); | 
|  |  | 
|  | OnDiskFile File1(*Provider.D, FilePath); | 
|  | EXPECT_TRUE(File1.equalsCurrentContent("some data\n")); | 
|  |  | 
|  | // Append same data. | 
|  | EXPECT_THAT_ERROR(Backend->createFile(FilePath, Config).moveInto(O2), | 
|  | Succeeded()); | 
|  | O2 << "more data\n"; | 
|  | EXPECT_THAT_ERROR(O2.keep(), Succeeded()); | 
|  | EXPECT_FALSE(O2.isOpen()); | 
|  |  | 
|  | // Check data is appended. | 
|  | OnDiskFile File2(*Provider.D, FilePath); | 
|  | EXPECT_TRUE(File2.equalsCurrentContent("some data\nmore data\n")); | 
|  |  | 
|  | // Non atomic append. | 
|  | EXPECT_THAT_ERROR( | 
|  | Backend->createFile(FilePath, Config.setNoAtomicWrite()).moveInto(O3), | 
|  | Succeeded()); | 
|  | O3 << "more more\n"; | 
|  | EXPECT_THAT_ERROR(O3.keep(), Succeeded()); | 
|  | EXPECT_FALSE(O3.isOpen()); | 
|  |  | 
|  | // Check data is appended. | 
|  | OnDiskFile File3(*Provider.D, FilePath); | 
|  | EXPECT_TRUE(File3.equalsCurrentContent("some data\nmore data\nmore more\n")); | 
|  | } | 
|  |  | 
|  | TEST(HashingBackendTest, HashOutput) { | 
|  | HashingOutputBackend<BLAKE3> Backend; | 
|  | OutputFile O1, O2, O3, O4, O5; | 
|  | EXPECT_THAT_ERROR(Backend.createFile("file1").moveInto(O1), Succeeded()); | 
|  | O1 << "some data"; | 
|  | EXPECT_THAT_ERROR(O1.keep(), Succeeded()); | 
|  | EXPECT_THAT_ERROR(Backend.createFile("file2").moveInto(O2), Succeeded()); | 
|  | O2 << "some data"; | 
|  | EXPECT_THAT_ERROR(O2.keep(), Succeeded()); | 
|  | EXPECT_EQ(Backend.getHashValueForFile("file1"), | 
|  | Backend.getHashValueForFile("file2")); | 
|  |  | 
|  | EXPECT_THAT_ERROR(Backend.createFile("file3").moveInto(O3), Succeeded()); | 
|  | O3 << "some "; | 
|  | O3 << "data"; | 
|  | EXPECT_THAT_ERROR(O3.keep(), Succeeded()); | 
|  | EXPECT_EQ(Backend.getHashValueForFile("file1"), | 
|  | Backend.getHashValueForFile("file3")); | 
|  |  | 
|  | EXPECT_THAT_ERROR(Backend.createFile("file4").moveInto(O4), Succeeded()); | 
|  | O4 << "same data"; | 
|  | O4.getOS().pwrite("o", 1, 1); | 
|  | EXPECT_THAT_ERROR(O4.keep(), Succeeded()); | 
|  | EXPECT_EQ(Backend.getHashValueForFile("file1"), | 
|  | Backend.getHashValueForFile("file4")); | 
|  |  | 
|  | EXPECT_THAT_ERROR(Backend.createFile("file5").moveInto(O5), Succeeded()); | 
|  | O5 << "different data"; | 
|  | EXPECT_THAT_ERROR(O5.keep(), Succeeded()); | 
|  | EXPECT_NE(Backend.getHashValueForFile("file1"), | 
|  | Backend.getHashValueForFile("file5")); | 
|  | } | 
|  |  | 
|  | TEST(HashingBackendTest, ParallelHashOutput) { | 
|  | const unsigned NumFile = 20; | 
|  | DefaultThreadPool Pool; | 
|  | HashingOutputBackend<BLAKE3> Backend; | 
|  | auto getFileName = [](unsigned Idx) -> std::string { | 
|  | std::string Name; | 
|  | raw_string_ostream OS(Name); | 
|  | OS << "file" << Idx; | 
|  | return Name; | 
|  | }; | 
|  | for (unsigned I = 0; I < NumFile; ++I) { | 
|  | Pool.async( | 
|  | [&](unsigned Idx) { | 
|  | OutputFile O; | 
|  | auto Name = getFileName(Idx); | 
|  | EXPECT_THAT_ERROR(Backend.createFile(Name).moveInto(O), Succeeded()); | 
|  | O << "some data" << Idx; | 
|  | EXPECT_THAT_ERROR(O.keep(), Succeeded()); | 
|  | }, | 
|  | I); | 
|  | } | 
|  | Pool.wait(); | 
|  |  | 
|  | for (unsigned I = 0; I < NumFile; ++I) { | 
|  | auto Name = getFileName(I); | 
|  | auto Hash = Backend.getHashValueForFile(Name); | 
|  | EXPECT_TRUE(Hash); | 
|  | } | 
|  | } | 
|  | } // end namespace |