0

Add test coverage for WAL mode in database_unittest

This change makes it so that all tests in database_unittest.cc now run
both with WAL mode on and off. Better test coverage will help us
transition to using WAL mode by default in the future.

Bug: 78507
Change-Id: I116a8151c1ab4919a78657e46df98b26eddb7f11
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2490664
Commit-Queue: Shubham Aggarwal <shuagga@microsoft.com>
Reviewed-by: Victor Costan <pwnall@chromium.org>
Cr-Commit-Position: refs/heads/master@{#821790}
This commit is contained in:
Shubham Aggarwal
2020-10-28 17:57:54 +00:00
committed by Commit Bot
parent 1bfcd597da
commit 00370898ce
3 changed files with 119 additions and 82 deletions

@ -114,9 +114,31 @@ class ScopedUmaskSetter {
} // namespace
using SQLDatabaseTest = sql::SQLTestBase;
// We use the parameter to run all tests with WAL mode on and off.
class SQLDatabaseTest : public SQLTestBase,
public testing::WithParamInterface<bool> {
public:
SQLDatabaseTest() : SQLTestBase(GetDBOptions()) {}
explicit SQLDatabaseTest(DatabaseOptions options) : SQLTestBase(options) {}
TEST_F(SQLDatabaseTest, Execute) {
sql::DatabaseOptions GetDBOptions() {
sql::DatabaseOptions options;
options.wal_mode = IsWALEnabled();
// TODO(crbug.com/1120969): Remove after switching to exclusive mode on by
// default.
options.exclusive_locking = false;
#if defined(OS_FUCHSIA) // Exclusive mode needs to be enabled to enter WAL mode
// on Fuchsia
if (IsWALEnabled()) {
options.exclusive_locking = true;
}
#endif // defined(OS_FUCHSIA)
return options;
}
bool IsWALEnabled() { return GetParam(); }
};
TEST_P(SQLDatabaseTest, Execute) {
// Valid statement should return true.
ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
EXPECT_EQ(SQLITE_OK, db().GetErrorCode());
@ -127,7 +149,7 @@ TEST_F(SQLDatabaseTest, Execute) {
EXPECT_EQ(SQLITE_ERROR, db().GetErrorCode());
}
TEST_F(SQLDatabaseTest, ExecuteWithErrorCode) {
TEST_P(SQLDatabaseTest, ExecuteWithErrorCode) {
ASSERT_EQ(SQLITE_OK,
db().ExecuteAndReturnErrorCode("CREATE TABLE foo (a, b)"));
ASSERT_EQ(SQLITE_ERROR, db().ExecuteAndReturnErrorCode("CREATE TABLE TABLE"));
@ -135,7 +157,7 @@ TEST_F(SQLDatabaseTest, ExecuteWithErrorCode) {
"INSERT INTO foo(a, b) VALUES (1, 2, 3, 4)"));
}
TEST_F(SQLDatabaseTest, CachedStatement) {
TEST_P(SQLDatabaseTest, CachedStatement) {
sql::StatementID id1 = SQL_FROM_HERE;
sql::StatementID id2 = SQL_FROM_HERE;
static const char kId1Sql[] = "SELECT a FROM foo";
@ -195,13 +217,13 @@ TEST_F(SQLDatabaseTest, CachedStatement) {
<< "Using a different SQL with the same statement ID should DCHECK";
}
TEST_F(SQLDatabaseTest, IsSQLValidTest) {
TEST_P(SQLDatabaseTest, IsSQLValidTest) {
ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
ASSERT_TRUE(db().IsSQLValid("SELECT a FROM foo"));
ASSERT_FALSE(db().IsSQLValid("SELECT no_exist FROM foo"));
}
TEST_F(SQLDatabaseTest, DoesTableExist) {
TEST_P(SQLDatabaseTest, DoesTableExist) {
EXPECT_FALSE(db().DoesTableExist("foo"));
EXPECT_FALSE(db().DoesTableExist("foo_index"));
@ -215,7 +237,7 @@ TEST_F(SQLDatabaseTest, DoesTableExist) {
EXPECT_FALSE(db().DoesTableExist("FOO"));
}
TEST_F(SQLDatabaseTest, DoesIndexExist) {
TEST_P(SQLDatabaseTest, DoesIndexExist) {
ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
EXPECT_FALSE(db().DoesIndexExist("foo"));
EXPECT_FALSE(db().DoesIndexExist("foo_ubdex"));
@ -230,7 +252,7 @@ TEST_F(SQLDatabaseTest, DoesIndexExist) {
EXPECT_FALSE(db().DoesIndexExist("FOO_INDEX"));
}
TEST_F(SQLDatabaseTest, DoesViewExist) {
TEST_P(SQLDatabaseTest, DoesViewExist) {
EXPECT_FALSE(db().DoesViewExist("voo"));
ASSERT_TRUE(db().Execute("CREATE VIEW voo (a) AS SELECT 1"));
EXPECT_FALSE(db().DoesIndexExist("voo"));
@ -242,7 +264,7 @@ TEST_F(SQLDatabaseTest, DoesViewExist) {
EXPECT_FALSE(db().DoesViewExist("VOO"));
}
TEST_F(SQLDatabaseTest, DoesColumnExist) {
TEST_P(SQLDatabaseTest, DoesColumnExist) {
ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
EXPECT_FALSE(db().DoesColumnExist("foo", "bar"));
@ -257,7 +279,7 @@ TEST_F(SQLDatabaseTest, DoesColumnExist) {
EXPECT_TRUE(db().DoesColumnExist("foo", "A"));
}
TEST_F(SQLDatabaseTest, GetLastInsertRowId) {
TEST_P(SQLDatabaseTest, GetLastInsertRowId) {
ASSERT_TRUE(db().Execute("CREATE TABLE foo (id INTEGER PRIMARY KEY, value)"));
ASSERT_TRUE(db().Execute("INSERT INTO foo (value) VALUES (12)"));
@ -273,7 +295,7 @@ TEST_F(SQLDatabaseTest, GetLastInsertRowId) {
EXPECT_EQ(12, s.ColumnInt(0));
}
TEST_F(SQLDatabaseTest, Rollback) {
TEST_P(SQLDatabaseTest, Rollback) {
ASSERT_TRUE(db().BeginTransaction());
ASSERT_TRUE(db().BeginTransaction());
EXPECT_EQ(2, db().transaction_nesting());
@ -284,7 +306,7 @@ TEST_F(SQLDatabaseTest, Rollback) {
// Test the scoped error expecter by attempting to insert a duplicate
// value into an index.
TEST_F(SQLDatabaseTest, ScopedErrorExpecter) {
TEST_P(SQLDatabaseTest, ScopedErrorExpecter) {
const char* kCreateSql = "CREATE TABLE foo (id INTEGER UNIQUE)";
ASSERT_TRUE(db().Execute(kCreateSql));
ASSERT_TRUE(db().Execute("INSERT INTO foo (id) VALUES (12)"));
@ -299,7 +321,7 @@ TEST_F(SQLDatabaseTest, ScopedErrorExpecter) {
// Test that clients of GetUntrackedStatement() can test corruption-handling
// with ScopedErrorExpecter.
TEST_F(SQLDatabaseTest, ScopedIgnoreUntracked) {
TEST_P(SQLDatabaseTest, ScopedIgnoreUntracked) {
const char* kCreateSql = "CREATE TABLE foo (id INTEGER UNIQUE)";
ASSERT_TRUE(db().Execute(kCreateSql));
ASSERT_FALSE(db().DoesTableExist("bar"));
@ -321,7 +343,7 @@ TEST_F(SQLDatabaseTest, ScopedIgnoreUntracked) {
}
}
TEST_F(SQLDatabaseTest, ErrorCallback) {
TEST_P(SQLDatabaseTest, ErrorCallback) {
const char* kCreateSql = "CREATE TABLE foo (id INTEGER UNIQUE)";
ASSERT_TRUE(db().Execute(kCreateSql));
ASSERT_TRUE(db().Execute("INSERT INTO foo (id) VALUES (12)"));
@ -378,7 +400,7 @@ TEST_F(SQLDatabaseTest, ErrorCallback) {
// Test that sql::Database::Raze() results in a database without the
// tables from the original database.
TEST_F(SQLDatabaseTest, Raze) {
TEST_P(SQLDatabaseTest, Raze) {
const char* kCreateSql = "CREATE TABLE foo (id INTEGER PRIMARY KEY, value)";
ASSERT_TRUE(db().Execute(kCreateSql));
ASSERT_TRUE(db().Execute("INSERT INTO foo (value) VALUES (12)"));
@ -476,7 +498,7 @@ void TestPageSize(const base::FilePath& db_prefix,
// Verify that sql::Recovery maintains the page size, and the virtual table
// works with page sizes other than SQLite's default. Also verify the case
// where the default page size has changed.
TEST_F(SQLDatabaseTest, RazePageSize) {
TEST_P(SQLDatabaseTest, RazePageSize) {
const std::string default_page_size =
ExecuteWithResult(&db(), "PRAGMA page_size");
@ -501,11 +523,11 @@ TEST_F(SQLDatabaseTest, RazePageSize) {
}
// Test that Raze() results are seen in other connections.
TEST_F(SQLDatabaseTest, RazeMultiple) {
TEST_P(SQLDatabaseTest, RazeMultiple) {
const char* kCreateSql = "CREATE TABLE foo (id INTEGER PRIMARY KEY, value)";
ASSERT_TRUE(db().Execute(kCreateSql));
sql::Database other_db;
sql::Database other_db(GetDBOptions());
ASSERT_TRUE(other_db.Open(db_path()));
// Check that the second connection sees the table.
@ -517,14 +539,14 @@ TEST_F(SQLDatabaseTest, RazeMultiple) {
ASSERT_EQ(0, SqliteMasterCount(&other_db));
}
TEST_F(SQLDatabaseTest, RazeLocked) {
TEST_P(SQLDatabaseTest, RazeLocked) {
const char* kCreateSql = "CREATE TABLE foo (id INTEGER PRIMARY KEY, value)";
ASSERT_TRUE(db().Execute(kCreateSql));
// Open a transaction and write some data in a second connection.
// This will acquire a PENDING or EXCLUSIVE transaction, which will
// cause the raze to fail.
sql::Database other_db;
sql::Database other_db(GetDBOptions());
ASSERT_TRUE(other_db.Open(db_path()));
ASSERT_TRUE(other_db.BeginTransaction());
const char* kInsertSql = "INSERT INTO foo VALUES (1, 'data')";
@ -544,7 +566,7 @@ TEST_F(SQLDatabaseTest, RazeLocked) {
// blocks raze.
// This doesn't happen in WAL mode because reads are no longer blocked by
// write operations when using a WAL.
if (!base::FeatureList::IsEnabled(sql::features::kEnableWALModeByDefault)) {
if (!IsWALEnabled()) {
const char* kQuery = "SELECT COUNT(*) FROM foo";
sql::Statement s(other_db.GetUniqueStatement(kQuery));
ASSERT_TRUE(s.Step());
@ -558,7 +580,7 @@ TEST_F(SQLDatabaseTest, RazeLocked) {
// Verify that Raze() can handle an empty file. SQLite should treat
// this as an empty database.
TEST_F(SQLDatabaseTest, RazeEmptyDB) {
TEST_P(SQLDatabaseTest, RazeEmptyDB) {
const char* kCreateSql = "CREATE TABLE foo (id INTEGER PRIMARY KEY, value)";
ASSERT_TRUE(db().Execute(kCreateSql));
db().Close();
@ -573,7 +595,7 @@ TEST_F(SQLDatabaseTest, RazeEmptyDB) {
// Verify that Raze() can handle a file of junk.
// Need exclusive mode off here as there are some subtleties (by design) around
// how the cache is used with it on which causes the test to fail.
TEST_F(SQLDatabaseTest, RazeNOTADB) {
TEST_P(SQLDatabaseTest, RazeNOTADB) {
db().Close();
sql::Database::Delete(db_path());
ASSERT_FALSE(GetPathExists(db_path()));
@ -599,7 +621,7 @@ TEST_F(SQLDatabaseTest, RazeNOTADB) {
}
// Verify that Raze() can handle a database overwritten with garbage.
TEST_F(SQLDatabaseTest, RazeNOTADB2) {
TEST_P(SQLDatabaseTest, RazeNOTADB2) {
const char* kCreateSql = "CREATE TABLE foo (id INTEGER PRIMARY KEY, value)";
ASSERT_TRUE(db().Execute(kCreateSql));
ASSERT_EQ(1, SqliteMasterCount(&db()));
@ -628,7 +650,7 @@ TEST_F(SQLDatabaseTest, RazeNOTADB2) {
// essential for cases where the Open() can fail entirely, so the
// Raze() cannot happen later. Additionally test that when the
// callback does this during Open(), the open is retried and succeeds.
TEST_F(SQLDatabaseTest, RazeCallbackReopen) {
TEST_P(SQLDatabaseTest, RazeCallbackReopen) {
const char* kCreateSql = "CREATE TABLE foo (id INTEGER PRIMARY KEY, value)";
ASSERT_TRUE(db().Execute(kCreateSql));
ASSERT_EQ(1, SqliteMasterCount(&db()));
@ -661,7 +683,7 @@ TEST_F(SQLDatabaseTest, RazeCallbackReopen) {
}
// Basic test of RazeAndClose() operation.
TEST_F(SQLDatabaseTest, RazeAndClose) {
TEST_P(SQLDatabaseTest, RazeAndClose) {
const char* kCreateSql = "CREATE TABLE foo (id INTEGER PRIMARY KEY, value)";
const char* kPopulateSql = "INSERT INTO foo (value) VALUES (12)";
@ -689,7 +711,7 @@ TEST_F(SQLDatabaseTest, RazeAndClose) {
// Test that various operations fail without crashing after
// RazeAndClose().
TEST_F(SQLDatabaseTest, RazeAndCloseDiagnostics) {
TEST_P(SQLDatabaseTest, RazeAndCloseDiagnostics) {
const char* kCreateSql = "CREATE TABLE foo (id INTEGER PRIMARY KEY, value)";
const char* kPopulateSql = "INSERT INTO foo (value) VALUES (12)";
const char* kSimpleSql = "SELECT 1";
@ -763,7 +785,7 @@ TEST_F(SQLDatabaseTest, RazeAndCloseDiagnostics) {
// Test that Raze() turns off memory mapping so that the file is truncated.
// [This would not cover the case of multiple connections where one of the other
// connections is memory-mapped. That is infrequent in Chromium.]
TEST_F(SQLDatabaseTest, RazeTruncate) {
TEST_P(SQLDatabaseTest, RazeTruncate) {
// The empty database has 0 or 1 pages. Raze() should leave it with exactly 1
// page. Not checking directly because auto_vacuum on Android adds a freelist
// page.
@ -800,7 +822,7 @@ TEST_F(SQLDatabaseTest, RazeTruncate) {
}
#if defined(OS_ANDROID)
TEST_F(SQLDatabaseTest, SetTempDirForSQL) {
TEST_P(SQLDatabaseTest, SetTempDirForSQL) {
sql::MetaTable meta_table;
// Below call needs a temporary directory in sqlite3
// On Android, it can pass only when the temporary directory is set.
@ -811,27 +833,7 @@ TEST_F(SQLDatabaseTest, SetTempDirForSQL) {
}
#endif // defined(OS_ANDROID)
// Contained param indicates whether WAL mode is switched on or not.
class JournalModeTest : public SQLDatabaseTest,
public testing::WithParamInterface<bool> {
public:
JournalModeTest() : SQLDatabaseTest(GetDBOptions()) {}
sql::DatabaseOptions GetDBOptions() {
sql::DatabaseOptions options;
options.wal_mode = IsWALEnabled();
#if defined(OS_FUCHSIA) // Exclusive mode needs to be enabled to enter WAL mode
// on Fuchsia
if (IsWALEnabled()) {
options.exclusive_locking = true;
}
#endif // defined(OS_FUCHSIA)
return options;
}
bool IsWALEnabled() { return GetParam(); }
};
TEST_P(JournalModeTest, Delete) {
TEST_P(SQLDatabaseTest, Delete) {
EXPECT_TRUE(db().Execute("CREATE TABLE x (x)"));
db().Close();
@ -853,15 +855,13 @@ TEST_P(JournalModeTest, Delete) {
// WAL mode is currently not supported on Fuchsia
#if !defined(OS_FUCHSIA)
INSTANTIATE_TEST_SUITE_P(SQLDatabaseTest, JournalModeTest, testing::Bool());
INSTANTIATE_TEST_SUITE_P(JournalMode, SQLDatabaseTest, testing::Bool());
#else
INSTANTIATE_TEST_SUITE_P(SQLDatabaseTest,
JournalModeTest,
testing::Values(false));
INSTANTIATE_TEST_SUITE_P(JournalMode, SQLDatabaseTest, testing::Values(false));
#endif
#if defined(OS_POSIX) // This test operates on POSIX file permissions.
TEST_P(JournalModeTest, PosixFilePermissions) {
TEST_P(SQLDatabaseTest, PosixFilePermissions) {
db().Close();
sql::Database::Delete(db_path());
ASSERT_FALSE(GetPathExists(db_path()));
@ -910,7 +910,7 @@ TEST_P(JournalModeTest, PosixFilePermissions) {
#endif // defined(OS_POSIX)
// Test that errors start happening once Poison() is called.
TEST_F(SQLDatabaseTest, Poison) {
TEST_P(SQLDatabaseTest, Poison) {
EXPECT_TRUE(db().Execute("CREATE TABLE x (x)"));
// Before the Poison() call, things generally work.
@ -957,7 +957,7 @@ TEST_F(SQLDatabaseTest, Poison) {
EXPECT_FALSE(db().CommitTransaction());
}
TEST_F(SQLDatabaseTest, AttachDatabase) {
TEST_P(SQLDatabaseTest, AttachDatabase) {
EXPECT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
// Create a database to attach to.
@ -990,7 +990,7 @@ TEST_F(SQLDatabaseTest, AttachDatabase) {
EXPECT_FALSE(db().IsSQLValid("SELECT count(*) from other.bar"));
}
TEST_F(SQLDatabaseTest, AttachDatabaseWithOpenTransaction) {
TEST_P(SQLDatabaseTest, AttachDatabaseWithOpenTransaction) {
EXPECT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
// Create a database to attach to.
@ -1036,7 +1036,7 @@ TEST_F(SQLDatabaseTest, AttachDatabaseWithOpenTransaction) {
EXPECT_FALSE(db().IsSQLValid("SELECT count(*) from other.bar"));
}
TEST_F(SQLDatabaseTest, Basic_QuickIntegrityCheck) {
TEST_P(SQLDatabaseTest, Basic_QuickIntegrityCheck) {
const char* kCreateSql = "CREATE TABLE foo (id INTEGER PRIMARY KEY, value)";
ASSERT_TRUE(db().Execute(kCreateSql));
EXPECT_TRUE(db().QuickIntegrityCheck());
@ -1053,7 +1053,7 @@ TEST_F(SQLDatabaseTest, Basic_QuickIntegrityCheck) {
}
}
TEST_F(SQLDatabaseTest, Basic_FullIntegrityCheck) {
TEST_P(SQLDatabaseTest, Basic_FullIntegrityCheck) {
const std::string kOk("ok");
std::vector<std::string> messages;
@ -1080,7 +1080,7 @@ TEST_F(SQLDatabaseTest, Basic_FullIntegrityCheck) {
// file that would pass the quick check and fail the full check.
}
TEST_F(SQLDatabaseTest, OnMemoryDump) {
TEST_P(SQLDatabaseTest, OnMemoryDump) {
base::trace_event::MemoryDumpArgs args = {
base::trace_event::MemoryDumpLevelOfDetail::DETAILED};
base::trace_event::ProcessMemoryDump pmd(args);
@ -1090,7 +1090,7 @@ TEST_F(SQLDatabaseTest, OnMemoryDump) {
// Test that the functions to collect diagnostic data run to completion, without
// worrying too much about what they generate (since that will change).
TEST_F(SQLDatabaseTest, CollectDiagnosticInfo) {
TEST_P(SQLDatabaseTest, CollectDiagnosticInfo) {
const std::string corruption_info = db().CollectCorruptionInfo();
EXPECT_NE(std::string::npos, corruption_info.find("SQLITE_CORRUPT"));
EXPECT_NE(std::string::npos, corruption_info.find("integrity_check"));
@ -1123,7 +1123,7 @@ TEST_F(SQLDatabaseTest, CollectDiagnosticInfo) {
// Test that a fresh database has mmap enabled by default, if mmap'ed I/O is
// enabled by SQLite.
TEST_F(SQLDatabaseTest, MmapInitiallyEnabled) {
TEST_P(SQLDatabaseTest, MmapInitiallyEnabled) {
{
sql::Statement s(db().GetUniqueStatement("PRAGMA mmap_size"));
ASSERT_TRUE(s.Step())
@ -1151,7 +1151,7 @@ TEST_F(SQLDatabaseTest, MmapInitiallyEnabled) {
// Test whether a fresh database gets mmap enabled when using alternate status
// storage.
TEST_F(SQLDatabaseTest, MmapInitiallyEnabledAltStatus) {
TEST_P(SQLDatabaseTest, MmapInitiallyEnabledAltStatus) {
// Re-open fresh database with alt-status flag set.
db().Close();
sql::Database::Delete(db_path());
@ -1183,7 +1183,7 @@ TEST_F(SQLDatabaseTest, MmapInitiallyEnabledAltStatus) {
EXPECT_EQ("0", ExecuteWithResult(&db(), "PRAGMA mmap_size"));
}
TEST_F(SQLDatabaseTest, GetAppropriateMmapSize) {
TEST_P(SQLDatabaseTest, GetAppropriateMmapSize) {
const size_t kMmapAlot = 25 * 1024 * 1024;
int64_t mmap_status = MetaTable::kMmapFailure;
@ -1226,7 +1226,7 @@ TEST_F(SQLDatabaseTest, GetAppropriateMmapSize) {
ASSERT_EQ(MetaTable::kMmapSuccess, mmap_status);
}
TEST_F(SQLDatabaseTest, GetAppropriateMmapSizeAltStatus) {
TEST_P(SQLDatabaseTest, GetAppropriateMmapSizeAltStatus) {
const size_t kMmapAlot = 25 * 1024 * 1024;
// At this point, Database still expects a future [meta] table.
@ -1262,7 +1262,7 @@ TEST_F(SQLDatabaseTest, GetAppropriateMmapSizeAltStatus) {
ExecuteWithResult(&db(), "SELECT * FROM MmapStatus"));
}
TEST_F(SQLDatabaseTest, GetMemoryUsage) {
TEST_P(SQLDatabaseTest, GetMemoryUsage) {
// Databases with mmap enabled may not follow the assumptions below.
db().Close();
db().set_mmap_disabled();
@ -1287,24 +1287,29 @@ TEST_F(SQLDatabaseTest, GetMemoryUsage) {
class SQLDatabaseTestExclusiveMode : public SQLDatabaseTest {
public:
SQLDatabaseTestExclusiveMode()
: SQLDatabaseTest({.exclusive_locking = true}) {}
SQLDatabaseTestExclusiveMode() : SQLDatabaseTest(GetDBOptions()) {}
DatabaseOptions GetDBOptions() {
DatabaseOptions options = SQLDatabaseTest::GetDBOptions();
options.exclusive_locking = true;
return options;
}
};
TEST_F(SQLDatabaseTestExclusiveMode, LockingModeExclusive) {
TEST_P(SQLDatabaseTestExclusiveMode, LockingModeExclusive) {
EXPECT_EQ(ExecuteWithResult(&db(), "PRAGMA locking_mode"), "exclusive");
}
TEST_F(SQLDatabaseTest, LockingModeNormal) {
TEST_P(SQLDatabaseTest, LockingModeNormal) {
EXPECT_EQ(ExecuteWithResult(&db(), "PRAGMA locking_mode"), "normal");
}
TEST_P(JournalModeTest, OpenedInCorrectMode) {
TEST_P(SQLDatabaseTest, OpenedInCorrectMode) {
std::string expected_mode = IsWALEnabled() ? "wal" : "truncate";
EXPECT_EQ(ExecuteWithResult(&db(), "PRAGMA journal_mode"), expected_mode);
}
TEST_P(JournalModeTest, CheckpointDatabase) {
TEST_P(SQLDatabaseTest, CheckpointDatabase) {
if (!IsWALEnabled())
return;
@ -1338,14 +1343,16 @@ TEST_P(JournalModeTest, CheckpointDatabase) {
EXPECT_EQ(ExecuteWithResult(&db(), "SELECT value FROM foo where id=2"), "2");
}
TEST_F(SQLDatabaseTest, CorruptSizeInHeaderTest) {
TEST_P(SQLDatabaseTest, CorruptSizeInHeaderTest) {
ASSERT_TRUE(db().Execute("CREATE TABLE foo (x)"));
ASSERT_TRUE(db().Execute("CREATE TABLE bar (x)"));
db().Close();
ASSERT_TRUE(CorruptSizeInHeaderOfDB());
{
sql::test::ScopedErrorExpecter expecter;
expecter.ExpectError(SQLITE_CORRUPT);
ASSERT_TRUE(db().Open(db_path()));
EXPECT_FALSE(db().Execute("INSERT INTO foo values (1)"));
EXPECT_FALSE(db().DoesTableExist("foo"));
EXPECT_FALSE(db().DoesTableExist("bar"));
@ -1357,7 +1364,7 @@ TEST_F(SQLDatabaseTest, CorruptSizeInHeaderTest) {
// To prevent invalid SQL from accidentally shipping to production, prepared
// statements which fail to compile with SQLITE_ERROR call DLOG(DCHECK). This
// case cannot be suppressed with an error callback.
TEST_F(SQLDatabaseTest, CompileError) {
TEST_P(SQLDatabaseTest, CompileError) {
// DEATH tests not supported on Android, iOS, or Fuchsia.
#if !defined(OS_ANDROID) && !defined(OS_IOS) && !defined(OS_FUCHSIA)
if (DLOG_IS_ON(FATAL)) {

@ -9,6 +9,7 @@ namespace sql {
namespace test {
struct ColumnInfo;
bool CorruptSizeInHeader(const base::FilePath&);
} // namespace test
// Restricts access to APIs internal to the //sql package.
@ -23,6 +24,7 @@ class InternalApiToken {
friend class DatabaseTestPeer;
friend class Recovery;
friend struct test::ColumnInfo;
friend bool test::CorruptSizeInHeader(const base::FilePath&);
};
} // namespace sql

@ -10,12 +10,12 @@
#include <memory>
#include <string>
#include "base/check.h"
#include "base/check_op.h"
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/threading/thread_restrictions.h"
#include "sql/database.h"
#include "sql/sql_features.h"
#include "sql/statement.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/sqlite/sqlite3.h"
@ -71,21 +71,49 @@ void WriteBigEndian(unsigned val, unsigned char* buf, size_t bytes) {
}
}
bool IsWalDatabase(const base::FilePath& db_path) {
// The SQLite header is documented at:
// https://www.sqlite.org/fileformat.html#the_database_header
//
// Read the entire header.
constexpr int kHeaderSize = 100;
uint8_t header[kHeaderSize];
base::ReadFile(db_path, reinterpret_cast<char*>(header), sizeof(header));
constexpr int kWriteVersionHeaderOffset = 18;
constexpr int kReadVersionHeaderOffset = 19;
// If the read version is unsupported, we can't rely on our ability to
// interpret anything else in the header.
DCHECK_LE(header[kReadVersionHeaderOffset], 2)
<< "Unsupported SQLite file format";
return header[kWriteVersionHeaderOffset] == 2;
}
} // namespace
namespace sql {
namespace test {
bool CorruptSizeInHeader(const base::FilePath& db_path) {
// Checkpoint the WAL file in Truncate mode before corrupting to ensure that
// any future transaction always touches the DB file and not just the WAL
// file.
if (base::FeatureList::IsEnabled(sql::features::kEnableWALModeByDefault)) {
if (IsWalDatabase(db_path)) {
// Checkpoint the WAL file in Truncate mode before corrupting to ensure that
// any future transaction always touches the DB file and not just the WAL
// file.
base::ScopedAllowBlockingForTesting allow_blocking;
sql::Database db;
// TODO: This function doesn't reliably work if connections to the DB are
// still open. Change any uses to ensure that we close all database
// connections before calling this function.
sql::Database db({.exclusive_locking = false, .wal_mode = true});
if (!db.Open(db_path))
return false;
if (!db.Execute("PRAGMA wal_checkpoint(TRUNCATE)"))
int wal_log_size = 0;
int checkpointed_frame_count = 0;
int truncate_result = sqlite3_wal_checkpoint_v2(
db.db(InternalApiToken()), /*zDb=*/nullptr, SQLITE_CHECKPOINT_TRUNCATE,
&wal_log_size, &checkpointed_frame_count);
// A successful checkpoint in truncate mode sets these to zero.
DCHECK(wal_log_size == 0);
DCHECK(checkpointed_frame_count == 0);
if (truncate_result != SQLITE_OK)
return false;
db.Close();
}