| // 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 |