blob: d61b65bbee15a29eb1216ad9b700d2907ea7eaba [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/byte_size.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/scoped_refptr.h"
#include "base/message_loop/message_pump_type.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/single_thread_task_executor.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/mock_callback.h"
#include "base/test/multiprocess_test.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/threading/platform_thread.h"
#include "base/threading/thread.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "components/sqlite_vfs/client.h"
#include "components/sqlite_vfs/constants.h"
#include "components/sqlite_vfs/multiprocess_test.test-mojom.h"
#include "components/sqlite_vfs/sqlite_database_vfs_file_set.h"
#include "components/sqlite_vfs/sqlite_sandboxed_vfs.h"
#include "components/sqlite_vfs/vfs_utils.h"
#include "mojo/core/embedder/embedder.h"
#include "mojo/core/embedder/scoped_ipc_support.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "mojo/public/cpp/platform/platform_channel.h"
#include "mojo/public/cpp/system/invitation.h"
#include "sql/database.h"
#include "sql/sqlite_result_code_values.h"
#include "sql/statement.h"
#include "sql/transaction.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/multiprocess_func_list.h"
namespace sqlite_vfs {
namespace {
using ::base::test::ErrorIs;
using ::base::test::ValueIs;
using ::testing::_;
using ::testing::AnyNumber;
using ::testing::Bool;
using ::testing::Combine;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::IsEmpty;
using ::testing::Ne;
using ::testing::UnorderedElementsAre;
constexpr base::FilePath::CharType kBaseName[] = FILE_PATH_LITERAL("test_db");
std::optional<SqliteVfsFileSet> CreateAndBindFileSet(
const base::FilePath& directory,
const base::FilePath& base_name,
bool journal_mode_wal) {
auto pending_file_set =
MakePendingFileSet(Client::kTest, directory, base_name,
/*single_connection=*/false, journal_mode_wal);
if (!pending_file_set.has_value()) {
return std::nullopt;
}
return SqliteVfsFileSet::Bind(Client::kTest, *std::move(pending_file_set));
}
sql::DatabaseOptions MakeDatabaseOptionsForFileSet(
const SqliteVfsFileSet& file_set) {
return sql::DatabaseOptions()
.set_exclusive_locking(file_set.is_single_connection())
.set_wal_mode(file_set.wal_mode())
.set_vfs_name_discouraged(SqliteSandboxedVfsDelegate::kSqliteVfsName)
.set_mmap_enabled(false)
.set_read_only(file_set.read_only());
}
class ReadOnlyConnectionImpl : public sqlite_vfs::mojom::ReadOnlyConnection {
public:
explicit ReadOnlyConnectionImpl(sqlite_vfs::PendingFileSet file_set)
: file_set_(SqliteVfsFileSet::Bind(Client::kTest, std::move(file_set))),
unregister_runner_(
SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles(
*file_set_)),
db_(MakeDatabaseOptionsForFileSet(*file_set_),
sql::Database::Tag("Test")) {
db_.set_error_callback(base::BindRepeating(
[](std::vector<int32_t>* errors, int error, sql::Statement* stmt) {
errors->push_back(error);
},
base::Unretained(&reported_errors_)));
is_open_ = db_.Open(file_set_->GetDbVirtualFilePath());
}
bool is_open() const { return is_open_; }
void Read(ReadCallback callback) override {
sql::Statement stm(db_.GetUniqueStatement(
"SELECT val FROM test ORDER BY rowid DESC LIMIT 1"));
if (stm.Step()) {
std::move(callback).Run(base::ok(stm.ColumnString(0)));
} else if (stm.Succeeded()) {
std::move(callback).Run(
base::unexpected(static_cast<int>(sql::SqliteResultCode::kDone)));
} else {
std::move(callback).Run(base::unexpected(db_.GetErrorCode()));
}
}
void RunReadSequence(int32_t count,
RunReadSequenceCallback callback) override {
int success_count = 0;
sql::Statement stm(db_.GetUniqueStatement(
"SELECT val FROM test ORDER BY rowid DESC LIMIT 1"));
for (int i = 0; i < count; ++i) {
if (stm.Step()) {
success_count++;
}
stm.Reset(true);
base::PlatformThread::Sleep(base::Milliseconds(1));
}
std::move(callback).Run(success_count);
}
void CloseDatabase(CloseDatabaseCallback callback) override {
db_.Close();
is_open_ = false;
std::move(callback).Run(reported_errors_);
}
private:
std::optional<SqliteVfsFileSet> file_set_;
SqliteSandboxedVfsDelegate::UnregisterRunner unregister_runner_;
std::vector<int32_t> reported_errors_;
sql::Database db_;
bool is_open_ = false;
};
class ReadWriteConnectionImpl : public sqlite_vfs::mojom::ReadWriteConnection {
public:
explicit ReadWriteConnectionImpl(sqlite_vfs::PendingFileSet file_set)
: file_set_(SqliteVfsFileSet::Bind(Client::kTest, std::move(file_set))),
unregister_runner_(
SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles(
*file_set_)),
db_(MakeDatabaseOptionsForFileSet(*file_set_),
sql::Database::Tag("Test")) {
db_.set_error_callback(base::BindRepeating(
[](std::vector<int32_t>* errors, int error, sql::Statement* stmt) {
errors->push_back(error);
},
base::Unretained(&reported_errors_)));
is_open_ = db_.Open(file_set_->GetDbVirtualFilePath());
if (is_open_) {
std::ignore = db_.Execute("CREATE TABLE IF NOT EXISTS test (val TEXT)");
}
}
bool is_open() const { return is_open_; }
void Read(ReadCallback callback) override {
sql::Statement stm(db_.GetUniqueStatement(
"SELECT val FROM test ORDER BY rowid DESC LIMIT 1"));
if (stm.Step()) {
std::move(callback).Run(base::ok(stm.ColumnString(0)));
} else if (stm.Succeeded()) {
std::move(callback).Run(
base::unexpected(static_cast<int>(sql::SqliteResultCode::kDone)));
} else {
std::move(callback).Run(base::unexpected(db_.GetErrorCode()));
}
}
void RunReadSequence(int32_t count,
RunReadSequenceCallback callback) override {
int success_count = 0;
sql::Statement stm(db_.GetUniqueStatement(
"SELECT val FROM test ORDER BY rowid DESC LIMIT 1"));
for (int i = 0; i < count; ++i) {
if (stm.Step()) {
success_count++;
}
stm.Reset(true);
base::PlatformThread::Sleep(base::Milliseconds(1));
}
std::move(callback).Run(success_count);
}
void Write(const std::string& value, WriteCallback callback) override {
bool success = db_.Execute(
base::StrCat({"INSERT INTO test (val) VALUES ('", value, "')"}));
std::move(callback).Run(success);
}
void RunStressTest(int32_t iterations,
RunStressTestCallback callback) override {
int success_count = 0;
for (int i = 0; i < iterations; ++i) {
if (i % 2 == 0) {
sql::Statement stm(db_.GetUniqueStatement(
"SELECT val FROM test ORDER BY rowid DESC LIMIT 1"));
if (stm.Step()) {
success_count++;
}
} else {
sql::Statement stm(
db_.GetUniqueStatement("INSERT INTO test (val) VALUES (?)"));
stm.BindString(0, base::StrCat({"stress_", base::NumberToString(i)}));
if (stm.Run()) {
success_count++;
}
}
base::PlatformThread::Sleep(base::Milliseconds(1));
}
std::move(callback).Run(success_count);
}
void Checkpoint(CheckpointCallback callback) override {
std::move(callback).Run(db_.CheckpointDatabase(/*truncate=*/true));
}
void CloseDatabase(CloseDatabaseCallback callback) override {
db_.Close();
is_open_ = false;
std::move(callback).Run(reported_errors_);
}
private:
std::optional<SqliteVfsFileSet> file_set_;
SqliteSandboxedVfsDelegate::UnregisterRunner unregister_runner_;
std::vector<int32_t> reported_errors_;
sql::Database db_;
bool is_open_ = false;
};
class SqliteVfsMultiprocessTestHelperImpl
: public sqlite_vfs::mojom::SqliteVfsMultiprocessTestHelper {
public:
explicit SqliteVfsMultiprocessTestHelperImpl(base::OnceClosure quit_closure)
: quit_closure_(std::move(quit_closure)) {}
~SqliteVfsMultiprocessTestHelperImpl() override {
if (quit_closure_) {
std::move(quit_closure_).Run();
}
}
void OpenDatabase(sqlite_vfs::PendingFileSet file_set,
OpenDatabaseCallback callback) override {
auto impl = std::make_unique<ReadWriteConnectionImpl>(std::move(file_set));
if (!impl->is_open()) {
std::move(callback).Run(mojo::NullRemote());
return;
}
mojo::PendingRemote<sqlite_vfs::mojom::ReadWriteConnection> remote;
mojo::MakeSelfOwnedReceiver(std::move(impl),
remote.InitWithNewPipeAndPassReceiver());
std::move(callback).Run(std::move(remote));
}
void OpenDatabaseReadOnly(sqlite_vfs::PendingFileSet file_set,
OpenDatabaseReadOnlyCallback callback) override {
auto impl = std::make_unique<ReadOnlyConnectionImpl>(std::move(file_set));
if (!impl->is_open()) {
std::move(callback).Run(mojo::NullRemote());
return;
}
mojo::PendingRemote<sqlite_vfs::mojom::ReadOnlyConnection> remote;
mojo::MakeSelfOwnedReceiver(std::move(impl),
remote.InitWithNewPipeAndPassReceiver());
std::move(callback).Run(std::move(remote));
}
private:
base::OnceClosure quit_closure_;
};
} // namespace
MULTIPROCESS_TEST_MAIN(SqliteVfsChild) {
base::CommandLine::Init(/*argc=*/0, /*argv=*/nullptr);
auto& command_line = *base::CommandLine::ForCurrentProcess();
mojo::core::Init();
base::Thread ipc_thread("ipc");
ipc_thread.StartWithOptions(
base::Thread::Options(base::MessagePumpType::IO, 0));
mojo::core::ScopedIPCSupport ipc_support(
ipc_thread.task_runner(),
mojo::core::ScopedIPCSupport::ShutdownPolicy::FAST);
mojo::IncomingInvitation invitation = mojo::IncomingInvitation::Accept(
mojo::PlatformChannel::RecoverPassedEndpointFromCommandLine(
command_line));
mojo::ScopedMessagePipeHandle pipe =
invitation.ExtractMessagePipe(/*name=*/0);
base::SingleThreadTaskExecutor task_executor(base::MessagePumpType::IO,
/*is_main_thread=*/true);
base::RunLoop run_loop;
mojo::MakeSelfOwnedReceiver(
std::make_unique<SqliteVfsMultiprocessTestHelperImpl>(
run_loop.QuitClosure()),
mojo::PendingReceiver<sqlite_vfs::mojom::SqliteVfsMultiprocessTestHelper>(
std::move(pipe)));
run_loop.Run();
return 0;
}
// A helper to install a `MockCallback` as a database's error callback.
// Expectations may be set on an instance's `Run(int, sql::Statement*)` method.
class ScopedMockErrorCallback
: public base::MockCallback<sql::Database::ErrorCallback> {
public:
explicit ScopedMockErrorCallback(sql::Database& db) : db_(db) {
db_->set_error_callback(Get());
}
~ScopedMockErrorCallback() { db_->reset_error_callback(); }
private:
const raw_ref<sql::Database> db_;
};
class SqliteVfsMultiprocessTestBase : public ::testing::Test {
protected:
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
ipc_support_ = std::make_unique<mojo::core::ScopedIPCSupport>(
base::SingleThreadTaskRunner::GetCurrentDefault(),
mojo::core::ScopedIPCSupport::ShutdownPolicy::FAST);
}
base::FilePath file_set_directory() const { return temp_dir_.GetPath(); }
void TerminateLastChild() {
ASSERT_FALSE(child_processes_.empty());
base::Process& child_process = child_processes_.back();
ASSERT_TRUE(child_process.IsValid());
ASSERT_TRUE(child_process.Terminate(0, true));
child_processes_.pop_back();
}
mojo::Remote<sqlite_vfs::mojom::SqliteVfsMultiprocessTestHelper> SpawnChild(
const std::string& name) {
base::CommandLine child_command_line =
base::GetMultiProcessTestChildBaseCommandLine();
mojo::PlatformChannel channel;
mojo::OutgoingInvitation invitation;
mojo::ScopedMessagePipeHandle pipe =
invitation.AttachMessagePipe(/*name=*/0);
base::LaunchOptions launch_options;
channel.PrepareToPassRemoteEndpoint(&launch_options, &child_command_line);
base::Process process = base::SpawnMultiProcessTestChild(
name, child_command_line, launch_options);
EXPECT_TRUE(process.IsValid());
mojo::OutgoingInvitation::Send(std::move(invitation), process.Handle(),
channel.TakeLocalEndpoint());
mojo::Remote<sqlite_vfs::mojom::SqliteVfsMultiprocessTestHelper> remote;
remote.Bind(
mojo::PendingRemote<sqlite_vfs::mojom::SqliteVfsMultiprocessTestHelper>(
std::move(pipe), 0));
child_processes_.push_back(std::move(process));
return remote;
}
mojo::Remote<sqlite_vfs::mojom::ReadWriteConnection> OpenDatabase(
mojo::Remote<sqlite_vfs::mojom::SqliteVfsMultiprocessTestHelper>& child,
sqlite_vfs::PendingFileSet file_set) {
base::test::TestFuture<
mojo::PendingRemote<sqlite_vfs::mojom::ReadWriteConnection>>
future;
child->OpenDatabase(std::move(file_set), future.GetCallback());
auto remote = future.Take();
EXPECT_TRUE(remote.is_valid());
return mojo::Remote<sqlite_vfs::mojom::ReadWriteConnection>(
std::move(remote));
}
mojo::Remote<sqlite_vfs::mojom::ReadOnlyConnection> OpenDatabaseReadOnly(
mojo::Remote<sqlite_vfs::mojom::SqliteVfsMultiprocessTestHelper>& child,
sqlite_vfs::PendingFileSet file_set) {
base::test::TestFuture<
mojo::PendingRemote<sqlite_vfs::mojom::ReadOnlyConnection>>
future;
child->OpenDatabaseReadOnly(std::move(file_set), future.GetCallback());
auto remote = future.Take();
EXPECT_TRUE(remote.is_valid());
return mojo::Remote<sqlite_vfs::mojom::ReadOnlyConnection>(
std::move(remote));
}
base::ScopedTempDir temp_dir_;
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::MainThreadType::IO};
std::unique_ptr<mojo::core::ScopedIPCSupport> ipc_support_;
std::vector<base::Process> child_processes_;
};
class SqliteVfsMultiprocessTest
: public SqliteVfsMultiprocessTestBase,
public ::testing::WithParamInterface<bool /* journal_mode_wal */> {
protected:
static bool journal_mode_wal() { return GetParam(); }
};
TEST_P(SqliteVfsMultiprocessTest, ReaderBlocksWriterInRollback) {
if (journal_mode_wal()) {
GTEST_SKIP() << "This test is only for rollback journal mode";
}
ASSERT_OK_AND_ASSIGN(
auto file_set,
CreateAndBindFileSet(file_set_directory(), base::FilePath(kBaseName),
/*journal_mode_wal=*/false));
auto unregister_runner =
SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles(
file_set);
{
sql::Database db(MakeDatabaseOptionsForFileSet(file_set),
sql::Database::Tag("Test"));
::testing::StrictMock<ScopedMockErrorCallback> error_mock(db);
ASSERT_TRUE(db.Open(file_set.GetDbVirtualFilePath()));
// Create table.
ASSERT_TRUE(db.Execute("CREATE TABLE test (val TEXT)"));
ASSERT_TRUE(db.Execute("INSERT INTO test (val) VALUES ('initial')"));
}
// Spawn child.
mojo::Remote<sqlite_vfs::mojom::SqliteVfsMultiprocessTestHelper> child =
SpawnChild("SqliteVfsChild");
// Pass file set to child and open database.
ASSERT_OK_AND_ASSIGN(
auto pending_file_set,
ShareConnection(file_set_directory(), base::FilePath(kBaseName), file_set,
/*read_write=*/true));
mojo::Remote<sqlite_vfs::mojom::ReadWriteConnection> connection =
OpenDatabase(child, std::move(pending_file_set));
ASSERT_TRUE(connection.is_bound());
sql::Database db(MakeDatabaseOptionsForFileSet(file_set),
sql::Database::Tag("Test"));
::testing::StrictMock<ScopedMockErrorCallback> error_mock(db);
ASSERT_TRUE(db.Open(file_set.GetDbVirtualFilePath()));
sql::Transaction transaction(&db);
ASSERT_TRUE(transaction.Begin());
// Read to acquire shared lock.
{
sql::Statement stm(db.GetUniqueStatement("SELECT val FROM test"));
ASSERT_TRUE(stm.Step());
ASSERT_EQ(stm.ColumnString(0), "initial");
// Call Write on child. It should fail because we hold SHARED lock.
base::test::TestFuture<bool> future;
connection->Write("child_value", future.GetCallback());
bool success = future.Get();
EXPECT_FALSE(success);
} // stm is destroyed here!
// Commit transaction.
ASSERT_TRUE(transaction.Commit());
// Close DB in parent to release all locks!
db.Close();
// Call Write on child again. It should succeed now.
{
base::test::TestFuture<bool> future;
connection->Write("child_value", future.GetCallback());
bool success = future.Get();
EXPECT_TRUE(success);
}
}
TEST_P(SqliteVfsMultiprocessTest, WriterBlocksReaderInRollback) {
if (journal_mode_wal()) {
GTEST_SKIP() << "This test is only for rollback journal mode";
}
ASSERT_OK_AND_ASSIGN(
auto file_set,
CreateAndBindFileSet(file_set_directory(), base::FilePath(kBaseName),
/*journal_mode_wal=*/false));
auto unregister_runner =
SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles(
file_set);
{
sql::Database db(MakeDatabaseOptionsForFileSet(file_set),
sql::Database::Tag("Test"));
::testing::StrictMock<ScopedMockErrorCallback> error_mock(db);
ASSERT_TRUE(db.Open(file_set.GetDbVirtualFilePath()));
// Create table.
ASSERT_TRUE(db.Execute("CREATE TABLE test (val TEXT)"));
}
// Spawn child.
mojo::Remote<sqlite_vfs::mojom::SqliteVfsMultiprocessTestHelper> child =
SpawnChild("SqliteVfsChild");
// Pass file set to child and open database.
ASSERT_OK_AND_ASSIGN(
auto pending_file_set,
ShareConnection(file_set_directory(), base::FilePath(kBaseName), file_set,
/*read_write=*/false));
mojo::Remote<sqlite_vfs::mojom::ReadOnlyConnection> connection =
OpenDatabaseReadOnly(child, std::move(pending_file_set));
ASSERT_TRUE(connection.is_bound());
// Re-open DB in parent.
sql::Database db(MakeDatabaseOptionsForFileSet(file_set),
sql::Database::Tag("Test"));
::testing::StrictMock<ScopedMockErrorCallback> error_mock(db);
ASSERT_TRUE(db.Open(file_set.GetDbVirtualFilePath()));
sql::Transaction transaction(&db);
ASSERT_TRUE(transaction.Begin());
ASSERT_TRUE(db.Execute("INSERT INTO test (val) VALUES ('parent_value')"));
// Call Read on child. It should succeed but return nullopt (or not
// 'parent_value').
{
base::test::TestFuture<base::expected<std::string, int32_t>> future;
connection->Read(future.GetCallback());
EXPECT_THAT(future.Take(),
ErrorIs(static_cast<int>(sql::SqliteResultCode::kDone)));
}
// Commit transaction.
ASSERT_TRUE(transaction.Commit());
// Close DB in parent to release all locks!
db.Close();
// Call Read on child again. It should see the new value now.
{
base::test::TestFuture<base::expected<std::string, int32_t>> future;
connection->Read(future.GetCallback());
EXPECT_THAT(future.Take(), ValueIs(std::string("parent_value")));
}
}
TEST_P(SqliteVfsMultiprocessTest, ReaderDoesNotBlockWriterInWal) {
if (!journal_mode_wal()) {
GTEST_SKIP() << "This test is only for WAL mode";
}
ASSERT_OK_AND_ASSIGN(
auto file_set,
CreateAndBindFileSet(file_set_directory(), base::FilePath(kBaseName),
/*journal_mode_wal=*/true));
auto unregister_runner =
SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles(
file_set);
{
sql::Database db(MakeDatabaseOptionsForFileSet(file_set),
sql::Database::Tag("Test"));
::testing::StrictMock<ScopedMockErrorCallback> error_mock(db);
ASSERT_TRUE(db.Open(file_set.GetDbVirtualFilePath()));
// Create table.
ASSERT_TRUE(db.Execute("CREATE TABLE test (val TEXT)"));
ASSERT_TRUE(db.Execute("INSERT INTO test (val) VALUES ('initial')"));
}
// Spawn child.
mojo::Remote<sqlite_vfs::mojom::SqliteVfsMultiprocessTestHelper> child =
SpawnChild("SqliteVfsChild");
// Pass file set to child and open database.
ASSERT_OK_AND_ASSIGN(
auto pending_file_set,
ShareConnection(file_set_directory(), base::FilePath(kBaseName), file_set,
/*read_write=*/true));
mojo::Remote<sqlite_vfs::mojom::ReadWriteConnection> connection =
OpenDatabase(child, std::move(pending_file_set));
ASSERT_TRUE(connection.is_bound());
// Re-open DB in parent.
sql::Database db(MakeDatabaseOptionsForFileSet(file_set),
sql::Database::Tag("Test"));
::testing::StrictMock<ScopedMockErrorCallback> error_mock(db);
ASSERT_TRUE(db.Open(file_set.GetDbVirtualFilePath()));
sql::Transaction transaction(&db);
ASSERT_TRUE(transaction.Begin());
// Read to acquire shared lock (snapshot).
{
sql::Statement stm(db.GetUniqueStatement("SELECT val FROM test"));
ASSERT_TRUE(stm.Step());
ASSERT_EQ(stm.ColumnString(0), "initial");
}
// Call Write on child. It should SUCCEED in WAL mode!
{
base::test::TestFuture<bool> future;
connection->Write("child_value", future.GetCallback());
bool success = future.Get();
EXPECT_TRUE(success);
}
// Read again (in same transaction) and should STILL see "initial"!
{
sql::Statement stm2(db.GetUniqueStatement("SELECT val FROM test"));
ASSERT_TRUE(stm2.Step());
ASSERT_EQ(stm2.ColumnString(0), "initial");
}
// Commit transaction.
ASSERT_TRUE(transaction.Commit());
// Close DB in parent to release all locks!
db.Close();
// Force checkpoint from child now that parent has closed its connection!
{
base::test::TestFuture<bool> future;
connection->Checkpoint(future.GetCallback());
bool success = future.Get();
ASSERT_TRUE(success);
}
base::test::TestFuture<std::vector<int32_t>> future;
connection->CloseDatabase(base::BindOnce(
[](base::OnceCallback<void(std::vector<int32_t>)> cb,
const std::vector<int32_t>& errors) { std::move(cb).Run(errors); },
future.GetCallback()));
EXPECT_THAT(future.Take(), IsEmpty());
// Re-open DB in parent to see if it picks up the change!
ASSERT_TRUE(db.Open(file_set.GetDbVirtualFilePath()));
sql::Statement stm3(
db.GetUniqueStatement("SELECT val FROM test ORDER BY rowid DESC"));
ASSERT_TRUE(stm3.Step());
EXPECT_THAT(stm3.ColumnString(0), Eq(std::string("child_value")));
}
TEST_P(SqliteVfsMultiprocessTest, WalReadMarkNecessity) {
if (!journal_mode_wal()) {
GTEST_SKIP() << "This test is only for WAL mode";
}
ASSERT_OK_AND_ASSIGN(
auto file_set,
CreateAndBindFileSet(file_set_directory(), base::FilePath(kBaseName),
/*journal_mode_wal=*/true));
auto unregister_runner =
SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles(
file_set);
// 1. Parent creates DB and inserts initial data.
{
sql::Database db(MakeDatabaseOptionsForFileSet(file_set),
sql::Database::Tag("Test"));
::testing::StrictMock<ScopedMockErrorCallback> error_mock(db);
ASSERT_TRUE(db.Open(file_set.GetDbVirtualFilePath()));
// Create table.
ASSERT_TRUE(db.Execute("CREATE TABLE test (val TEXT)"));
ASSERT_TRUE(db.Execute("INSERT INTO test (val) VALUES ('initial')"));
}
// Spawn child (Reader).
mojo::Remote<sqlite_vfs::mojom::SqliteVfsMultiprocessTestHelper> child =
SpawnChild("SqliteVfsChild");
// Pass READ-ONLY file set to child.
ASSERT_OK_AND_ASSIGN(
auto pending_file_set,
ShareConnection(file_set_directory(), base::FilePath(kBaseName), file_set,
/*read_write=*/false));
mojo::Remote<sqlite_vfs::mojom::ReadOnlyConnection> connection =
OpenDatabaseReadOnly(child, std::move(pending_file_set));
ASSERT_TRUE(connection.is_bound());
// Child should see 'initial'!
{
base::test::TestFuture<base::expected<std::string, int32_t>> future;
connection->Read(future.GetCallback());
EXPECT_THAT(future.Take(), ValueIs(std::string("initial")));
}
// Parent (Writer) opens DB again to write new data!
sql::Database db(MakeDatabaseOptionsForFileSet(file_set),
sql::Database::Tag("Test"));
::testing::StrictMock<ScopedMockErrorCallback> error_mock(db);
ASSERT_TRUE(db.Open(file_set.GetDbVirtualFilePath()));
// Write new data!
ASSERT_TRUE(db.Execute("INSERT INTO test (val) VALUES ('new_data')"));
// Child tries to read WITHOUT UpdateReadMark!
// It should see 'new_data' because the writer likely updated a read-mark
// during its transaction, which the read-only reader can use.
{
base::test::TestFuture<base::expected<std::string, int32_t>> future;
connection->Read(future.GetCallback());
EXPECT_THAT(future.Take(), ValueIs(std::string("new_data")));
}
// Cleanup.
base::test::TestFuture<std::vector<int32_t>> close_future;
connection->CloseDatabase(base::BindOnce(
[](base::OnceCallback<void(std::vector<int32_t>)> cb,
const std::vector<int32_t>& errors) { std::move(cb).Run(errors); },
close_future.GetCallback()));
EXPECT_THAT(close_future.Take(), IsEmpty());
}
TEST_P(SqliteVfsMultiprocessTest, WalSourceTest) {
if (!journal_mode_wal()) {
GTEST_SKIP() << "This test is only for WAL mode";
}
ASSERT_OK_AND_ASSIGN(
auto file_set,
CreateAndBindFileSet(file_set_directory(), base::FilePath(kBaseName),
/*journal_mode_wal=*/true));
auto unregister_runner =
SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles(
file_set);
{
sql::Database db(MakeDatabaseOptionsForFileSet(file_set),
sql::Database::Tag("Test"));
::testing::StrictMock<ScopedMockErrorCallback> error_mock(db);
ASSERT_TRUE(db.Open(file_set.GetDbVirtualFilePath()));
ASSERT_TRUE(db.Execute("CREATE TABLE test (val TEXT)"));
ASSERT_TRUE(db.Execute("INSERT INTO test (val) VALUES ('initial')"));
}
// Spawn child.
mojo::Remote<sqlite_vfs::mojom::SqliteVfsMultiprocessTestHelper> child =
SpawnChild("SqliteVfsChild");
// Pass file set to child and open database.
ASSERT_OK_AND_ASSIGN(
auto pending_file_set,
ShareConnection(file_set_directory(), base::FilePath(kBaseName), file_set,
/*read_write=*/true));
mojo::Remote<sqlite_vfs::mojom::ReadWriteConnection> connection =
OpenDatabase(child, std::move(pending_file_set));
ASSERT_TRUE(connection.is_bound());
// Parent writes more data (goes to WAL).
{
sql::Database db(MakeDatabaseOptionsForFileSet(file_set),
sql::Database::Tag("Test"));
::testing::StrictMock<ScopedMockErrorCallback> error_mock(db);
ASSERT_TRUE(db.Open(file_set.GetDbVirtualFilePath()));
ASSERT_TRUE(db.Execute("INSERT INTO test (val) VALUES ('wal_value')"));
}
// Child reads. It should see 'wal_value' (reading from WAL).
{
base::test::TestFuture<base::expected<std::string, int32_t>> future;
connection->Read(future.GetCallback());
EXPECT_THAT(future.Take(), ValueIs(std::string("wal_value")));
}
// Parent checkpoints.
{
sql::Database db(MakeDatabaseOptionsForFileSet(file_set),
sql::Database::Tag("Test"));
::testing::StrictMock<ScopedMockErrorCallback> error_mock(db);
ASSERT_TRUE(db.Open(file_set.GetDbVirtualFilePath()));
ASSERT_TRUE(db.CheckpointDatabase(/*truncate=*/true));
}
// Child reads again. It should still see 'wal_value' (now reading from main
// DB).
{
base::test::TestFuture<base::expected<std::string, int32_t>> future;
connection->Read(future.GetCallback());
EXPECT_THAT(future.Take(), ValueIs(std::string("wal_value")));
}
}
TEST_P(SqliteVfsMultiprocessTest, ParallelStressTest) {
ASSERT_OK_AND_ASSIGN(
auto file_set,
CreateAndBindFileSet(file_set_directory(), base::FilePath(kBaseName),
journal_mode_wal()));
auto unregister_runner =
SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles(
file_set);
sql::Database db(MakeDatabaseOptionsForFileSet(file_set),
sql::Database::Tag("Test"));
::testing::StrictMock<ScopedMockErrorCallback> error_mock(db);
ASSERT_TRUE(db.Open(file_set.GetDbVirtualFilePath()));
ASSERT_TRUE(db.Execute("CREATE TABLE test (val TEXT)"));
ASSERT_TRUE(db.Execute("INSERT INTO test (val) VALUES ('initial')"));
ASSERT_TRUE(::testing::Mock::VerifyAndClearExpectations(&error_mock));
// Spawn child.
mojo::Remote<sqlite_vfs::mojom::SqliteVfsMultiprocessTestHelper> child =
SpawnChild("SqliteVfsChild");
// Pass file set to child and open database.
ASSERT_OK_AND_ASSIGN(
auto pending_file_set,
ShareConnection(file_set_directory(), base::FilePath(kBaseName), file_set,
/*read_write=*/true));
mojo::Remote<sqlite_vfs::mojom::ReadWriteConnection> connection =
OpenDatabase(child, std::move(pending_file_set));
ASSERT_TRUE(connection.is_bound());
// Run stress test on child in parallel!
base::test::TestFuture<int> child_future;
connection->RunStressTest(100, child_future.GetCallback());
// Run stress test on parent too!
EXPECT_CALL(error_mock, Run(static_cast<int>(sql::SqliteErrorCode::kBusy), _))
.Times(AnyNumber());
int parent_success_count = 0;
for (int i = 0; i < 100; ++i) {
if (i % 2 == 0) {
sql::Statement stm(db.GetUniqueStatement(
"SELECT val FROM test ORDER BY rowid DESC LIMIT 1"));
if (stm.Step()) {
parent_success_count++;
}
} else {
sql::Statement stm(
db.GetUniqueStatement("INSERT INTO test (val) VALUES (?)"));
stm.BindString(0, base::StrCat({"parent_", base::NumberToString(i)}));
if (stm.Run()) {
parent_success_count++;
}
}
base::PlatformThread::Sleep(base::Milliseconds(1));
}
int child_success_count = child_future.Get();
EXPECT_GT(parent_success_count, 0);
EXPECT_GT(child_success_count, 0);
}
TEST_P(SqliteVfsMultiprocessTest, WalDynamicGrowth) {
if (!journal_mode_wal()) {
GTEST_SKIP() << "This test is only for WAL mode";
}
ASSERT_OK_AND_ASSIGN(
auto file_set,
CreateAndBindFileSet(file_set_directory(), base::FilePath(kBaseName),
/*journal_mode_wal=*/true));
auto unregister_runner =
SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles(
file_set);
{
sql::Database db(MakeDatabaseOptionsForFileSet(file_set),
sql::Database::Tag("Test"));
::testing::StrictMock<ScopedMockErrorCallback> error_mock(db);
ASSERT_TRUE(db.Open(file_set.GetDbVirtualFilePath()));
// Create table.
ASSERT_TRUE(db.Execute("CREATE TABLE test (val BLOB)"));
}
// Spawn child.
mojo::Remote<sqlite_vfs::mojom::SqliteVfsMultiprocessTestHelper> child =
SpawnChild("SqliteVfsChild");
// Pass READ-ONLY file set to child and open database.
ASSERT_OK_AND_ASSIGN(
auto pending_file_set,
ShareConnection(file_set_directory(), base::FilePath(kBaseName), file_set,
/*read_write=*/false));
mojo::Remote<sqlite_vfs::mojom::ReadOnlyConnection> connection =
OpenDatabaseReadOnly(child, std::move(pending_file_set));
ASSERT_TRUE(connection.is_bound());
// Reader performs a read BEFORE the writer does its work.
{
base::test::TestFuture<base::expected<std::string, int32_t>> future;
connection->Read(future.GetCallback());
EXPECT_THAT(future.Take(),
ErrorIs(static_cast<int>(sql::SqliteResultCode::kDone)));
}
// Main process writes massive transactions to grow the WAL-index (-shm file).
sql::Database db(
MakeDatabaseOptionsForFileSet(file_set)
// Disable automatic checkpointing to grow the write-ahead log.
.set_wal_commit_callback(base::DoNothing())
// Disable sync so the test isn't slower than it needs to be.
.set_no_sync(true),
sql::Database::Tag("Test"));
::testing::StrictMock<ScopedMockErrorCallback> error_mock(db);
ASSERT_TRUE(db.Open(file_set.GetDbVirtualFilePath()));
auto wal_index_size =
file_set.GetSandboxedDbFile()->GetWalIndexFile().GetLength();
// Insert a whole lot of data. The write-ahead log will be big after this, and
// the WAL-index will have grown a few times.
sql::Transaction transaction(&db);
ASSERT_TRUE(transaction.Begin());
auto blob =
base::MakeRefCounted<base::RefCountedString>(std::string(4096, 'x'));
sql::Statement insert_stmt(
db.GetUniqueStatement("INSERT INTO test (val) VALUES (?)"));
for (int i = 0; i < 25000; ++i) {
insert_stmt.BindBlob(0, blob);
ASSERT_TRUE(insert_stmt.Run());
insert_stmt.Reset(true);
}
ASSERT_TRUE(transaction.Commit());
// Verify that the WAL-index has grown.
EXPECT_GT(file_set.GetSandboxedDbFile()->GetWalIndexFile().GetLength(),
wal_index_size);
// A read-only connection makes a read, causing it to check the WAL-index and
// WAL.
{
base::test::TestFuture<base::expected<std::string, int32_t>> future;
connection->Read(future.GetCallback());
EXPECT_OK(future.Take());
}
wal_index_size = file_set.GetSandboxedDbFile()->GetWalIndexFile().GetLength();
// Grow the WAL even more.
{
sql::Transaction transaction2(&db);
ASSERT_TRUE(transaction2.Begin());
for (int i = 0; i < 10000; ++i) {
insert_stmt.BindBlob(0, blob);
ASSERT_TRUE(insert_stmt.Run());
insert_stmt.Reset(true);
}
ASSERT_TRUE(transaction2.Commit());
}
// Verify that the WAL-index has grown more.
EXPECT_GT(file_set.GetSandboxedDbFile()->GetWalIndexFile().GetLength(),
wal_index_size);
// A read-only connection makes another read, causing it to find new data in
// the WAL-index.
{
base::test::TestFuture<base::expected<std::string, int32_t>> future;
connection->Read(future.GetCallback());
EXPECT_OK(future.Take());
}
// Cleanup.
base::test::TestFuture<std::vector<int32_t>> close_future;
connection->CloseDatabase(base::BindOnce(
[](base::OnceCallback<void(std::vector<int32_t>)> cb,
const std::vector<int32_t>& errors) { std::move(cb).Run(errors); },
close_future.GetCallback()));
EXPECT_THAT(close_future.Take(), IsEmpty());
}
TEST_P(SqliteVfsMultiprocessTest, ReadOnlyFirstConnection) {
if (!journal_mode_wal()) {
GTEST_SKIP() << "This test is only for WAL mode";
}
// Phase 1: Child process creates shared WAL database and inserts data.
{
ASSERT_OK_AND_ASSIGN(
auto file_set,
CreateAndBindFileSet(file_set_directory(), base::FilePath(kBaseName),
/*journal_mode_wal=*/true));
auto unregister_runner =
SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles(
file_set);
mojo::Remote<sqlite_vfs::mojom::SqliteVfsMultiprocessTestHelper> child =
SpawnChild("SqliteVfsChild");
ASSERT_OK_AND_ASSIGN(auto pending_file_set_child,
ShareConnection(file_set_directory(),
base::FilePath(kBaseName), file_set,
/*read_write=*/true));
mojo::Remote<sqlite_vfs::mojom::ReadWriteConnection> connection =
OpenDatabase(child, std::move(pending_file_set_child));
ASSERT_TRUE(connection.is_bound());
// Write data without checkpointing.
{
base::test::TestFuture<bool> future;
connection->Write("hello", future.GetCallback());
ASSERT_TRUE(future.Get());
}
// Forcefully terminate the child to leave WAL index and WAL files behind.
TerminateLastChild();
}
// Phase 2: Create a new connection to the same files.
ASSERT_OK_AND_ASSIGN(auto pending_rw,
MakePendingFileSet(Client::kTest, file_set_directory(),
base::FilePath(kBaseName),
/*single_connection=*/false,
/*journal_mode_wal=*/true));
ASSERT_OK_AND_ASSIGN(
auto file_set_rw_parent,
SqliteVfsFileSet::Bind(Client::kTest, std::move(pending_rw)));
// Share a read-only connection.
ASSERT_OK_AND_ASSIGN(
auto pending_ro,
ShareConnection(file_set_directory(), base::FilePath(kBaseName),
file_set_rw_parent, /*read_write=*/false));
ASSERT_OK_AND_ASSIGN(
auto file_set_ro,
SqliteVfsFileSet::Bind(Client::kTest, std::move(pending_ro)));
// Register read-only files with VFS.
SqliteSandboxedVfsDelegate::UnregisterRunner ro_unregister_runner =
SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles(
file_set_ro);
sql::Database db_ro(MakeDatabaseOptionsForFileSet(file_set_ro),
sql::Database::Tag("Test"));
// Open read-only connection.
ASSERT_TRUE(db_ro.Open(file_set_ro.GetDbVirtualFilePath()));
// Verify that data can be read!
{
sql::Statement s(db_ro.GetUniqueStatement("SELECT val FROM test"));
ASSERT_TRUE(s.Step());
EXPECT_EQ(s.ColumnString(0), "hello");
}
// Phase 3: Open the read-write connection in SQLite.
SqliteSandboxedVfsDelegate::UnregisterRunner rw_unregister_runner =
SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles(
file_set_rw_parent);
sql::Database db_rw(MakeDatabaseOptionsForFileSet(file_set_rw_parent),
sql::Database::Tag("Test"));
ASSERT_TRUE(db_rw.Open(file_set_rw_parent.GetDbVirtualFilePath()));
// Verify both can read.
{
sql::Statement s(db_ro.GetUniqueStatement("SELECT val FROM test"));
ASSERT_TRUE(s.Step());
EXPECT_EQ(s.ColumnString(0), "hello");
}
{
sql::Statement s(db_rw.GetUniqueStatement("SELECT val FROM test"));
ASSERT_TRUE(s.Step());
EXPECT_EQ(s.ColumnString(0), "hello");
}
// Phase 4: Checkpoint the database.
EXPECT_TRUE(db_rw.CheckpointDatabase());
// Verify both can read.
{
sql::Statement s(db_ro.GetUniqueStatement("SELECT val FROM test"));
ASSERT_TRUE(s.Step());
EXPECT_EQ(s.ColumnString(0), "hello");
}
{
sql::Statement s(db_rw.GetUniqueStatement("SELECT val FROM test"));
ASSERT_TRUE(s.Step());
EXPECT_EQ(s.ColumnString(0), "hello");
}
db_ro.Close();
db_rw.Close();
}
TEST_P(SqliteVfsMultiprocessTest, AbandonReadOnlyShared) {
ASSERT_OK_AND_ASSIGN(
auto file_set,
CreateAndBindFileSet(file_set_directory(), base::FilePath(kBaseName),
journal_mode_wal()));
auto unregister_runner =
SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles(
file_set);
sql::Database db(MakeDatabaseOptionsForFileSet(file_set),
sql::Database::Tag("Test"));
::testing::StrictMock<ScopedMockErrorCallback> error_mock(db);
ASSERT_TRUE(db.Open(file_set.GetDbVirtualFilePath()));
// Create table.
ASSERT_TRUE(db.Execute("CREATE TABLE test (val TEXT)"));
ASSERT_TRUE(db.Execute("INSERT INTO test (val) VALUES ('initial')"));
// Spawn child.
mojo::Remote<sqlite_vfs::mojom::SqliteVfsMultiprocessTestHelper> child =
SpawnChild("SqliteVfsChild");
// Pass READ-ONLY file set to child.
ASSERT_OK_AND_ASSIGN(
auto pending_file_set,
ShareConnection(file_set_directory(), base::FilePath(kBaseName), file_set,
/*read_write=*/false));
mojo::Remote<sqlite_vfs::mojom::ReadOnlyConnection> connection =
OpenDatabaseReadOnly(child, std::move(pending_file_set));
ASSERT_TRUE(connection.is_bound());
// Child should see 'initial'!
{
base::test::TestFuture<base::expected<std::string, int32_t>> future;
connection->Read(future.GetCallback());
EXPECT_THAT(future.Take(), ValueIs("initial"));
}
// Parent calls Abandon on the file set.
file_set.Abandon();
// Child tries to read again. It should fail with SQLITE_IOERR_LOCK.
{
base::test::TestFuture<base::expected<std::string, int32_t>> future;
connection->Read(future.GetCallback());
EXPECT_THAT(future.Take(),
ErrorIs(static_cast<int>(sql::SqliteErrorCode::kIoLock)));
}
// Parent closes database.
db.Close();
// Cleanup.
base::test::TestFuture<std::vector<int32_t>> close_future;
connection->CloseDatabase(base::BindOnce(
[](base::OnceCallback<void(std::vector<int32_t>)> cb,
const std::vector<int32_t>& errors) { std::move(cb).Run(errors); },
close_future.GetCallback()));
EXPECT_THAT(close_future.Take(),
ElementsAre(static_cast<int>(sql::SqliteErrorCode::kIoLock)));
}
TEST_P(SqliteVfsMultiprocessTest, AbandonAndReconnect) {
ASSERT_OK_AND_ASSIGN(
auto file_set_1,
CreateAndBindFileSet(file_set_directory(), base::FilePath(kBaseName),
journal_mode_wal()));
{
auto unregister_runner_1 =
SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles(
file_set_1);
sql::Database db_1(MakeDatabaseOptionsForFileSet(file_set_1),
sql::Database::Tag("Test"));
::testing::StrictMock<ScopedMockErrorCallback> error_mock(db_1);
ASSERT_TRUE(db_1.Open(file_set_1.GetDbVirtualFilePath()));
// Create table.
ASSERT_TRUE(db_1.Execute("CREATE TABLE test (val TEXT)"));
ASSERT_TRUE(db_1.Execute("INSERT INTO test (val) VALUES ('initial')"));
}
// Spawn child.
mojo::Remote<sqlite_vfs::mojom::SqliteVfsMultiprocessTestHelper> child =
SpawnChild("SqliteVfsChild");
// Pass read-only file set to child.
ASSERT_OK_AND_ASSIGN(
auto pending_file_set_1,
ShareConnection(file_set_directory(), base::FilePath(kBaseName),
file_set_1, /*read_write=*/false));
mojo::Remote<sqlite_vfs::mojom::ReadOnlyConnection> connection_1 =
OpenDatabaseReadOnly(child, std::move(pending_file_set_1));
ASSERT_TRUE(connection_1.is_bound());
// Child should see 'initial'.
{
base::test::TestFuture<base::expected<std::string, int32_t>> future;
connection_1->Read(future.GetCallback());
EXPECT_THAT(future.Take(), ValueIs("initial"));
}
// Parent Abandons the file set.
file_set_1.Abandon();
// Parent creates a new connection to the same physical database files.
ASSERT_OK_AND_ASSIGN(
auto pending_file_set_2,
MakePendingFileSet(Client::kTest, file_set_directory(),
base::FilePath(kBaseName),
/*single_connection=*/false, journal_mode_wal()));
ASSERT_OK_AND_ASSIGN(
auto file_set_2,
SqliteVfsFileSet::Bind(Client::kTest, std::move(pending_file_set_2)));
auto unregister_runner_2 =
SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles(
file_set_2);
{
sql::Database db_2(MakeDatabaseOptionsForFileSet(file_set_2),
sql::Database::Tag("Test"));
::testing::StrictMock<ScopedMockErrorCallback> error_mock(db_2);
ASSERT_TRUE(db_2.Open(file_set_2.GetDbVirtualFilePath()));
ASSERT_TRUE(db_2.Execute("INSERT INTO test (val) VALUES ('new_value')"));
}
// Parent shares this new connection with the child.
ASSERT_OK_AND_ASSIGN(
auto pending_file_set_2_ro,
ShareConnection(file_set_directory(), base::FilePath(kBaseName),
file_set_2, /*read_write=*/false));
// Child succeeds in opening a second connection because the first has been
// abandoned.
base::test::TestFuture<
mojo::PendingRemote<sqlite_vfs::mojom::ReadOnlyConnection>>
future_2;
child->OpenDatabaseReadOnly(std::move(pending_file_set_2_ro),
future_2.GetCallback());
auto remote_2 = future_2.Take();
ASSERT_TRUE(remote_2.is_valid());
mojo::Remote<sqlite_vfs::mojom::ReadOnlyConnection> connection_2(
std::move(remote_2));
// Verify that child can use the new connection to read the new value.
{
base::test::TestFuture<base::expected<std::string, int32_t>> future;
connection_2->Read(future.GetCallback());
EXPECT_THAT(future.Take(), ValueIs(std::string("new_value")));
}
// Verify that child gets a kIoLock error on connection_1.
{
base::test::TestFuture<base::expected<std::string, int32_t>> future;
connection_1->Read(future.GetCallback());
EXPECT_THAT(future.Take(),
ErrorIs(static_cast<int>(sql::SqliteErrorCode::kIoLock)));
}
// Cleanup.
{
base::test::TestFuture<std::vector<int32_t>> close_future;
connection_1->CloseDatabase(base::BindOnce(
[](base::OnceCallback<void(std::vector<int32_t>)> cb,
const std::vector<int32_t>& errors) { std::move(cb).Run(errors); },
close_future.GetCallback()));
EXPECT_THAT(close_future.Take(),
ElementsAre(static_cast<int>(sql::SqliteErrorCode::kIoLock)));
}
{
base::test::TestFuture<std::vector<int32_t>> close_future;
connection_2->CloseDatabase(base::BindOnce(
[](base::OnceCallback<void(std::vector<int32_t>)> cb,
const std::vector<int32_t>& errors) { std::move(cb).Run(errors); },
close_future.GetCallback()));
EXPECT_THAT(close_future.Take(), IsEmpty());
}
}
INSTANTIATE_TEST_SUITE_P(,
SqliteVfsMultiprocessTest,
Bool(),
[](const ::testing::TestParamInfo<bool>& info) {
return info.param ? "Wal" : "Rollback";
});
class VfsTransitionMultiprocessTest
: public SqliteVfsMultiprocessTestBase,
public ::testing::WithParamInterface<std::tuple<bool, bool>> {
protected:
static bool single_connection() { return std::get<0>(GetParam()); }
static bool journal_mode_wal() { return std::get<1>(GetParam()); }
};
TEST_P(VfsTransitionMultiprocessTest, TransitionAfterCrash) {
const bool single_conn = single_connection();
const bool wal = journal_mode_wal();
const base::FilePath base_name(kBaseName);
const Client client = Client::kTest;
// Step 1: Create file set with parameterized options.
ASSERT_OK_AND_ASSIGN(auto pending_file_set,
MakePendingFileSet(client, file_set_directory(),
base_name, single_conn, wal));
// Spawn child.
mojo::Remote<sqlite_vfs::mojom::SqliteVfsMultiprocessTestHelper> child =
SpawnChild("SqliteVfsChild");
// Pass file set to child.
mojo::Remote<sqlite_vfs::mojom::ReadWriteConnection> connection =
OpenDatabase(child, std::move(pending_file_set));
ASSERT_TRUE(connection.is_bound());
// Child writes to database.
{
base::test::TestFuture<bool> future;
connection->Write("child_value", future.GetCallback());
ASSERT_TRUE(future.Get());
}
// Terminate child process abruptly.
TerminateLastChild();
{
// Step 3: Make new pending file set with opposite journal mode in parent.
ASSERT_OK_AND_ASSIGN(auto pending_file_set_2,
MakePendingFileSet(client, file_set_directory(),
base_name, single_conn, !wal));
ASSERT_OK_AND_ASSIGN(
auto file_set_2,
SqliteVfsFileSet::Bind(client, std::move(pending_file_set_2)));
// Register files in parent.
SqliteSandboxedVfsDelegate::UnregisterRunner unregister_runner_2 =
SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles(
file_set_2);
// Verify database can be opened.
sql::Database db_2(MakeDatabaseOptionsForFileSet(file_set_2), "Test");
EXPECT_TRUE(db_2.Open(file_set_2.GetDbVirtualFilePath()));
// Confirm that the database is using the expected journal mode.
{
sql::Statement s(db_2.GetUniqueStatement("PRAGMA journal_mode"));
ASSERT_TRUE(s.Step());
EXPECT_EQ(s.ColumnString(0), !wal ? "wal" : "truncate");
}
// Verify that the data written by the child is present.
{
sql::Statement s(db_2.GetUniqueStatement("SELECT val FROM test"));
ASSERT_TRUE(s.Step());
EXPECT_EQ(s.ColumnString(0), "child_value");
}
// Execute a statement and confirm once again.
ASSERT_TRUE(db_2.Execute("INSERT INTO test (val) VALUES ('later')"));
{
sql::Statement s(db_2.GetUniqueStatement("PRAGMA journal_mode"));
ASSERT_TRUE(s.Step());
EXPECT_EQ(s.ColumnString(0), !wal ? "wal" : "truncate");
}
// Confirm that the unused journal file is empty.
auto* extension = wal ? kWalJournalFileExtension : kJournalFileExtension;
EXPECT_EQ(base::GetFileSize(file_set_directory()
.Append(base::FilePath(kBaseName))
.AddExtension(extension)),
0LL);
}
// Reopen the database and make sure that the unused file is gone.
{
ASSERT_OK_AND_ASSIGN(auto pending_file_set_3,
MakePendingFileSet(client, file_set_directory(),
base_name, single_conn, !wal));
ASSERT_OK_AND_ASSIGN(
auto file_set_3,
SqliteVfsFileSet::Bind(client, std::move(pending_file_set_3)));
// Register files in parent.
SqliteSandboxedVfsDelegate::UnregisterRunner unregister_runner_3 =
SqliteSandboxedVfsDelegate::GetInstance()->RegisterSandboxedFiles(
file_set_3);
// Verify database can be opened.
sql::Database db_3(MakeDatabaseOptionsForFileSet(file_set_3), "Test");
EXPECT_TRUE(db_3.Open(file_set_3.GetDbVirtualFilePath()));
// Confirm that the database is using the expected journal mode.
{
sql::Statement s(db_3.GetUniqueStatement("PRAGMA journal_mode"));
ASSERT_TRUE(s.Step());
EXPECT_EQ(s.ColumnString(0), !wal ? "wal" : "truncate");
}
// Verify that the previously-written data is present.
{
sql::Statement s(db_3.GetUniqueStatement("SELECT val FROM test"));
std::vector<std::string> values;
ASSERT_TRUE(s.Step());
values.push_back(s.ColumnString(0));
ASSERT_TRUE(s.Step());
values.push_back(s.ColumnString(0));
EXPECT_THAT(values, UnorderedElementsAre("child_value", "later"));
}
// Confirm that the unused journal file is absent.
auto* extension = wal ? kWalJournalFileExtension : kJournalFileExtension;
EXPECT_FALSE(base::PathExists(file_set_directory()
.Append(base::FilePath(kBaseName))
.AddExtension(extension)));
}
}
INSTANTIATE_TEST_SUITE_P(
,
VfsTransitionMultiprocessTest,
Combine(Bool(), Bool()),
[](const ::testing::TestParamInfo<VfsTransitionMultiprocessTest::ParamType>&
info) {
const bool single_connection = std::get<0>(info.param);
const bool journal_mode_wal = std::get<1>(info.param);
return std::string(single_connection ? "SingleConnection"
: "MultiConnection") +
"_" + (journal_mode_wal ? "Wal" : "Rollback");
});
} // namespace sqlite_vfs