blob: 032dec5e0f690a7c652a8132b128d425a0a22f66 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/persistent_cache/backend_storage.h"
#include <optional>
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "components/persistent_cache/backend.h"
#include "components/persistent_cache/entry.h"
#include "components/persistent_cache/transaction_error.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace persistent_cache {
namespace {
using testing::EndsWith;
using testing::Property;
using testing::Return;
class MockBackendStorageDelegate : public BackendStorage::Delegate {
public:
MOCK_METHOD(base::FilePath,
GetBaseName,
(const base::FilePath& file),
(override));
MOCK_METHOD(int64_t,
DeleteFiles,
(const base::FilePath& directory,
const base::FilePath& base_name),
(override));
MOCK_METHOD(std::unique_ptr<Backend>,
MakeBackend,
(const base::FilePath& directory,
const base::FilePath& base_name),
(override));
};
class MockBackend : public Backend {
public:
MOCK_METHOD(bool, Initialize, (), (override));
MOCK_METHOD((base::expected<std::unique_ptr<Entry>, TransactionError>),
Find,
(std::string_view),
(override));
MOCK_METHOD((base::expected<void, TransactionError>),
Insert,
(std::string_view key,
base::span<const uint8_t> content,
EntryMetadata metadata),
(override));
MOCK_METHOD(BackendType, GetType, (), (const, override));
MOCK_METHOD(bool, IsReadOnly, (), (const, override));
MOCK_METHOD(std::optional<BackendParams>,
ExportReadOnlyParams,
(),
(override));
MOCK_METHOD(std::optional<BackendParams>,
ExportReadWriteParams,
(),
(override));
MOCK_METHOD(void, Abandon, (), (override));
};
class BackendStorageTest : public testing::Test {
protected:
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
auto delegate =
std::make_unique<testing::StrictMock<MockBackendStorageDelegate>>();
mock_delegate_ = delegate.get();
backend_storage_.emplace(std::move(delegate), GetStorageDir());
}
base::FilePath GetStorageDir() const {
return temp_dir_.GetPath().Append(kStorageName);
}
MockBackendStorageDelegate& mock_delegate() { return *mock_delegate_; }
BackendStorage& backend_storage() { return *backend_storage_; }
private:
static constexpr base::FilePath::CharType kStorageName[] =
FILE_PATH_LITERAL("StorageName");
base::ScopedTempDir temp_dir_;
std::optional<BackendStorage> backend_storage_;
raw_ptr<MockBackendStorageDelegate> mock_delegate_ = nullptr;
};
TEST_F(BackendStorageTest, ConstructionWorks) {
// The instance's directory should be set properly.
ASSERT_EQ(backend_storage().directory(), GetStorageDir());
// Its directory should exist.
ASSERT_PRED1(base::DirectoryExists, GetStorageDir());
}
TEST_F(BackendStorageTest, MakeBackendFails) {
base::FilePath base_name(FILE_PATH_LITERAL("some_base_name"));
EXPECT_CALL(mock_delegate(), MakeBackend(GetStorageDir(), base_name))
.WillOnce(Return(std::unique_ptr<Backend>()));
auto result = backend_storage().MakeBackend(base_name);
EXPECT_EQ(result, nullptr);
}
TEST_F(BackendStorageTest, MakeBackendSucceeds) {
base::FilePath base_name(FILE_PATH_LITERAL("some_base_name"));
auto backend = std::make_unique<testing::StrictMock<MockBackend>>();
auto* backend_raw = backend.get();
EXPECT_CALL(mock_delegate(), MakeBackend(GetStorageDir(), base_name))
.WillOnce(Return(std::move(backend)));
auto result = backend_storage().MakeBackend(base_name);
EXPECT_EQ(result.get(), backend_raw);
}
TEST_F(BackendStorageTest, DeleteAllFiles) {
// Nothing should happen if the directory is empty.
const auto storage_dir = GetStorageDir();
ASSERT_PRED1(base::IsDirectoryEmpty, storage_dir);
backend_storage().DeleteAllFiles();
// Should still have a directory.
ASSERT_PRED1(base::DirectoryExists, storage_dir);
// Add a few files.
for (int i = 0; i < 5; ++i) {
std::string ascii = base::StrCat({"ascii", base::NumberToString(i)});
base::WriteFile(storage_dir.AppendASCII(ascii), ascii);
}
ASSERT_FALSE(base::IsDirectoryEmpty(storage_dir));
// Delete should delete the files.
backend_storage().DeleteAllFiles();
// Should still have a directory.
ASSERT_PRED1(base::DirectoryExists, storage_dir);
ASSERT_PRED1(base::IsDirectoryEmpty, storage_dir);
}
// No-op if the directory is empty.
TEST_F(BackendStorageTest, BringDownTotalFootprintOfFilesEmpty) {
auto result = backend_storage().BringDownTotalFootprintOfFiles(1024);
ASSERT_EQ(result.current_footprint, 0);
ASSERT_EQ(result.number_of_bytes_deleted, 0);
}
// No-op if the directory is smaller than the threshold.
TEST_F(BackendStorageTest, BringDownTotalFootprintOfFilesBelowThreshold) {
// The delegate considers ".dat" files to be under its purview, and it will
// also delete ".txt" files.
static constexpr base::FilePath::CharType kDatExtension[] =
FILE_PATH_LITERAL(".dat");
static constexpr base::FilePath::CharType kTxtExtension[] =
FILE_PATH_LITERAL(".txt");
EXPECT_CALL(mock_delegate(), GetBaseName(Property(&base::FilePath::value,
EndsWith(kDatExtension))))
.WillRepeatedly([](const base::FilePath& path) {
return path.BaseName().RemoveFinalExtension();
});
EXPECT_CALL(mock_delegate(), GetBaseName(Property(&base::FilePath::value,
EndsWith(kTxtExtension))))
.WillRepeatedly(Return(base::FilePath()));
const auto storage_dir = GetStorageDir();
static constexpr int64_t kMaxSize = 1024;
auto kContentBytes = base::byte_span_from_cstring("hi mom");
// Write five pair of files into the dir.
int64_t expected_size = 0;
for (int i = 0; i < 5; ++i) {
std::string ascii = base::StrCat({"ascii", base::NumberToString(i)});
auto path = storage_dir.AppendASCII(ascii).AddExtension(kDatExtension);
base::WriteFile(path, kContentBytes);
expected_size += kContentBytes.size();
path = storage_dir.AppendASCII(ascii).AddExtension(kTxtExtension);
base::WriteFile(path, kContentBytes);
expected_size += kContentBytes.size();
}
// Ensure that the true size is lower than the max so that nothing happens
// below.
ASSERT_LT(expected_size, kMaxSize);
// The delegate will not be asked to delete any files.
ASSERT_EQ(backend_storage()
.BringDownTotalFootprintOfFiles(kMaxSize)
.number_of_bytes_deleted,
0);
}
// The oldest entries are deleted first when the directory is too big.
TEST_F(BackendStorageTest, BringDownTotalFootprintOfFilesAboveThreshold) {
// The delegate considers ".dat" files to be under its purview, and it will
// also delete ".txt" files.
static constexpr base::FilePath::CharType kDatExtension[] =
FILE_PATH_LITERAL(".dat");
static constexpr base::FilePath::CharType kTxtExtension[] =
FILE_PATH_LITERAL(".txt");
EXPECT_CALL(mock_delegate(), GetBaseName(Property(&base::FilePath::value,
EndsWith(kDatExtension))))
.WillRepeatedly([](const base::FilePath& path) {
return path.BaseName().RemoveFinalExtension();
});
EXPECT_CALL(mock_delegate(), GetBaseName(Property(&base::FilePath::value,
EndsWith(kTxtExtension))))
.WillRepeatedly(Return(base::FilePath()));
const auto storage_dir = GetStorageDir();
// Write four pair of files into the dir that fill it precisely.
int64_t expected_size = 0;
static constexpr int64_t kMaxSize = 1024;
std::vector<uint8_t> data(kMaxSize / 8, 42);
for (int i = 0; i < 4; ++i) {
std::string ascii = base::StrCat({"ascii", base::NumberToString(i)});
auto path = storage_dir.AppendASCII(ascii).AddExtension(kDatExtension);
ASSERT_TRUE(base::WriteFile(path, data));
expected_size += data.size();
path = storage_dir.AppendASCII(ascii).AddExtension(kTxtExtension);
ASSERT_TRUE(base::WriteFile(path, data));
expected_size += data.size();
}
ASSERT_EQ(expected_size, kMaxSize);
// Write two more pair that are older than the rest, and which put the
// directory over the limit.
base::Time last_access_time = base::Time::Now();
base::Time last_modified_time = last_access_time - base::Hours(1);
for (int i = 0; i < 2; ++i) {
std::string ascii = base::StrCat({"old", base::NumberToString(i)});
auto path = storage_dir.AppendASCII(ascii).AddExtension(kDatExtension);
ASSERT_TRUE(base::WriteFile(path, data));
expected_size += data.size();
ASSERT_TRUE(base::TouchFile(path, last_access_time, last_modified_time));
path = storage_dir.AppendASCII(ascii).AddExtension(kTxtExtension);
ASSERT_TRUE(base::WriteFile(path, data));
expected_size += data.size();
ASSERT_TRUE(base::TouchFile(path, last_access_time, last_modified_time));
// The delegate should be asked to delete these two files.
EXPECT_CALL(mock_delegate(),
DeleteFiles(GetStorageDir(), base::FilePath::FromASCII(ascii)))
.WillOnce(Return(data.size() * 2));
}
// Ensure that the true size is lower than the max so that nothing happens
// below.
ASSERT_GT(expected_size, kMaxSize);
// Delete the four older files.
auto result = backend_storage().BringDownTotalFootprintOfFiles(kMaxSize);
ASSERT_EQ(result.current_footprint, kMaxSize);
ASSERT_EQ(result.number_of_bytes_deleted,
static_cast<int64_t>(data.size()) * 4);
}
} // namespace
} // namespace persistent_cache