blob: d1ee797ac93c9e6905dc5df379abc27020640ad3 [file] [log] [blame]
// Copyright 2024 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/persistent_cache.h"
#include <stdint.h>
#include <memory>
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/types/expected.h"
#include "components/persistent_cache/backend_params.h"
#include "components/persistent_cache/mock/mock_backend.h"
#include "components/persistent_cache/sqlite/sqlite_backend_impl.h"
#include "components/persistent_cache/sqlite/test_helper.h"
#include "components/persistent_cache/test_utils.h"
#include "components/persistent_cache/transaction_error.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_map.h"
namespace {
constexpr const char* kKey = "foo";
using testing::_;
using testing::Return;
class PersistentCacheMockedBackendTest : public testing::Test {
protected:
void SetUp() override {
backend_ = std::make_unique<persistent_cache::MockBackend>();
}
void CreateCache(bool successful) {
EXPECT_CALL(*backend_, Initialize()).WillOnce(Return(successful));
cache_ = std::make_unique<persistent_cache::PersistentCache>(
std::move(backend_));
}
persistent_cache::MockBackend* GetBackend() {
// Can't be called without a cache.
CHECK(cache_);
return static_cast<persistent_cache::MockBackend*>(
cache_->GetBackendForTesting());
}
persistent_cache::BackendParams params_;
std::unique_ptr<persistent_cache::MockBackend> backend_;
std::unique_ptr<persistent_cache::PersistentCache> cache_;
};
} // namespace
namespace persistent_cache {
TEST_F(PersistentCacheMockedBackendTest, CreatingCacheInitializesBackend) {
EXPECT_CALL(*backend_, Initialize()).WillOnce(Return(true));
std::unique_ptr<PersistentCache> cache =
std::make_unique<PersistentCache>(std::move(backend_));
EXPECT_NE(cache->GetBackendForTesting(), nullptr);
}
TEST_F(PersistentCacheMockedBackendTest, CacheFindCallsBackendFind) {
CreateCache(true);
EXPECT_CALL(*GetBackend(), Find(kKey)).WillOnce(Return(base::ok(nullptr)));
EXPECT_THAT(cache_->Find(kKey), base::test::ValueIs(testing::IsNull()));
}
TEST_F(PersistentCacheMockedBackendTest, FindReturnsBackendError) {
CreateCache(true);
EXPECT_CALL(*GetBackend(), Find(kKey))
.WillOnce(Return(base::unexpected(TransactionError::kTransient)));
EXPECT_THAT(cache_->Find(kKey),
base::test::ErrorIs(TransactionError::kTransient));
}
TEST_F(PersistentCacheMockedBackendTest, InsertReturnsBackendError) {
CreateCache(true);
EXPECT_CALL(*GetBackend(), Insert(_, _, _))
.WillOnce(Return(base::unexpected(TransactionError::kTransient)));
EXPECT_THAT(cache_->Insert(kKey, base::byte_span_from_cstring("1")),
base::test::ErrorIs(TransactionError::kTransient));
}
TEST_F(PersistentCacheMockedBackendTest, CacheInsertCallsBackendInsert) {
CreateCache(true);
EXPECT_CALL(*GetBackend(), Insert(kKey, _, _));
EXPECT_THAT(cache_->Insert(kKey, base::byte_span_from_cstring("1")),
base::test::HasValue());
}
TEST_F(PersistentCacheMockedBackendTest,
FailedBackendInitializationMeansNoFurtherCalls) {
EXPECT_CALL(*backend_, Insert(kKey, _, _)).Times(0);
EXPECT_CALL(*backend_, Find(kKey)).Times(0);
CreateCache(false);
EXPECT_THAT(cache_->Insert(kKey, base::byte_span_from_cstring("1")),
base::test::ErrorIs(TransactionError::kPermanent));
EXPECT_THAT(cache_->Find(kKey),
base::test::ErrorIs(TransactionError::kPermanent));
}
#if !BUILDFLAG(IS_FUCHSIA)
class PersistentCacheTest : public testing::Test,
public testing::WithParamInterface<BackendType> {
protected:
// Used to creates a new cache independent from any other.
std::unique_ptr<PersistentCache> OpenCache() {
auto backend = params_provider_.CreateBackendWithFiles(GetParam());
if (!backend) {
ADD_FAILURE() << "Failed to create backend";
return nullptr;
}
return std::make_unique<PersistentCache>(std::move(backend));
}
// Used to create a new cache with provided params. Use with params copied
// from the creation of another cache to share backing files between the two.
std::unique_ptr<PersistentCache> OpenCache(BackendParams backend_params) {
auto cache = PersistentCache::Open(std::move(backend_params));
CHECK(cache->GetBackendForTesting());
return cache;
}
test_support::TestHelper params_provider_;
};
TEST_P(PersistentCacheTest, FindReturnsNullWhenEmpty) {
auto cache = OpenCache();
EXPECT_THAT(cache->Find(kKey), base::test::ValueIs(testing::IsNull()));
}
TEST_P(PersistentCacheTest, FindReturnsValueWhenPresent) {
auto cache = OpenCache();
for (int i = 0; i < 20; ++i) {
std::string key = base::NumberToString(i);
auto value = base::as_byte_span(key);
EXPECT_THAT(cache->Find(key), base::test::ValueIs(testing::IsNull()));
EXPECT_THAT(cache->Insert(key, value), base::test::HasValue());
ASSERT_THAT(cache->Find(key), base::test::ValueIs(HasContents(value)));
}
}
TEST_P(PersistentCacheTest, EmptyValueIsStorable) {
auto cache = OpenCache();
EXPECT_THAT(cache->Insert(kKey, base::byte_span_from_cstring("")),
base::test::HasValue());
ASSERT_THAT(
cache->Find(kKey),
base::test::ValueIs(HasContents(base::span<const std::uint8_t>{})));
}
TEST_P(PersistentCacheTest, ValueContainingNullCharIsStorable) {
auto cache = OpenCache();
constexpr std::array<std::uint8_t, 5> value_array{'\0', 'a', 'b', 'c', '\0'};
const base::span<const std::uint8_t> value_span(value_array);
CHECK_EQ(value_span.size(), value_array.size())
<< "All characters must be included in span";
EXPECT_THAT(cache->Insert(kKey, value_span), base::test::HasValue());
ASSERT_THAT(cache->Find(kKey), base::test::ValueIs(HasContents(value_span)));
}
TEST_P(PersistentCacheTest, ValueContainingInvalidUtf8IsStorable) {
auto cache = OpenCache();
constexpr std::array<std::uint8_t, 4> value_array{0x20, 0x0F, 0xFF, 0xFF};
const base::span<const std::uint8_t> value_span(value_array);
CHECK(
!base::IsStringUTF8(std::string(value_array.begin(), value_array.end())))
<< "Test needs invalid utf8";
EXPECT_THAT(cache->Insert(kKey, value_span), base::test::HasValue());
ASSERT_THAT(cache->Find(kKey), base::test::ValueIs(HasContents(value_span)));
}
TEST_P(PersistentCacheTest, OverwritingChangesValue) {
auto cache = OpenCache();
EXPECT_THAT(cache->Insert(kKey, base::byte_span_from_cstring("1")),
base::test::HasValue());
EXPECT_THAT(cache->Insert(kKey, base::byte_span_from_cstring("2")),
base::test::HasValue());
ASSERT_THAT(
cache->Find(kKey),
base::test::ValueIs(HasContents(base::byte_span_from_cstring("2"))));
}
TEST_P(PersistentCacheTest, OverwritingChangesValueVaryingSizes) {
auto cache = OpenCache();
EXPECT_THAT(cache->Insert(kKey, base::byte_span_from_cstring("1")),
base::test::HasValue());
EXPECT_THAT(
cache->Insert(kKey, base::as_byte_span(std::string(1024 * 7, 'b'))),
base::test::HasValue());
ASSERT_THAT(cache->Find(kKey),
base::test::ValueIs(
HasContents(base::as_byte_span(std::string(1024 * 7, 'b')))));
}
TEST_P(PersistentCacheTest, MetadataIsRetrievable) {
EntryMetadata metadata{.input_signature =
base::Time::Now().InMillisecondsSinceUnixEpoch()};
auto cache = OpenCache();
EXPECT_THAT(cache->Insert(kKey, base::byte_span_from_cstring("1"), metadata),
base::test::HasValue());
int64_t seconds_since_epoch =
base::Time::Now().InMillisecondsSinceUnixEpoch() / 1000;
ASSERT_OK_AND_ASSIGN(std::unique_ptr<Entry> entry, cache->Find(kKey));
EXPECT_EQ(entry->GetMetadata().input_signature, metadata.input_signature);
EXPECT_GE(entry->GetMetadata().write_timestamp, seconds_since_epoch);
// The test is supposed to time out before it takes this long to insert a
// value.
EXPECT_LE(entry->GetMetadata().write_timestamp, seconds_since_epoch + 30);
}
TEST_P(PersistentCacheTest, OverwritingChangesMetadata) {
EntryMetadata metadata{.input_signature =
base::Time::Now().InMillisecondsSinceUnixEpoch()};
auto cache = OpenCache();
EXPECT_THAT(cache->Insert(kKey, base::byte_span_from_cstring("1"), metadata),
base::test::HasValue());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<Entry> entry, cache->Find(kKey));
EXPECT_EQ(entry->GetMetadata().input_signature, metadata.input_signature);
EXPECT_THAT(
cache->Insert(kKey, base::byte_span_from_cstring("1"), EntryMetadata{}),
base::test::HasValue());
ASSERT_OK_AND_ASSIGN(entry, cache->Find(kKey));
EXPECT_EQ(entry->GetMetadata().input_signature, 0);
}
TEST_P(PersistentCacheTest, MultipleEphemeralCachesAreIndependent) {
for (int i = 0; i < 3; ++i) {
auto cache = OpenCache();
// `kKey` never inserted in this cache so not found.
EXPECT_THAT(cache->Find(kKey), base::test::ValueIs(testing::IsNull()));
EXPECT_THAT(cache->Insert(kKey, base::byte_span_from_cstring("1")),
base::test::HasValue());
// `kKey` now present.
EXPECT_THAT(cache->Find(kKey), base::test::HasValue());
}
}
TEST_P(PersistentCacheTest, MultipleLiveCachesAreIndependent) {
std::vector<std::unique_ptr<PersistentCache>> caches;
for (int i = 0; i < 3; ++i) {
caches.push_back(OpenCache());
std::unique_ptr<PersistentCache>& cache = caches.back();
// `kKey` never inserted in this cache so not found.
EXPECT_THAT(cache->Find(kKey), base::test::ValueIs(testing::IsNull()));
EXPECT_THAT(cache->Insert(kKey, base::byte_span_from_cstring("1")),
base::test::HasValue());
// `kKey` now present.
EXPECT_THAT(cache->Find(kKey), base::test::ValueIs(testing::NotNull()));
}
}
TEST_P(PersistentCacheTest, EphemeralCachesSharingParamsShareData) {
std::unique_ptr<Backend> backend =
params_provider_.CreateBackendWithFiles(GetParam());
ASSERT_TRUE(backend);
for (int i = 0; i < 3; ++i) {
ASSERT_OK_AND_ASSIGN(auto params, backend->ExportReadWriteParams());
auto cache = OpenCache(std::move(params));
// First run, setup.
if (i == 0) {
// `kKey` never inserted so not found.
EXPECT_THAT(cache->Find(kKey), base::test::ValueIs(testing::IsNull()));
EXPECT_THAT(cache->Insert(kKey, base::byte_span_from_cstring("1")),
base::test::HasValue());
// `kKey` now present.
EXPECT_THAT(cache->Find(kKey), base::test::ValueIs(testing::NotNull()));
} else {
// `kKey` is present because data is shared.
EXPECT_THAT(cache->Find(kKey), base::test::ValueIs(testing::NotNull()));
}
}
}
TEST_P(PersistentCacheTest, LiveCachesSharingParamsShareData) {
std::unique_ptr<Backend> backend =
params_provider_.CreateBackendWithFiles(GetParam());
std::vector<std::unique_ptr<PersistentCache>> caches;
for (int i = 0; i < 3; ++i) {
ASSERT_OK_AND_ASSIGN(auto params, backend->ExportReadWriteParams());
caches.push_back(OpenCache(std::move(params)));
std::unique_ptr<PersistentCache>& cache = caches.back();
// First run, setup.
if (i == 0) {
// `kKey` never inserted so not found.
EXPECT_THAT(cache->Find(kKey), base::test::ValueIs(testing::IsNull()));
EXPECT_THAT(cache->Insert(kKey, base::byte_span_from_cstring("1")),
base::test::HasValue());
// `kKey` now present.
EXPECT_THAT(cache->Find(kKey), base::test::ValueIs(testing::NotNull()));
} else {
EXPECT_THAT(cache->Find(kKey), base::test::ValueIs(testing::NotNull()));
}
}
}
// Create an instance and share it for read-only access to others.
TEST_P(PersistentCacheTest, MultipleInstancesShareData) {
// The main read-write instance.
auto main_cache = OpenCache();
std::vector<std::unique_ptr<PersistentCache>> caches;
for (int i = 0; i < 3; ++i) {
// Export a read-only view to the main instance.
ASSERT_OK_AND_ASSIGN(auto params,
main_cache->ExportReadOnlyBackendParams());
// Create a new instance that will read from the original.
caches.push_back(OpenCache(std::move(params)));
std::unique_ptr<PersistentCache>& ro_cache = caches.back();
if (i == 0) {
// The db is empty when the first client connects.
EXPECT_THAT(ro_cache->Find(kKey), base::test::ValueIs(testing::IsNull()));
// Insert a value via the read-write instance.
EXPECT_THAT(main_cache->Insert(kKey, base::byte_span_from_cstring("1")),
base::test::HasValue());
// It should be there.
EXPECT_THAT(ro_cache->Find(kKey),
base::test::ValueIs(testing::NotNull()));
}
// The new read-only client should see the value that was previously
// inserted.
EXPECT_THAT(ro_cache->Find(kKey), base::test::ValueIs(testing::NotNull()));
}
}
// Create an instance and share it for read-write access to others.
TEST_P(PersistentCacheTest, MultipleInstancesCanWriteData) {
static constexpr char kThisKeyPrefix[] = "thiskey-";
static constexpr char kOtherKeyPrefix[] = "otherkey-";
// The main read-write instance.
auto main_cache = OpenCache();
std::vector<std::unique_ptr<PersistentCache>> caches;
for (int i = 0; i < 3; ++i) {
// Export a read-only view to the main instance.
ASSERT_OK_AND_ASSIGN(auto params,
main_cache->ExportReadWriteBackendParams());
// Create a new instance that will read/write from/to the original.
caches.push_back(OpenCache(std::move(params)));
std::unique_ptr<PersistentCache>& rw_cache = caches.back();
// This new cache has access to all previous values.
for (int j = 0; j < i; ++j) {
std::string value = base::NumberToString(j);
EXPECT_THAT(rw_cache->Find(base::StrCat({kThisKeyPrefix, value})),
base::test::ValueIs(testing::NotNull()));
EXPECT_THAT(rw_cache->Find(base::StrCat({kOtherKeyPrefix, value})),
base::test::ValueIs(testing::NotNull()));
}
// A new value added from the original is seen here.
std::string value = base::NumberToString(i);
std::string other_key = base::StrCat({kOtherKeyPrefix, value});
EXPECT_THAT(main_cache->Find(other_key),
base::test::ValueIs(testing::IsNull()));
EXPECT_THAT(rw_cache->Find(other_key),
base::test::ValueIs(testing::IsNull()));
EXPECT_THAT(main_cache->Insert(other_key, base::as_byte_span(value)),
base::test::HasValue());
EXPECT_THAT(main_cache->Find(other_key),
base::test::ValueIs(testing::NotNull()));
EXPECT_THAT(rw_cache->Find(other_key),
base::test::ValueIs(testing::NotNull()));
// A new value added here is seen in the original.
std::string this_key = base::StrCat({kThisKeyPrefix, value});
EXPECT_THAT(main_cache->Find(this_key),
base::test::ValueIs(testing::IsNull()));
EXPECT_THAT(rw_cache->Find(this_key),
base::test::ValueIs(testing::IsNull()));
EXPECT_THAT(rw_cache->Insert(this_key, base::as_byte_span(value)),
base::test::HasValue());
EXPECT_THAT(main_cache->Find(this_key),
base::test::ValueIs(testing::NotNull()));
EXPECT_THAT(rw_cache->Find(this_key),
base::test::ValueIs(testing::NotNull()));
}
}
TEST_P(PersistentCacheTest, ThreadSafeAccess) {
base::test::TaskEnvironment env;
// Create the cache and insert on this sequence.
auto value = base::byte_span_from_cstring("1");
auto cache = OpenCache();
EXPECT_THAT(cache->Insert(kKey, value), base::test::HasValue());
// Find() on ThreadPool. Result should be expected and there are no sequence
// checkers tripped.
base::test::TestFuture<
base::expected<std::unique_ptr<Entry>, TransactionError>>
future_entry;
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock()},
base::BindOnce(
[](PersistentCache* cache,
base::OnceCallback<void(
base::expected<std::unique_ptr<Entry>, TransactionError>)>
on_entry) {
auto entry = cache->Find(kKey);
std::move(on_entry).Run(std::move(entry));
},
cache.get(), future_entry.GetSequenceBoundCallback()));
// Wait for result availability and check.
ASSERT_OK_AND_ASSIGN(std::unique_ptr<Entry> entry, future_entry.Take());
ASSERT_NE(entry, nullptr);
EXPECT_EQ(entry->GetContentSpan(), value);
}
TEST_P(PersistentCacheTest, MultipleLiveEntries) {
auto cache = OpenCache();
absl::flat_hash_map<std::string, std::unique_ptr<Entry>> entries;
for (size_t i = 0; i < 20; ++i) {
std::string key = base::NumberToString(i);
auto value = base::as_byte_span(key);
EXPECT_THAT(cache->Insert(key, value), base::test::HasValue());
// Create an entry where the value is equal to the key.
ASSERT_OK_AND_ASSIGN(entries[key], cache->Find(key));
}
// Verify that entries have the expected content.
for (auto& [key, entry] : entries) {
ASSERT_NE(entry, nullptr);
ASSERT_EQ(entry->GetContentSpan(), base::as_byte_span(key));
}
}
TEST_P(PersistentCacheTest, MultipleLiveEntriesWithVaryingLifetime) {
static constexpr size_t kNumberOfEntries = 40;
auto cache = OpenCache();
absl::flat_hash_map<std::string, std::unique_ptr<Entry>> entries;
for (size_t i = 0; i < kNumberOfEntries; ++i) {
std::string key = base::NumberToString(i);
auto value = base::as_byte_span(key);
EXPECT_THAT(cache->Insert(key, value), base::test::HasValue());
// Create an entry where the value is equal to the key.
ASSERT_OK_AND_ASSIGN(entries[key], cache->Find(key));
// Every other iteration delete an entry that came before.
if (i && i % 2 == 0) {
entries.erase(base::NumberToString(i / 2));
}
}
// Assert that some entries remain to be verified in the next loop.
ASSERT_GE(entries.size(), kNumberOfEntries / 2);
// Verify that entries have the expected content.
for (auto& [key, entry] : entries) {
ASSERT_NE(entry, nullptr);
ASSERT_EQ(entry->GetContentSpan(), base::as_byte_span(key));
}
}
TEST_P(PersistentCacheTest, AbandonementDetected) {
auto cache = OpenCache();
// Value is correctly inserted.
EXPECT_THAT(
cache->Insert(kKey, base::byte_span_from_cstring("1"), EntryMetadata{}),
base::test::HasValue());
ASSERT_OK_AND_ASSIGN(auto entry, cache->Find(kKey));
EXPECT_NE(entry, nullptr);
// Abandon cache, no further operations will succeed.
cache->Abandon();
// Calling Find() is no longer successful.
EXPECT_THAT(cache->Find(kKey),
base::test::ErrorIs(TransactionError::kConnectionError));
// Calling Insert() is no longer successful.
EXPECT_THAT(
cache->Insert(kKey, base::byte_span_from_cstring("1"), EntryMetadata{}),
base::test::ErrorIs(TransactionError::kConnectionError));
}
TEST_P(PersistentCacheTest, RecoveryFromTransientError) {
auto cache = OpenCache();
ASSERT_OK_AND_ASSIGN(auto reader_params,
cache->ExportReadOnlyBackendParams());
// Baseline insert works.
EXPECT_THAT(
cache->Insert(kKey, base::byte_span_from_cstring("1"), EntryMetadata{}),
base::test::HasValue());
// Lock the db file in shared mode.
auto reader_vfs_file_set =
SqliteBackendImpl::GetVfsFileSetFromParams(std::move(reader_params));
auto reader_files = reader_vfs_file_set.GetFiles();
auto reader_db_file = reader_files[0].second;
ASSERT_EQ(reader_db_file->Lock(SQLITE_LOCK_SHARED), SQLITE_OK);
// Held lock causes transient error.
EXPECT_THAT(
cache->Insert(kKey, base::byte_span_from_cstring("1"), EntryMetadata{}),
base::test::ErrorIs(TransactionError::kTransient));
// Unlock works.
ASSERT_EQ(reader_db_file->Unlock(SQLITE_LOCK_NONE), SQLITE_OK);
ASSERT_EQ(reader_db_file->LockModeForTesting(), SQLITE_LOCK_NONE);
// Insert now succeeds.
EXPECT_THAT(
cache->Insert(kKey, base::byte_span_from_cstring("1"), EntryMetadata{}),
base::test::HasValue());
}
INSTANTIATE_TEST_SUITE_P(All,
PersistentCacheTest,
testing::Values(BackendType::kSqlite));
#endif
} // namespace persistent_cache