blob: 245f00845e5158aa7cad0feae66e2b91e5666a12 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/sqlite_vfs/sqlite_database_vfs_file_set.h"
#include <stdlib.h>
#include <vector>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/test/gmock_expected_support.h"
#include "build/blink_buildflags.h"
#include "build/build_config.h"
#include "components/sqlite_vfs/client.h"
#include "components/sqlite_vfs/constants.h"
#include "components/sqlite_vfs/file_type.h"
#include "components/sqlite_vfs/sandboxed_file.h"
#include "components/sqlite_vfs/vfs_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#if BUILDFLAG(USE_BLINK) && !BUILDFLAG(IS_ANDROID)
#include "base/check.h"
#include "base/command_line.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/test/multiprocess_test.h"
#include "base/test/test_timeouts.h"
#include "testing/multiprocess_func_list.h"
#endif
namespace sqlite_vfs {
namespace {
// Test is parameterized on the values for `single_connection` and
// `journal_mode_wal`.
class SqliteVfsFileSetTest
: public testing::TestWithParam<std::tuple<bool, bool>> {
protected:
static constexpr base::FilePath::StringViewType kBaseName =
FILE_PATH_LITERAL("TEST");
void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); }
const base::FilePath& GetTempDir() const { return temp_dir_.GetPath(); }
std::vector<base::FilePath> GetFilePaths() {
std::vector<base::FilePath> paths;
paths.push_back(
temp_dir_.GetPath().Append(kBaseName).AddExtension(kDbFileExtension));
paths.push_back(temp_dir_.GetPath().Append(kBaseName).AddExtension(
kJournalFileExtension));
if (journal_mode_wal()) {
paths.push_back(temp_dir_.GetPath().Append(kBaseName).AddExtension(
kWalJournalFileExtension));
}
return paths;
}
std::optional<SqliteVfsFileSet> CreateFilesAndBuildVfsFileSet() {
std::optional<SqliteVfsFileSet> file_set;
if (auto pending_file_set = MakePendingFileSet(
Client::kTest, temp_dir_.GetPath(), base::FilePath(kBaseName),
is_single_connection(), journal_mode_wal());
!pending_file_set.has_value()) {
ADD_FAILURE() << "Failed creating pending file_set";
} else {
file_set =
SqliteVfsFileSet::Bind(Client::kTest, *std::move(pending_file_set));
EXPECT_NE(file_set, std::nullopt) << "Failed creating pending file_set";
}
return file_set;
}
const base::FilePath& file_set_directory() const {
return temp_dir_.GetPath();
}
static bool is_single_connection() { return std::get<0>(GetParam()); }
static bool journal_mode_wal() { return std::get<1>(GetParam()); }
private:
base::ScopedTempDir temp_dir_;
};
// Tests that creating and destroying a file set doesn't delete the files.
TEST_P(SqliteVfsFileSetTest, FilesAreNotDeleted) {
auto paths = GetFilePaths();
{
ASSERT_OK_AND_ASSIGN(auto file_set, CreateFilesAndBuildVfsFileSet());
ASSERT_THAT(paths,
testing::Each(testing::ResultOf(base::PathExists, true)));
}
ASSERT_THAT(paths, testing::Each(testing::ResultOf(base::PathExists, true)));
}
// Tests that a file set's files can be deleted while it's in use and are
// absent upon destruction.
TEST_P(SqliteVfsFileSetTest, FilesCanBeDeleted) {
auto paths = GetFilePaths();
{
ASSERT_OK_AND_ASSIGN(auto file_set, CreateFilesAndBuildVfsFileSet());
ASSERT_THAT(paths,
testing::Each(testing::ResultOf(base::PathExists, true)));
ASSERT_THAT(paths,
testing::Each(testing::ResultOf(base::DeleteFile, true)));
}
ASSERT_THAT(paths, testing::Each(testing::ResultOf(base::PathExists, false)));
// No other files should have been left behind.
ASSERT_PRED1(base::IsDirectoryEmpty, GetTempDir());
}
// Multiprocess tests are not supported on non-blink platforms (i.e., iOS), and
// they don't work from this test harness on Android.
#if BUILDFLAG(USE_BLINK) && !BUILDFLAG(IS_ANDROID)
static constexpr std::string_view kDirectorySwitch = "directory";
static constexpr std::string_view kBaseNameSwitch = "base-name";
static constexpr std::string_view kSingleConnection = "single-connection";
static constexpr std::string_view kJournalModeWal = "journal-mode-wal";
// The main function for a child process that returns EXIT_SUCCESS if a named
// file set can be opened in a given directory.
MULTIPROCESS_TEST_MAIN(CanOpenConnectionInChild) {
base::CommandLine& cmd_line = *base::CommandLine::ForCurrentProcess();
base::FilePath directory = cmd_line.GetSwitchValuePath(kDirectorySwitch);
base::FilePath base_name = cmd_line.GetSwitchValuePath(kBaseNameSwitch);
bool single_connection = cmd_line.HasSwitch(kSingleConnection);
bool journal_mode_wal = cmd_line.HasSwitch(kJournalModeWal);
auto pending_file_set = MakePendingFileSet(
Client::kTest, directory, base_name, single_connection, journal_mode_wal);
#if BUILDFLAG(IS_WIN)
// On Windows, the files cannot even be opened a second time if the parent
// used single_connection=true.
if (!pending_file_set.has_value()) {
return EXIT_FAILURE;
}
#else
// Other platforms don't have such protections -- single_connection is
// checked below when the main database file is opened.
CHECK(pending_file_set.has_value());
#endif
auto file_set =
SqliteVfsFileSet::Bind(Client::kTest, *std::move(pending_file_set));
CHECK(file_set.has_value());
SandboxedFile* db_file = file_set->GetSandboxedDbFile();
if (auto file = db_file->TakeUnderlyingFile(FileType::kMainDb);
file.IsValid()) {
// Take care to complete the SandboxedVfs open protocol and close the file
// if it was opened.
db_file->OnFileOpened(std::move(file));
db_file->Close();
return EXIT_SUCCESS; // The file was opened.
}
return EXIT_FAILURE; // Could not open the file.
}
// Tests that an open database can/can't be accessed for other connections based
// on the `single_connection` parameter.
TEST_P(SqliteVfsFileSetTest, MultipleConnections) {
ASSERT_OK_AND_ASSIGN(auto file_set, CreateFilesAndBuildVfsFileSet());
// Open the file, thereby locking it for exclusive access if it was created
// for only a single connection.
SandboxedFile* db_file = file_set.GetSandboxedDbFile();
db_file->OnFileOpened(db_file->TakeUnderlyingFile(FileType::kMainDb));
// Attempt to open the file in another process.
base::CommandLine child_command_line =
base::GetMultiProcessTestChildBaseCommandLine();
child_command_line.AppendSwitchPath(kDirectorySwitch, file_set_directory());
child_command_line.AppendSwitchPath(kBaseNameSwitch,
base::FilePath(kBaseName));
if (is_single_connection()) {
child_command_line.AppendSwitch(kSingleConnection);
}
base::LaunchOptions launch_options;
#if BUILDFLAG(IS_WIN)
launch_options.start_hidden = true;
launch_options.feedback_cursor_off = true;
#endif
base::Process process = base::SpawnMultiProcessTestChild(
"CanOpenConnectionInChild", child_command_line, launch_options);
int exit_code = -1;
ASSERT_TRUE(base::WaitForMultiprocessTestChildExit(
process, TestTimeouts::action_timeout(), &exit_code));
if (is_single_connection()) {
ASSERT_EQ(exit_code, EXIT_FAILURE);
} else {
ASSERT_EQ(exit_code, EXIT_SUCCESS);
}
db_file->Close();
}
#endif // BUILDFLAG(USE_BLINK) && !BUILDFLAG(IS_ANDROID)
INSTANTIATE_TEST_SUITE_P(MultipleConnections,
SqliteVfsFileSetTest,
testing::Combine(testing::Values(false),
testing::Values(false)));
INSTANTIATE_TEST_SUITE_P(SingleConnection,
SqliteVfsFileSetTest,
testing::Combine(testing::Values(true),
testing::Values(false)));
INSTANTIATE_TEST_SUITE_P(JournalModeWal,
SqliteVfsFileSetTest,
testing::Combine(testing::Values(true),
testing::Values(true)));
TEST(SqliteVfsFileSetStaticsTest, GetVirtualFilePathType) {
ASSERT_EQ(SqliteVfsFileSet::GetVirtualFileHistogramVariant(
base::FilePath(FILE_PATH_LITERAL("47/data"))),
"DbFile");
ASSERT_EQ(SqliteVfsFileSet::GetVirtualFileHistogramVariant(
base::FilePath(FILE_PATH_LITERAL("47/data-journal"))),
"JournalFile");
ASSERT_EQ(SqliteVfsFileSet::GetVirtualFileHistogramVariant(
base::FilePath(FILE_PATH_LITERAL("47/data-wal"))),
"WalJournalFile");
}
} // namespace
} // namespace sqlite_vfs