| //===- VirtualOutputBackendsTest.cpp - Tests for vfs::OutputBackend impls -===// |
| // |
| // 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/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, getFilePathToCreate()); |
| 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.DisableTemporaries; |
| } |
| |
| 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.DisableRemoveOnSignal = true; |
| return std::make_unique<OnDiskOutputBackendProvider>(Settings); |
| }}, |
| {"OnDisk_DisableTemporaries", |
| []() { |
| OnDiskOutputBackend::OutputSettings Settings; |
| Settings.DisableTemporaries = true; |
| 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.startswith(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.startswith(Stem)) |
| continue; |
| if (!OriginalStem.drop_front(Stem.size()).startswith("-")) |
| 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; |
| |
| 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"); |
| |
| 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)); |
| |
| // Make sure the output path file is not modified with same content. |
| EXPECT_EQ(Status1.getUniqueID(), Status2.getUniqueID()); |
| |
| // 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)); |
| |
| // This should overwrite the file and create a different UniqueID. |
| EXPECT_NE(Status1.getUniqueID(), Status3.getUniqueID()); |
| } |
| |
| 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")); |
| } |
| |
| } // end namespace |