From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- storage/test/gtest/moz.build | 39 ++ storage/test/gtest/storage_test_harness.cpp | 220 ++++++++++ storage/test/gtest/storage_test_harness.h | 206 ++++++++++ storage/test/gtest/test_AsXXX_helpers.cpp | 111 +++++ storage/test/gtest/test_StatementCache.cpp | 132 ++++++ .../test_asyncStatementExecution_transaction.cpp | 445 +++++++++++++++++++++ .../test_async_callbacks_with_spun_event_loops.cpp | 146 +++++++ storage/test/gtest/test_binding_params.cpp | 186 +++++++++ storage/test/gtest/test_deadlock_detector.cpp | 56 +++ storage/test/gtest/test_file_perms.cpp | 35 ++ .../gtest/test_interruptSynchronousConnection.cpp | 81 ++++ storage/test/gtest/test_mutex.cpp | 73 ++++ .../test/gtest/test_spinningSynchronousClose.cpp | 72 ++++ storage/test/gtest/test_statement_scoper.cpp | 86 ++++ storage/test/gtest/test_transaction_helper.cpp | 184 +++++++++ storage/test/gtest/test_true_async.cpp | 161 ++++++++ storage/test/gtest/test_unlock_notify.cpp | 234 +++++++++++ 17 files changed, 2467 insertions(+) create mode 100644 storage/test/gtest/moz.build create mode 100644 storage/test/gtest/storage_test_harness.cpp create mode 100644 storage/test/gtest/storage_test_harness.h create mode 100644 storage/test/gtest/test_AsXXX_helpers.cpp create mode 100644 storage/test/gtest/test_StatementCache.cpp create mode 100644 storage/test/gtest/test_asyncStatementExecution_transaction.cpp create mode 100644 storage/test/gtest/test_async_callbacks_with_spun_event_loops.cpp create mode 100644 storage/test/gtest/test_binding_params.cpp create mode 100644 storage/test/gtest/test_deadlock_detector.cpp create mode 100644 storage/test/gtest/test_file_perms.cpp create mode 100644 storage/test/gtest/test_interruptSynchronousConnection.cpp create mode 100644 storage/test/gtest/test_mutex.cpp create mode 100644 storage/test/gtest/test_spinningSynchronousClose.cpp create mode 100644 storage/test/gtest/test_statement_scoper.cpp create mode 100644 storage/test/gtest/test_transaction_helper.cpp create mode 100644 storage/test/gtest/test_true_async.cpp create mode 100644 storage/test/gtest/test_unlock_notify.cpp (limited to 'storage/test/gtest') diff --git a/storage/test/gtest/moz.build b/storage/test/gtest/moz.build new file mode 100644 index 0000000000..62f91b6d95 --- /dev/null +++ b/storage/test/gtest/moz.build @@ -0,0 +1,39 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +UNIFIED_SOURCES += [ + "storage_test_harness.cpp", + "test_AsXXX_helpers.cpp", + "test_async_callbacks_with_spun_event_loops.cpp", + "test_asyncStatementExecution_transaction.cpp", + "test_binding_params.cpp", + "test_file_perms.cpp", + "test_interruptSynchronousConnection.cpp", + "test_mutex.cpp", + "test_spinningSynchronousClose.cpp", + "test_statement_scoper.cpp", + "test_StatementCache.cpp", + "test_transaction_helper.cpp", + "test_true_async.cpp", + "test_unlock_notify.cpp", +] + +if ( + CONFIG["MOZ_DEBUG"] + and CONFIG["OS_ARCH"] not in ("WINNT") + and CONFIG["OS_TARGET"] != "Android" +): + # FIXME bug 523392: test_deadlock_detector doesn't like Windows + # Bug 1054249: Doesn't work on Android + UNIFIED_SOURCES += [ + "test_deadlock_detector.cpp", + ] + +LOCAL_INCLUDES += [ + "../..", +] + +FINAL_LIBRARY = "xul-gtest" diff --git a/storage/test/gtest/storage_test_harness.cpp b/storage/test/gtest/storage_test_harness.cpp new file mode 100644 index 0000000000..3f5a9ff82b --- /dev/null +++ b/storage/test/gtest/storage_test_harness.cpp @@ -0,0 +1,220 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef storage_test_harness_ +#define storage_test_harness_ + +#include "storage_test_harness.h" + +already_AddRefed getService() { + nsCOMPtr ss = + do_CreateInstance("@mozilla.org/storage/service;1"); + do_check_true(ss); + return ss.forget(); +} + +already_AddRefed getMemoryDatabase() { + nsCOMPtr ss = getService(); + nsCOMPtr conn; + nsresult rv = ss->OpenSpecialDatabase( + kMozStorageMemoryStorageKey, VoidCString(), + mozIStorageService::CONNECTION_DEFAULT, getter_AddRefs(conn)); + do_check_success(rv); + return conn.forget(); +} + +already_AddRefed getDatabase(nsIFile* aDBFile, + uint32_t aConnectionFlags) { + nsCOMPtr dbFile; + nsresult rv; + if (!aDBFile) { + MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Can't get tmp dir off mainthread."); + (void)NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(dbFile)); + NS_ASSERTION(dbFile, "The directory doesn't exists?!"); + + rv = dbFile->Append(u"storage_test_db.sqlite"_ns); + do_check_success(rv); + } else { + dbFile = aDBFile; + } + + nsCOMPtr ss = getService(); + nsCOMPtr conn; + rv = ss->OpenDatabase(dbFile, aConnectionFlags, getter_AddRefs(conn)); + do_check_success(rv); + return conn.forget(); +} + +NS_IMPL_ISUPPORTS(AsyncStatementSpinner, mozIStorageStatementCallback, + mozIStorageCompletionCallback) + +AsyncStatementSpinner::AsyncStatementSpinner() + : completionReason(0), mCompleted(false) {} + +NS_IMETHODIMP +AsyncStatementSpinner::HandleResult(mozIStorageResultSet* aResultSet) { + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatementSpinner::HandleError(mozIStorageError* aError) { + int32_t result; + nsresult rv = aError->GetResult(&result); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString message; + rv = aError->GetMessage(message); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString warnMsg; + warnMsg.AppendLiteral( + "An error occurred while executing an async statement: "); + warnMsg.AppendInt(result); + warnMsg.Append(' '); + warnMsg.Append(message); + NS_WARNING(warnMsg.get()); + + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatementSpinner::HandleCompletion(uint16_t aReason) { + completionReason = aReason; + mCompleted = true; + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatementSpinner::Complete(nsresult, nsISupports*) { + mCompleted = true; + return NS_OK; +} + +void AsyncStatementSpinner::SpinUntilCompleted() { + nsCOMPtr thread(::do_GetCurrentThread()); + nsresult rv = NS_OK; + bool processed = true; + while (!mCompleted && NS_SUCCEEDED(rv)) { + rv = thread->ProcessNextEvent(true, &processed); + } +} + +#define NS_DECL_ASYNCSTATEMENTSPINNER \ + NS_IMETHOD HandleResult(mozIStorageResultSet* aResultSet) override; + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * Execute an async statement, blocking the main thread until we get the + * callback completion notification. + */ +void blocking_async_execute(mozIStorageBaseStatement* stmt) { + RefPtr spinner(new AsyncStatementSpinner()); + + nsCOMPtr pendy; + (void)stmt->ExecuteAsync(spinner, getter_AddRefs(pendy)); + spinner->SpinUntilCompleted(); +} + +/** + * Invoke AsyncClose on the given connection, blocking the main thread until we + * get the completion notification. + */ +void blocking_async_close(mozIStorageConnection* db) { + RefPtr spinner(new AsyncStatementSpinner()); + + db->AsyncClose(spinner); + spinner->SpinUntilCompleted(); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Mutex Watching + +/** + * Verify that mozIStorageAsyncStatement's life-cycle never triggers a mutex on + * the caller (generally main) thread. We do this by decorating the sqlite + * mutex logic with our own code that checks what thread it is being invoked on + * and sets a flag if it is invoked on the main thread. We are able to easily + * decorate the SQLite mutex logic because SQLite allows us to retrieve the + * current function pointers being used and then provide a new set. + */ + +sqlite3_mutex_methods orig_mutex_methods; +sqlite3_mutex_methods wrapped_mutex_methods; + +bool mutex_used_on_watched_thread = false; +PRThread* watched_thread = nullptr; +/** + * Ugly hack to let us figure out what a connection's async thread is. If we + * were MOZILLA_INTERNAL_API and linked as such we could just include + * mozStorageConnection.h and just ask Connection directly. But that turns out + * poorly. + * + * When the thread a mutex is invoked on isn't watched_thread we save it to this + * variable. + */ +nsIThread* last_non_watched_thread = nullptr; + +/** + * Set a flag if the mutex is used on the thread we are watching, but always + * call the real mutex function. + */ +extern "C" void wrapped_MutexEnter(sqlite3_mutex* mutex) { + if (PR_GetCurrentThread() == watched_thread) + mutex_used_on_watched_thread = true; + else + last_non_watched_thread = NS_GetCurrentThread(); + orig_mutex_methods.xMutexEnter(mutex); +} + +extern "C" int wrapped_MutexTry(sqlite3_mutex* mutex) { + if (::PR_GetCurrentThread() == watched_thread) + mutex_used_on_watched_thread = true; + return orig_mutex_methods.xMutexTry(mutex); +} + +/** + * Call to clear the watch state and to set the watching against this thread. + * + * Check |mutex_used_on_watched_thread| to see if the mutex has fired since + * this method was last called. Since we're talking about the current thread, + * there are no race issues to be concerned about + */ +void watch_for_mutex_use_on_this_thread() { + watched_thread = ::PR_GetCurrentThread(); + mutex_used_on_watched_thread = false; +} + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * A horrible hack to figure out what the connection's async thread is. By + * creating a statement and async dispatching we can tell from the mutex who + * is the async thread, PRThread style. Then we map that to an nsIThread. + */ +already_AddRefed get_conn_async_thread(mozIStorageConnection* db) { + // Make sure we are tracking the current thread as the watched thread + watch_for_mutex_use_on_this_thread(); + + // - statement with nothing to bind + nsCOMPtr stmt; + db->CreateAsyncStatement("SELECT 1"_ns, getter_AddRefs(stmt)); + blocking_async_execute(stmt); + stmt->Finalize(); + + nsCOMPtr asyncThread = last_non_watched_thread; + + // Additionally, check that the thread we get as the background thread is the + // same one as the one we report from getInterface. + nsCOMPtr target = do_GetInterface(db); + nsCOMPtr allegedAsyncThread = do_QueryInterface(target); + do_check_eq(allegedAsyncThread, asyncThread); + return asyncThread.forget(); +} + +#endif // storage_test_harness_h__ diff --git a/storage/test/gtest/storage_test_harness.h b/storage/test/gtest/storage_test_harness.h new file mode 100644 index 0000000000..7df951fedb --- /dev/null +++ b/storage/test/gtest/storage_test_harness.h @@ -0,0 +1,206 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef storage_test_harness_h__ +#define storage_test_harness_h__ + +#include "gtest/gtest.h" + +#include "prthread.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsComponentManagerUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsIThread.h" +#include "nsThreadUtils.h" +#include "mozilla/ReentrantMonitor.h" + +#include "mozIStorageService.h" +#include "mozIStorageConnection.h" +#include "mozIStorageStatementCallback.h" +#include "mozIStorageCompletionCallback.h" +#include "mozIStorageBindingParamsArray.h" +#include "mozIStorageBindingParams.h" +#include "mozIStorageAsyncStatement.h" +#include "mozIStorageStatement.h" +#include "mozIStoragePendingStatement.h" +#include "mozIStorageError.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIEventTarget.h" + +#include "sqlite3.h" + +#define do_check_true(aCondition) EXPECT_TRUE(aCondition) + +#define do_check_false(aCondition) EXPECT_FALSE(aCondition) + +#define do_check_success(aResult) do_check_true(NS_SUCCEEDED(aResult)) + +#define do_check_eq(aExpected, aActual) do_check_true(aExpected == aActual) + +#define do_check_ok(aInvoc) do_check_true((aInvoc) == SQLITE_OK) + +already_AddRefed getService(); + +already_AddRefed getMemoryDatabase(); + +already_AddRefed getDatabase( + nsIFile* aDBFile = nullptr, + uint32_t aConnectionFlags = mozIStorageService::CONNECTION_DEFAULT); + +class AsyncStatementSpinner : public mozIStorageStatementCallback, + public mozIStorageCompletionCallback { + public: + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGESTATEMENTCALLBACK + NS_DECL_MOZISTORAGECOMPLETIONCALLBACK + + AsyncStatementSpinner(); + + void SpinUntilCompleted(); + + uint16_t completionReason; + + protected: + virtual ~AsyncStatementSpinner() {} + volatile bool mCompleted; +}; + +#define NS_DECL_ASYNCSTATEMENTSPINNER \ + NS_IMETHOD HandleResult(mozIStorageResultSet* aResultSet) override; + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * Execute an async statement, blocking the main thread until we get the + * callback completion notification. + */ +void blocking_async_execute(mozIStorageBaseStatement* stmt); + +/** + * Invoke AsyncClose on the given connection, blocking the main thread until we + * get the completion notification. + */ +void blocking_async_close(mozIStorageConnection* db); + +//////////////////////////////////////////////////////////////////////////////// +//// Mutex Watching + +/** + * Verify that mozIStorageAsyncStatement's life-cycle never triggers a mutex on + * the caller (generally main) thread. We do this by decorating the sqlite + * mutex logic with our own code that checks what thread it is being invoked on + * and sets a flag if it is invoked on the main thread. We are able to easily + * decorate the SQLite mutex logic because SQLite allows us to retrieve the + * current function pointers being used and then provide a new set. + */ + +extern sqlite3_mutex_methods orig_mutex_methods; +extern sqlite3_mutex_methods wrapped_mutex_methods; + +extern bool mutex_used_on_watched_thread; +extern PRThread* watched_thread; +/** + * Ugly hack to let us figure out what a connection's async thread is. If we + * were MOZILLA_INTERNAL_API and linked as such we could just include + * mozStorageConnection.h and just ask Connection directly. But that turns out + * poorly. + * + * When the thread a mutex is invoked on isn't watched_thread we save it to this + * variable. + */ +extern nsIThread* last_non_watched_thread; + +/** + * Set a flag if the mutex is used on the thread we are watching, but always + * call the real mutex function. + */ +extern "C" void wrapped_MutexEnter(sqlite3_mutex* mutex); + +extern "C" int wrapped_MutexTry(sqlite3_mutex* mutex); + +class HookSqliteMutex { + public: + HookSqliteMutex() { + // We need to initialize and teardown SQLite to get it to set up the + // default mutex handlers for us so we can steal them and wrap them. + do_check_ok(sqlite3_initialize()); + do_check_ok(sqlite3_shutdown()); + do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &orig_mutex_methods)); + do_check_ok( + ::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &wrapped_mutex_methods)); + wrapped_mutex_methods.xMutexEnter = wrapped_MutexEnter; + wrapped_mutex_methods.xMutexTry = wrapped_MutexTry; + do_check_ok(::sqlite3_config(SQLITE_CONFIG_MUTEX, &wrapped_mutex_methods)); + } + + ~HookSqliteMutex() { + do_check_ok(sqlite3_shutdown()); + do_check_ok(::sqlite3_config(SQLITE_CONFIG_MUTEX, &orig_mutex_methods)); + do_check_ok(sqlite3_initialize()); + } +}; + +/** + * Call to clear the watch state and to set the watching against this thread. + * + * Check |mutex_used_on_watched_thread| to see if the mutex has fired since + * this method was last called. Since we're talking about the current thread, + * there are no race issues to be concerned about + */ +void watch_for_mutex_use_on_this_thread(); + +//////////////////////////////////////////////////////////////////////////////// +//// Thread Wedgers + +/** + * A runnable that blocks until code on another thread invokes its unwedge + * method. By dispatching this to a thread you can ensure that no subsequent + * runnables dispatched to the thread will execute until you invoke unwedge. + * + * The wedger is self-dispatching, just construct it with its target. + */ +class ThreadWedger : public mozilla::Runnable { + public: + explicit ThreadWedger(nsIEventTarget* aTarget) + : mozilla::Runnable("ThreadWedger"), + mReentrantMonitor("thread wedger"), + unwedged(false) { + aTarget->Dispatch(this, aTarget->NS_DISPATCH_NORMAL); + } + + NS_IMETHOD Run() override { + mozilla::ReentrantMonitorAutoEnter automon(mReentrantMonitor); + + if (!unwedged) automon.Wait(); + + return NS_OK; + } + + void unwedge() { + mozilla::ReentrantMonitorAutoEnter automon(mReentrantMonitor); + unwedged = true; + automon.Notify(); + } + + private: + mozilla::ReentrantMonitor mReentrantMonitor MOZ_UNANNOTATED; + bool unwedged; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * A horrible hack to figure out what the connection's async thread is. By + * creating a statement and async dispatching we can tell from the mutex who + * is the async thread, PRThread style. Then we map that to an nsIThread. + */ +already_AddRefed get_conn_async_thread(mozIStorageConnection* db); + +#endif // storage_test_harness_h__ diff --git a/storage/test/gtest/test_AsXXX_helpers.cpp b/storage/test/gtest/test_AsXXX_helpers.cpp new file mode 100644 index 0000000000..2320b8383c --- /dev/null +++ b/storage/test/gtest/test_AsXXX_helpers.cpp @@ -0,0 +1,111 @@ +/* + *Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +#include "storage_test_harness.h" +#include "mozIStorageRow.h" +#include "mozIStorageResultSet.h" +#include "nsComponentManagerUtils.h" + +/** + * This file tests AsXXX (AsInt32, AsInt64, ...) helpers. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Event Loop Spinning + +class Spinner : public AsyncStatementSpinner { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_ASYNCSTATEMENTSPINNER + Spinner() {} + + protected: + ~Spinner() override = default; +}; + +NS_IMPL_ISUPPORTS_INHERITED0(Spinner, AsyncStatementSpinner) + +NS_IMETHODIMP +Spinner::HandleResult(mozIStorageResultSet* aResultSet) { + nsCOMPtr row; + do_check_true(NS_SUCCEEDED(aResultSet->GetNextRow(getter_AddRefs(row))) && + row); + + do_check_eq(row->AsInt32(0), 0); + do_check_eq(row->AsInt64(0), 0); + do_check_eq(row->AsDouble(0), 0.0); + + uint32_t len = 100; + do_check_eq(row->AsSharedUTF8String(0, &len), (const char*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(row->AsSharedWString(0, &len), (const char16_t*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(row->AsSharedBlob(0, &len), (const uint8_t*)nullptr); + do_check_eq(len, 0); + + do_check_eq(row->IsNull(0), true); + return NS_OK; +} + +TEST(storage_AsXXX_helpers, NULLFallback) +{ + nsCOMPtr db(getMemoryDatabase()); + + nsCOMPtr stmt; + (void)db->CreateStatement("SELECT NULL"_ns, getter_AddRefs(stmt)); + + nsCOMPtr valueArray = do_QueryInterface(stmt); + do_check_true(valueArray); + + bool hasMore; + do_check_true(NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore); + + do_check_eq(stmt->AsInt32(0), 0); + do_check_eq(stmt->AsInt64(0), 0); + do_check_eq(stmt->AsDouble(0), 0.0); + uint32_t len = 100; + do_check_eq(stmt->AsSharedUTF8String(0, &len), (const char*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(stmt->AsSharedWString(0, &len), (const char16_t*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(stmt->AsSharedBlob(0, &len), (const uint8_t*)nullptr); + do_check_eq(len, 0); + do_check_eq(stmt->IsNull(0), true); + + do_check_eq(valueArray->AsInt32(0), 0); + do_check_eq(valueArray->AsInt64(0), 0); + do_check_eq(valueArray->AsDouble(0), 0.0); + len = 100; + do_check_eq(valueArray->AsSharedUTF8String(0, &len), (const char*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(valueArray->AsSharedWString(0, &len), (const char16_t*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(valueArray->AsSharedBlob(0, &len), (const uint8_t*)nullptr); + do_check_eq(len, 0); + do_check_eq(valueArray->IsNull(0), true); +} + +TEST(storage_AsXXX_helpers, asyncNULLFallback) +{ + nsCOMPtr db(getMemoryDatabase()); + + nsCOMPtr stmt; + (void)db->CreateAsyncStatement("SELECT NULL"_ns, getter_AddRefs(stmt)); + + nsCOMPtr pendingStmt; + do_check_true( + NS_SUCCEEDED(stmt->ExecuteAsync(nullptr, getter_AddRefs(pendingStmt)))); + do_check_true(pendingStmt); + stmt->Finalize(); + RefPtr asyncSpin(new Spinner()); + db->AsyncClose(asyncSpin); + asyncSpin->SpinUntilCompleted(); +} diff --git a/storage/test/gtest/test_StatementCache.cpp b/storage/test/gtest/test_StatementCache.cpp new file mode 100644 index 0000000000..9908a34d79 --- /dev/null +++ b/storage/test/gtest/test_StatementCache.cpp @@ -0,0 +1,132 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "mozilla/Attributes.h" +#include "mozilla/storage/StatementCache.h" +using namespace mozilla::storage; + +/** + * This file test our statement cache in StatementCache.h. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Helpers + +class SyncCache : public StatementCache { + public: + explicit SyncCache(nsCOMPtr& aConnection) + : StatementCache(aConnection) {} +}; + +class AsyncCache : public StatementCache { + public: + explicit AsyncCache(nsCOMPtr& aConnection) + : StatementCache(aConnection) {} +}; + +/** + * Wraps nsCString so we can not implement the same functions twice for each + * type. + */ +class StringWrapper : public nsCString { + public: + MOZ_IMPLICIT StringWrapper(const char* aOther) { this->Assign(aOther); } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Test Functions + +// This is some gtest magic that allows us to parameterize tests by |const +// char[]| and |StringWrapper|. +template +class storage_StatementCache : public ::testing::Test {}; +typedef ::testing::Types TwoStringTypes; + +TYPED_TEST_SUITE(storage_StatementCache, TwoStringTypes); +TYPED_TEST(storage_StatementCache, GetCachedStatement) { + nsCOMPtr db(getMemoryDatabase()); + SyncCache cache(db); + + TypeParam sql = "SELECT * FROM sqlite_master"; + + // Make sure we get a statement back with the right state. + nsCOMPtr stmt = cache.GetCachedStatement(sql); + do_check_true(stmt); + int32_t state; + do_check_success(stmt->GetState(&state)); + do_check_eq(mozIStorageBaseStatement::MOZ_STORAGE_STATEMENT_READY, state); + + // Check to make sure we get the same copy the second time we ask. + nsCOMPtr stmt2 = cache.GetCachedStatement(sql); + do_check_true(stmt2); + do_check_eq(stmt.get(), stmt2.get()); +} + +TYPED_TEST_SUITE(storage_StatementCache, TwoStringTypes); +TYPED_TEST(storage_StatementCache, FinalizeStatements) { + nsCOMPtr db(getMemoryDatabase()); + SyncCache cache(db); + + TypeParam sql = "SELECT * FROM sqlite_master"; + + // Get a statement, and then tell the cache to finalize. + nsCOMPtr stmt = cache.GetCachedStatement(sql); + do_check_true(stmt); + + cache.FinalizeStatements(); + + // We should be in an invalid state at this point. + int32_t state; + do_check_success(stmt->GetState(&state)); + do_check_eq(mozIStorageBaseStatement::MOZ_STORAGE_STATEMENT_INVALID, state); + + // Should be able to close the database now too. + do_check_success(db->Close()); +} + +TYPED_TEST_SUITE(storage_StatementCache, TwoStringTypes); +TYPED_TEST(storage_StatementCache, GetCachedAsyncStatement) { + nsCOMPtr db(getMemoryDatabase()); + AsyncCache cache(db); + + TypeParam sql = "SELECT * FROM sqlite_master"; + + // Make sure we get a statement back with the right state. + nsCOMPtr stmt = cache.GetCachedStatement(sql); + do_check_true(stmt); + int32_t state; + do_check_success(stmt->GetState(&state)); + do_check_eq(mozIStorageBaseStatement::MOZ_STORAGE_STATEMENT_READY, state); + + // Check to make sure we get the same copy the second time we ask. + nsCOMPtr stmt2 = cache.GetCachedStatement(sql); + do_check_true(stmt2); + do_check_eq(stmt.get(), stmt2.get()); +} + +TYPED_TEST_SUITE(storage_StatementCache, TwoStringTypes); +TYPED_TEST(storage_StatementCache, FinalizeAsyncStatements) { + nsCOMPtr db(getMemoryDatabase()); + AsyncCache cache(db); + + TypeParam sql = "SELECT * FROM sqlite_master"; + + // Get a statement, and then tell the cache to finalize. + nsCOMPtr stmt = cache.GetCachedStatement(sql); + do_check_true(stmt); + + cache.FinalizeStatements(); + + // We should be in an invalid state at this point. + int32_t state; + do_check_success(stmt->GetState(&state)); + do_check_eq(mozIStorageBaseStatement::MOZ_STORAGE_STATEMENT_INVALID, state); + + // Should be able to close the database now too. + do_check_success(db->AsyncClose(nullptr)); +} diff --git a/storage/test/gtest/test_asyncStatementExecution_transaction.cpp b/storage/test/gtest/test_asyncStatementExecution_transaction.cpp new file mode 100644 index 0000000000..0e7dec1e8a --- /dev/null +++ b/storage/test/gtest/test_asyncStatementExecution_transaction.cpp @@ -0,0 +1,445 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +#include "storage_test_harness.h" + +#include "mozStorageConnection.h" + +#include "sqlite3.h" + +using namespace mozilla; +using namespace mozilla::storage; + +//////////////////////////////////////////////////////////////////////////////// +//// Helpers + +/** + * Commit hook to detect transactions. + * + * @param aArg + * An integer pointer that will be incremented for each commit. + */ +int commit_hook(void* aArg) { + int* arg = static_cast(aArg); + (*arg)++; + return 0; +} + +/** + * Executes the passed-in statements and checks if a transaction is created. + * When done statements are finalized and database connection is closed. + * + * @param aDB + * The database connection. + * @param aStmts + * Vector of statements. + * @param aStmtsLen + * Number of statements. + * @param aTransactionExpected + * Whether a transaction is expected or not. + */ +void check_transaction(mozIStorageConnection* aDB, + const nsTArray>& aStmts, + bool aTransactionExpected) { + // -- install a transaction commit hook. + int commit = 0; + static_cast(aDB)->setCommitHook(commit_hook, &commit); + + RefPtr asyncSpin(new AsyncStatementSpinner()); + nsCOMPtr asyncPend; + do_check_success( + aDB->ExecuteAsync(aStmts, asyncSpin, getter_AddRefs(asyncPend))); + do_check_true(asyncPend); + + // -- complete the execution + asyncSpin->SpinUntilCompleted(); + + // -- uninstall the transaction commit hook. + static_cast(aDB)->setCommitHook(nullptr); + + // -- check transaction + do_check_eq(aTransactionExpected, !!commit); + + // -- check that only one transaction was created. + if (aTransactionExpected) { + do_check_eq(1, commit); + } + + // -- cleanup + for (uint32_t i = 0; i < aStmts.Length(); ++i) { + aStmts[i]->Finalize(); + } + blocking_async_close(aDB); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +/** + * Test that executing multiple readonly AsyncStatements doesn't create a + * transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleAsyncReadStatements) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt1; + db->CreateAsyncStatement("SELECT * FROM sqlite_master"_ns, + getter_AddRefs(stmt1)); + + nsCOMPtr stmt2; + db->CreateAsyncStatement("SELECT * FROM sqlite_master"_ns, + getter_AddRefs(stmt2)); + + nsTArray> stmts = { + ToRefPtr(std::move(stmt1)), + ToRefPtr(std::move(stmt2)), + }; + + check_transaction(db, stmts.Clone(), false); +} + +/** + * Test that executing multiple readonly Statements doesn't create a + * transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleReadStatements) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt1; + db->CreateStatement("SELECT * FROM sqlite_master"_ns, getter_AddRefs(stmt1)); + + nsCOMPtr stmt2; + db->CreateStatement("SELECT * FROM sqlite_master"_ns, getter_AddRefs(stmt2)); + + nsTArray> stmts = { + ToRefPtr(std::move(stmt1)), + ToRefPtr(std::move(stmt2)), + }; + + check_transaction(db, stmts, false); +} + +/** + * Test that executing multiple AsyncStatements causing writes creates a + * transaction. + */ +TEST(storage_asyncStatementExecution_transaction, + MultipleAsyncReadWriteStatements) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt1; + db->CreateAsyncStatement("SELECT * FROM sqlite_master"_ns, + getter_AddRefs(stmt1)); + + nsCOMPtr stmt2; + db->CreateAsyncStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt2)); + + nsTArray> stmts = { + ToRefPtr(std::move(stmt1)), + ToRefPtr(std::move(stmt2)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing multiple Statements causing writes creates a transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleReadWriteStatements) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt1; + db->CreateStatement("SELECT * FROM sqlite_master"_ns, getter_AddRefs(stmt1)); + + nsCOMPtr stmt2; + db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt2)); + + nsTArray> stmts = { + ToRefPtr(std::move(stmt1)), + ToRefPtr(std::move(stmt2)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing multiple AsyncStatements causing writes creates a + * single transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleAsyncWriteStatements) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt1; + db->CreateAsyncStatement("CREATE TABLE test1 (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt1)); + + nsCOMPtr stmt2; + db->CreateAsyncStatement("CREATE TABLE test2 (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt2)); + + nsTArray> stmts = { + ToRefPtr(std::move(stmt1)), + ToRefPtr(std::move(stmt2)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing multiple Statements causing writes creates a + * single transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleWriteStatements) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt1; + db->CreateStatement("CREATE TABLE test1 (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt1)); + + nsCOMPtr stmt2; + db->CreateStatement("CREATE TABLE test2 (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt2)); + + nsTArray> stmts = { + ToRefPtr(std::move(stmt1)), + ToRefPtr(std::move(stmt2)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing a single read-only AsyncStatement doesn't create a + * transaction. + */ +TEST(storage_asyncStatementExecution_transaction, SingleAsyncReadStatement) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt; + db->CreateAsyncStatement("SELECT * FROM sqlite_master"_ns, + getter_AddRefs(stmt)); + + nsTArray> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, false); +} + +/** + * Test that executing a single read-only Statement doesn't create a + * transaction. + */ +TEST(storage_asyncStatementExecution_transaction, SingleReadStatement) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt; + db->CreateStatement("SELECT * FROM sqlite_master"_ns, getter_AddRefs(stmt)); + + nsTArray> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, false); +} + +/** + * Test that executing a single AsyncStatement causing writes creates a + * transaction. + */ +TEST(storage_asyncStatementExecution_transaction, SingleAsyncWriteStatement) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt; + db->CreateAsyncStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt)); + + nsTArray> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing a single Statement causing writes creates a transaction. + */ +TEST(storage_asyncStatementExecution_transaction, SingleWriteStatement) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt; + db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt)); + + nsTArray> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing a single read-only AsyncStatement with multiple params + * doesn't create a transaction. + */ +TEST(storage_asyncStatementExecution_transaction, + MultipleParamsAsyncReadStatement) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt; + db->CreateAsyncStatement("SELECT :param FROM sqlite_master"_ns, + getter_AddRefs(stmt)); + + // -- bind multiple BindingParams + nsCOMPtr paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (int32_t i = 0; i < 2; i++) { + nsCOMPtr params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName("param"_ns, 1); + paramsArray->AddParams(params); + } + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + + nsTArray> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, false); +} + +/** + * Test that executing a single read-only Statement with multiple params + * doesn't create a transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleParamsReadStatement) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt; + db->CreateStatement("SELECT :param FROM sqlite_master"_ns, + getter_AddRefs(stmt)); + + // -- bind multiple BindingParams + nsCOMPtr paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (int32_t i = 0; i < 2; i++) { + nsCOMPtr params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName("param"_ns, 1); + paramsArray->AddParams(params); + } + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + + nsTArray> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, false); +} + +/** + * Test that executing a single write AsyncStatement with multiple params + * creates a transaction. + */ +TEST(storage_asyncStatementExecution_transaction, + MultipleParamsAsyncWriteStatement) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create a table for writes + nsCOMPtr tableStmt; + db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(tableStmt)); + tableStmt->Execute(); + tableStmt->Finalize(); + + // -- create statements and execute them + nsCOMPtr stmt; + db->CreateAsyncStatement("DELETE FROM test WHERE id = :param"_ns, + getter_AddRefs(stmt)); + + // -- bind multiple BindingParams + nsCOMPtr paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (int32_t i = 0; i < 2; i++) { + nsCOMPtr params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName("param"_ns, 1); + paramsArray->AddParams(params); + } + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + + nsTArray> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing a single write Statement with multiple params + * creates a transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleParamsWriteStatement) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create a table for writes + nsCOMPtr tableStmt; + db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(tableStmt)); + tableStmt->Execute(); + tableStmt->Finalize(); + + // -- create statements and execute them + nsCOMPtr stmt; + db->CreateStatement("DELETE FROM test WHERE id = :param"_ns, + getter_AddRefs(stmt)); + + // -- bind multiple BindingParams + nsCOMPtr paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (int32_t i = 0; i < 2; i++) { + nsCOMPtr params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName("param"_ns, 1); + paramsArray->AddParams(params); + } + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + + nsTArray> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, true); +} diff --git a/storage/test/gtest/test_async_callbacks_with_spun_event_loops.cpp b/storage/test/gtest/test_async_callbacks_with_spun_event_loops.cpp new file mode 100644 index 0000000000..bd437b61ec --- /dev/null +++ b/storage/test/gtest/test_async_callbacks_with_spun_event_loops.cpp @@ -0,0 +1,146 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +#include "storage_test_harness.h" +#include "prthread.h" +#include "nsIInterfaceRequestorUtils.h" +#include "mozilla/Attributes.h" + +#include "sqlite3.h" + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * Spins the events loop for current thread until aCondition is true. + */ +void spin_events_loop_until_true(const bool* const aCondition) { + nsCOMPtr thread(::do_GetCurrentThread()); + nsresult rv = NS_OK; + bool processed = true; + while (!(*aCondition) && NS_SUCCEEDED(rv)) { + rv = thread->ProcessNextEvent(true, &processed); + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageStatementCallback implementation + +class UnownedCallback final : public mozIStorageStatementCallback { + public: + NS_DECL_ISUPPORTS + + // Whether the object has been destroyed. + static bool sAlive; + // Whether the first result was received. + static bool sResult; + // Whether an error was received. + static bool sError; + + explicit UnownedCallback(mozIStorageConnection* aDBConn) + : mDBConn(aDBConn), mCompleted(false) { + sAlive = true; + sResult = false; + sError = false; + } + + private: + ~UnownedCallback() { + sAlive = false; + blocking_async_close(mDBConn); + } + + public: + NS_IMETHOD HandleResult(mozIStorageResultSet* aResultSet) override { + sResult = true; + spin_events_loop_until_true(&mCompleted); + if (!sAlive) { + MOZ_CRASH("The statement callback was destroyed prematurely."); + } + return NS_OK; + } + + NS_IMETHOD HandleError(mozIStorageError* aError) override { + sError = true; + spin_events_loop_until_true(&mCompleted); + if (!sAlive) { + MOZ_CRASH("The statement callback was destroyed prematurely."); + } + return NS_OK; + } + + NS_IMETHOD HandleCompletion(uint16_t aReason) override { + mCompleted = true; + return NS_OK; + } + + protected: + nsCOMPtr mDBConn; + bool mCompleted; +}; + +NS_IMPL_ISUPPORTS(UnownedCallback, mozIStorageStatementCallback) + +bool UnownedCallback::sAlive = false; +bool UnownedCallback::sResult = false; +bool UnownedCallback::sError = false; + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +TEST(storage_async_callbacks_with_spun_event_loops, + SpinEventsLoopInHandleResult) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Create a test table and populate it. + nsCOMPtr stmt; + db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt)); + stmt->Execute(); + stmt->Finalize(); + + db->CreateStatement("INSERT INTO test (id) VALUES (?)"_ns, + getter_AddRefs(stmt)); + for (int32_t i = 0; i < 30; ++i) { + stmt->BindInt32ByIndex(0, i); + stmt->Execute(); + stmt->Reset(); + } + stmt->Finalize(); + + db->CreateStatement("SELECT * FROM test"_ns, getter_AddRefs(stmt)); + nsCOMPtr ps; + do_check_success( + stmt->ExecuteAsync(new UnownedCallback(db), getter_AddRefs(ps))); + stmt->Finalize(); + + spin_events_loop_until_true(&UnownedCallback::sResult); +} + +TEST(storage_async_callbacks_with_spun_event_loops, SpinEventsLoopInHandleError) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Create a test table and populate it. + nsCOMPtr stmt; + db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt)); + stmt->Execute(); + stmt->Finalize(); + + db->CreateStatement("INSERT INTO test (id) VALUES (1)"_ns, + getter_AddRefs(stmt)); + stmt->Execute(); + stmt->Finalize(); + + // This will cause a constraint error. + db->CreateStatement("INSERT INTO test (id) VALUES (1)"_ns, + getter_AddRefs(stmt)); + nsCOMPtr ps; + do_check_success( + stmt->ExecuteAsync(new UnownedCallback(db), getter_AddRefs(ps))); + stmt->Finalize(); + + spin_events_loop_until_true(&UnownedCallback::sError); +} diff --git a/storage/test/gtest/test_binding_params.cpp b/storage/test/gtest/test_binding_params.cpp new file mode 100644 index 0000000000..a9ca1799f9 --- /dev/null +++ b/storage/test/gtest/test_binding_params.cpp @@ -0,0 +1,186 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "mozilla/ArrayUtils.h" +#include "mozStorageHelper.h" + +using namespace mozilla; + +/** + * This file tests binding and reading out string parameters through the + * mozIStorageStatement API. + */ + +TEST(storage_binding_params, ASCIIString) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Create table with a single string column. + (void)db->ExecuteSimpleSQL("CREATE TABLE test (str STRING)"_ns); + + // Create statements to INSERT and SELECT the string. + nsCOMPtr insert, select; + (void)db->CreateStatement("INSERT INTO test (str) VALUES (?1)"_ns, + getter_AddRefs(insert)); + (void)db->CreateStatement("SELECT str FROM test"_ns, getter_AddRefs(select)); + + // Roundtrip a string through the table, and ensure it comes out as expected. + nsAutoCString inserted("I'm an ASCII string"); + { + mozStorageStatementScoper scoper(insert); + bool hasResult; + do_check_true(NS_SUCCEEDED(insert->BindUTF8StringByIndex(0, inserted))); + do_check_true(NS_SUCCEEDED(insert->ExecuteStep(&hasResult))); + do_check_false(hasResult); + } + + nsAutoCString result; + { + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetUTF8String(0, result))); + } + + do_check_true(result == inserted); + + (void)db->ExecuteSimpleSQL("DELETE FROM test"_ns); +} + +TEST(storage_binding_params, CString) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Create table with a single string column. + (void)db->ExecuteSimpleSQL("CREATE TABLE test (str STRING)"_ns); + + // Create statements to INSERT and SELECT the string. + nsCOMPtr insert, select; + (void)db->CreateStatement("INSERT INTO test (str) VALUES (?1)"_ns, + getter_AddRefs(insert)); + (void)db->CreateStatement("SELECT str FROM test"_ns, getter_AddRefs(select)); + + // Roundtrip a string through the table, and ensure it comes out as expected. + static const char sCharArray[] = + "I'm not a \xff\x00\xac\xde\xbb ASCII string!"; + nsAutoCString inserted(sCharArray, ArrayLength(sCharArray) - 1); + do_check_true(inserted.Length() == ArrayLength(sCharArray) - 1); + { + mozStorageStatementScoper scoper(insert); + bool hasResult; + do_check_true(NS_SUCCEEDED(insert->BindUTF8StringByIndex(0, inserted))); + do_check_true(NS_SUCCEEDED(insert->ExecuteStep(&hasResult))); + do_check_false(hasResult); + } + + { + nsAutoCString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetUTF8String(0, result))); + + do_check_true(result == inserted); + } + + (void)db->ExecuteSimpleSQL("DELETE FROM test"_ns); +} + +TEST(storage_binding_params, UTFStrings) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Create table with a single string column. + (void)db->ExecuteSimpleSQL("CREATE TABLE test (str STRING)"_ns); + + // Create statements to INSERT and SELECT the string. + nsCOMPtr insert, select; + (void)db->CreateStatement("INSERT INTO test (str) VALUES (?1)"_ns, + getter_AddRefs(insert)); + (void)db->CreateStatement("SELECT str FROM test"_ns, getter_AddRefs(select)); + + // Roundtrip a UTF8 string through the table, using UTF8 input and output. + static const char sCharArray[] = R"(I'm a ûüâäç UTF8 string!)"; + nsAutoCString insertedUTF8(sCharArray, ArrayLength(sCharArray) - 1); + do_check_true(insertedUTF8.Length() == ArrayLength(sCharArray) - 1); + NS_ConvertUTF8toUTF16 insertedUTF16(insertedUTF8); + do_check_true(insertedUTF8 == NS_ConvertUTF16toUTF8(insertedUTF16)); + { + mozStorageStatementScoper scoper(insert); + bool hasResult; + do_check_true(NS_SUCCEEDED(insert->BindUTF8StringByIndex(0, insertedUTF8))); + do_check_true(NS_SUCCEEDED(insert->ExecuteStep(&hasResult))); + do_check_false(hasResult); + } + + { + nsAutoCString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetUTF8String(0, result))); + + do_check_true(result == insertedUTF8); + } + + // Use UTF8 input and UTF16 output. + { + nsAutoString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetString(0, result))); + + do_check_true(result == insertedUTF16); + } + + (void)db->ExecuteSimpleSQL("DELETE FROM test"_ns); + + // Roundtrip the same string using UTF16 input and UTF8 output. + { + mozStorageStatementScoper scoper(insert); + bool hasResult; + do_check_true(NS_SUCCEEDED(insert->BindStringByIndex(0, insertedUTF16))); + do_check_true(NS_SUCCEEDED(insert->ExecuteStep(&hasResult))); + do_check_false(hasResult); + } + + { + nsAutoCString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetUTF8String(0, result))); + + do_check_true(result == insertedUTF8); + } + + // Use UTF16 input and UTF16 output. + { + nsAutoString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetString(0, result))); + + do_check_true(result == insertedUTF16); + } + + (void)db->ExecuteSimpleSQL("DELETE FROM test"_ns); +} diff --git a/storage/test/gtest/test_deadlock_detector.cpp b/storage/test/gtest/test_deadlock_detector.cpp new file mode 100644 index 0000000000..2679cf26f1 --- /dev/null +++ b/storage/test/gtest/test_deadlock_detector.cpp @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: sw=2 ts=4 et : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Note: This file is essentially a copy of +// xpcom/tests/gtest/TestDeadlockDetector.cpp, but all mutexes were turned into +// SQLiteMutexes. We use #include and some macros to avoid actual source code +// duplication. + +#include "mozilla/CondVar.h" +#include "mozilla/RecursiveMutex.h" +#include "mozilla/ReentrantMonitor.h" +#include "SQLiteMutex.h" + +// We need this one so _gdb_sleep_duration is also in "storage" namespace +#include "mozilla/gtest/MozHelpers.h" + +#include "gtest/gtest.h" + +using namespace mozilla; + +/** + * Helper class to allocate a sqlite3_mutex for our SQLiteMutex. + */ +class TestMutex : public mozilla::storage::SQLiteMutex { + public: + explicit TestMutex(const char* aName) + : mozilla::storage::SQLiteMutex(aName), + mInner(sqlite3_mutex_alloc(SQLITE_MUTEX_FAST)) { + NS_ASSERTION(mInner, "could not allocate a sqlite3_mutex"); + initWithMutex(mInner); + } + + ~TestMutex() { sqlite3_mutex_free(mInner); } + + void Lock() { lock(); } + + void Unlock() { unlock(); } + + private: + sqlite3_mutex* mInner; +}; + +// These are the two macros that differentiate this file from the XPCOM one. +#define MUTEX TestMutex +#define TESTNAME(name) storage_##name + +// Bug 1473531: the test storage_DeadlockDetectorTest.storage_Sanity5DeathTest +// times out on macosx ccov builds +#if defined(XP_MACOSX) && defined(MOZ_CODE_COVERAGE) +# define DISABLE_STORAGE_SANITY5_DEATH_TEST +#endif + +#include "../../../xpcom/tests/gtest/TestDeadlockDetector.cpp" diff --git a/storage/test/gtest/test_file_perms.cpp b/storage/test/gtest/test_file_perms.cpp new file mode 100644 index 0000000000..5e54f082ce --- /dev/null +++ b/storage/test/gtest/test_file_perms.cpp @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" +#include "nsIFile.h" +#include "prio.h" + +/** + * This file tests that the file permissions of the sqlite files match what + * we request they be + */ + +TEST(storage_file_perms, Test) +{ + nsCOMPtr db(getDatabase()); + nsCOMPtr dbFile; + do_check_success(db->GetDatabaseFile(getter_AddRefs(dbFile))); + + uint32_t perms = 0; + do_check_success(dbFile->GetPermissions(&perms)); + + // This reflexts the permissions defined by SQLITE_DEFAULT_FILE_PERMISSIONS in + // third_party/sqlite3/src/Makefile.in and must be kept in sync with that +#ifdef ANDROID + do_check_true(perms == (PR_IRUSR | PR_IWUSR)); +#elif defined(XP_WIN) + do_check_true(perms == (PR_IRUSR | PR_IWUSR | PR_IRGRP | PR_IWGRP | PR_IROTH | + PR_IWOTH)); +#else + do_check_true(perms == (PR_IRUSR | PR_IWUSR | PR_IRGRP | PR_IROTH)); +#endif +} diff --git a/storage/test/gtest/test_interruptSynchronousConnection.cpp b/storage/test/gtest/test_interruptSynchronousConnection.cpp new file mode 100644 index 0000000000..dfa19bc86e --- /dev/null +++ b/storage/test/gtest/test_interruptSynchronousConnection.cpp @@ -0,0 +1,81 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "storage_test_harness.h" + +#include "mozilla/SpinEventLoopUntil.h" + +class SynchronousConnectionInterruptionTest : public ::testing::Test { + protected: + void SetUp() override { + mConnection = + getDatabase(nullptr, mozIStorageService::CONNECTION_INTERRUPTIBLE); + ASSERT_TRUE(mConnection); + + ASSERT_EQ(NS_OK, NS_NewNamedThread("Test Thread", getter_AddRefs(mThread))); + } + + void TearDown() override { + // We might close the database connection early in test cases. + mozilla::Unused << mConnection->Close(); + + ASSERT_EQ(NS_OK, mThread->Shutdown()); + } + + nsCOMPtr mConnection; + + nsCOMPtr mThread; + + bool mDone = false; +}; + +TEST_F(SynchronousConnectionInterruptionTest, + shouldBeAbleToInterruptInfiniteOperation) { + // Delay is modest because we don't want to get interrupted by + // some unrelated hang or memory guard + const uint32_t delayMs = 500; + + ASSERT_EQ(NS_OK, mThread->DelayedDispatch( + NS_NewRunnableFunction( + "InterruptRunnable", + [this]() { + ASSERT_EQ(NS_OK, mConnection->Interrupt()); + mDone = true; + }), + delayMs)); + + const nsCString infiniteQuery = + "WITH RECURSIVE test(n) " + "AS (VALUES(1) UNION ALL SELECT n + 1 FROM test) " + "SELECT t.n FROM test, test AS t;"_ns; + nsCOMPtr stmt; + ASSERT_EQ(NS_OK, + mConnection->CreateStatement(infiniteQuery, getter_AddRefs(stmt))); + ASSERT_EQ(NS_ERROR_ABORT, stmt->Execute()); + ASSERT_EQ(NS_OK, stmt->Finalize()); + + ASSERT_TRUE(mDone); + + ASSERT_EQ(NS_OK, mConnection->Close()); +} + +TEST_F(SynchronousConnectionInterruptionTest, interruptAfterCloseWillFail) { + ASSERT_EQ(NS_OK, mConnection->Close()); + + ASSERT_EQ( + NS_OK, + mThread->Dispatch(NS_NewRunnableFunction("InterruptRunnable", [this]() { + ASSERT_EQ(NS_ERROR_NOT_INITIALIZED, mConnection->Interrupt()); + mDone = true; + }))); + + ASSERT_TRUE(mozilla::SpinEventLoopUntil("interruptAfterCloseWillFail"_ns, + [this]() { return mDone; })); + + ASSERT_EQ(NS_ERROR_NOT_INITIALIZED, mConnection->Close()); +} diff --git a/storage/test/gtest/test_mutex.cpp b/storage/test/gtest/test_mutex.cpp new file mode 100644 index 0000000000..db1a645e09 --- /dev/null +++ b/storage/test/gtest/test_mutex.cpp @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "SQLiteMutex.h" + +using namespace mozilla; +using namespace mozilla::storage; + +/** + * This file test our sqlite3_mutex wrapper in SQLiteMutex.h. + */ + +TEST(storage_mutex, AutoLock) +{ + int lockTypes[] = { + SQLITE_MUTEX_FAST, + SQLITE_MUTEX_RECURSIVE, + }; + for (int lockType : lockTypes) { + // Get our test mutex (we have to allocate a SQLite mutex of the right type + // too!). + SQLiteMutex mutex("TestMutex"); + sqlite3_mutex* inner = sqlite3_mutex_alloc(lockType); + do_check_true(inner); + mutex.initWithMutex(inner); + + // And test that our automatic locking wrapper works as expected. + mutex.assertNotCurrentThreadOwns(); + { + SQLiteMutexAutoLock lockedScope(mutex); + mutex.assertCurrentThreadOwns(); + } + mutex.assertNotCurrentThreadOwns(); + + // Free the wrapped mutex - we don't need it anymore. + sqlite3_mutex_free(inner); + } +} + +TEST(storage_mutex, AutoUnlock) +{ + int lockTypes[] = { + SQLITE_MUTEX_FAST, + SQLITE_MUTEX_RECURSIVE, + }; + for (int lockType : lockTypes) { + // Get our test mutex (we have to allocate a SQLite mutex of the right type + // too!). + SQLiteMutex mutex("TestMutex"); + sqlite3_mutex* inner = sqlite3_mutex_alloc(lockType); + do_check_true(inner); + mutex.initWithMutex(inner); + + // And test that our automatic unlocking wrapper works as expected. + { + SQLiteMutexAutoLock lockedScope(mutex); + + { + SQLiteMutexAutoUnlock unlockedScope(mutex); + mutex.assertNotCurrentThreadOwns(); + } + mutex.assertCurrentThreadOwns(); + } + + // Free the wrapped mutex - we don't need it anymore. + sqlite3_mutex_free(inner); + } +} diff --git a/storage/test/gtest/test_spinningSynchronousClose.cpp b/storage/test/gtest/test_spinningSynchronousClose.cpp new file mode 100644 index 0000000000..81f9bee11a --- /dev/null +++ b/storage/test/gtest/test_spinningSynchronousClose.cpp @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" +#include "prinrval.h" + +using namespace mozilla; + +/** + * Helper to verify that the event loop was spun. As long as this is dispatched + * prior to a call to Close()/SpinningSynchronousClose() we are guaranteed this + * will be run if the event loop is spun to perform a close. This is because + * SpinningSynchronousClose must spin the event loop to realize the close + * completed and our runnable will already be enqueued and therefore run before + * the AsyncCloseConnection's callback. Note that this invariant may be + * violated if our runnables end up in different queues thanks to Quantum + * changes, so this test may need to be updated if the close dispatch changes. + */ +class CompletionRunnable final : public Runnable { + public: + explicit CompletionRunnable() + : Runnable("CompletionRunnable"), mDone(false) {} + + NS_IMETHOD Run() override { + mDone = true; + return NS_OK; + } + + bool mDone; +}; + +// Can only run in optimized builds, or it would assert. +#ifndef DEBUG +TEST(storage_spinningSynchronousClose, CloseOnAsync) +{ + nsCOMPtr db(getMemoryDatabase()); + // Run an async statement. + nsCOMPtr stmt; + do_check_success(db->CreateAsyncStatement( + "CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, getter_AddRefs(stmt))); + nsCOMPtr p; + do_check_success(stmt->ExecuteAsync(nullptr, getter_AddRefs(p))); + do_check_success(stmt->Finalize()); + + // Wrongly use Close() instead of AsyncClose(). + RefPtr event = new CompletionRunnable(); + NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); + do_check_false(NS_SUCCEEDED(db->Close())); + do_check_true(event->mDone); +} +#endif + +TEST(storage_spinningSynchronousClose, spinningSynchronousCloseOnAsync) +{ + nsCOMPtr db(getMemoryDatabase()); + // Run an async statement. + nsCOMPtr stmt; + do_check_success(db->CreateAsyncStatement( + "CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, getter_AddRefs(stmt))); + nsCOMPtr p; + do_check_success(stmt->ExecuteAsync(nullptr, getter_AddRefs(p))); + do_check_success(stmt->Finalize()); + + // Use the spinning close API. + RefPtr event = new CompletionRunnable(); + NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); + do_check_success(db->SpinningSynchronousClose()); + do_check_true(event->mDone); +} diff --git a/storage/test/gtest/test_statement_scoper.cpp b/storage/test/gtest/test_statement_scoper.cpp new file mode 100644 index 0000000000..3327b0ff6b --- /dev/null +++ b/storage/test/gtest/test_statement_scoper.cpp @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "mozStorageHelper.h" + +/** + * This file test our statement scoper in mozStorageHelper.h. + */ + +TEST(storage_statement_scoper, automatic_reset) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Need to create a table to populate sqlite_master with an entry. + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + + nsCOMPtr stmt; + (void)db->CreateStatement("SELECT * FROM sqlite_master"_ns, + getter_AddRefs(stmt)); + + // Reality check - make sure we start off in the right state. + int32_t state = -1; + (void)stmt->GetState(&state); + do_check_true(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_READY); + + // Start executing the statement, which will put it into an executing state. + { + mozStorageStatementScoper scoper(stmt); + bool hasMore; + do_check_true(NS_SUCCEEDED(stmt->ExecuteStep(&hasMore))); + + // Reality check that we are executing. + state = -1; + (void)stmt->GetState(&state); + do_check_true(state == + mozIStorageStatement::MOZ_STORAGE_STATEMENT_EXECUTING); + } + + // And we should be ready again. + state = -1; + (void)stmt->GetState(&state); + do_check_true(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_READY); +} + +TEST(storage_statement_scoper, Abandon) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Need to create a table to populate sqlite_master with an entry. + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + + nsCOMPtr stmt; + (void)db->CreateStatement("SELECT * FROM sqlite_master"_ns, + getter_AddRefs(stmt)); + + // Reality check - make sure we start off in the right state. + int32_t state = -1; + (void)stmt->GetState(&state); + do_check_true(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_READY); + + // Start executing the statement, which will put it into an executing state. + { + mozStorageStatementScoper scoper(stmt); + bool hasMore; + do_check_true(NS_SUCCEEDED(stmt->ExecuteStep(&hasMore))); + + // Reality check that we are executing. + state = -1; + (void)stmt->GetState(&state); + do_check_true(state == + mozIStorageStatement::MOZ_STORAGE_STATEMENT_EXECUTING); + + // And call Abandon. We should not reset now when we fall out of scope. + scoper.Abandon(); + } + + // And we should still be executing. + state = -1; + (void)stmt->GetState(&state); + do_check_true(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_EXECUTING); +} diff --git a/storage/test/gtest/test_transaction_helper.cpp b/storage/test/gtest/test_transaction_helper.cpp new file mode 100644 index 0000000000..d11f631967 --- /dev/null +++ b/storage/test/gtest/test_transaction_helper.cpp @@ -0,0 +1,184 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "mozStorageHelper.h" +#include "mozStorageConnection.h" + +using namespace mozilla; +using namespace mozilla::storage; + +bool has_transaction(mozIStorageConnection* aDB) { + return !(static_cast(aDB)->getAutocommit()); +} + +/** + * This file tests our Transaction helper in mozStorageHelper.h. + */ + +TEST(storage_transaction_helper, Commit) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Create a table in a transaction, call Commit, and make sure that it does + // exists after the transaction falls out of scope. + { + mozStorageTransaction transaction(db, false); + do_check_success(transaction.Start()); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + (void)transaction.Commit(); + } + do_check_false(has_transaction(db)); + + bool exists = false; + (void)db->TableExists("test"_ns, &exists); + do_check_true(exists); +} + +TEST(storage_transaction_helper, Rollback) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Create a table in a transaction, call Rollback, and make sure that it does + // not exists after the transaction falls out of scope. + { + mozStorageTransaction transaction(db, true); + do_check_success(transaction.Start()); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + (void)transaction.Rollback(); + } + do_check_false(has_transaction(db)); + + bool exists = true; + (void)db->TableExists("test"_ns, &exists); + do_check_false(exists); +} + +TEST(storage_transaction_helper, AutoCommit) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Create a table in a transaction, and make sure that it exists after the + // transaction falls out of scope. This means the Commit was successful. + { + mozStorageTransaction transaction(db, true); + do_check_success(transaction.Start()); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + } + do_check_false(has_transaction(db)); + + bool exists = false; + (void)db->TableExists("test"_ns, &exists); + do_check_true(exists); +} + +TEST(storage_transaction_helper, AutoRollback) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Create a table in a transaction, and make sure that it does not exists + // after the transaction falls out of scope. This means the Rollback was + // successful. + { + mozStorageTransaction transaction(db, false); + do_check_success(transaction.Start()); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + } + do_check_false(has_transaction(db)); + + bool exists = true; + (void)db->TableExists("test"_ns, &exists); + do_check_false(exists); +} + +TEST(storage_transaction_helper, null_database_connection) +{ + // We permit the use of the Transaction helper when passing a null database + // in, so we need to make sure this still works without crashing. + mozStorageTransaction transaction(nullptr, false); + do_check_success(transaction.Start()); + do_check_true(NS_SUCCEEDED(transaction.Commit())); + do_check_true(NS_SUCCEEDED(transaction.Rollback())); +} + +TEST(storage_transaction_helper, async_Commit) +{ + HookSqliteMutex hook; + + nsCOMPtr db(getMemoryDatabase()); + + // -- wedge the thread + nsCOMPtr target(get_conn_async_thread(db)); + do_check_true(target); + RefPtr wedger(new ThreadWedger(target)); + + { + mozStorageTransaction transaction( + db, false, mozIStorageConnection::TRANSACTION_DEFERRED, true); + do_check_success(transaction.Start()); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + (void)transaction.Commit(); + } + do_check_true(has_transaction(db)); + + // -- unwedge the async thread + wedger->unwedge(); + + // Ensure the transaction has done its job by enqueueing an async execution. + nsCOMPtr stmt; + (void)db->CreateAsyncStatement("SELECT NULL"_ns, getter_AddRefs(stmt)); + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(has_transaction(db)); + bool exists = false; + (void)db->TableExists("test"_ns, &exists); + do_check_true(exists); + + blocking_async_close(db); +} + +TEST(storage_transaction_helper, Nesting) +{ + nsCOMPtr db(getMemoryDatabase()); + + { + mozStorageTransaction transaction(db, false); + do_check_success(transaction.Start()); + do_check_true(has_transaction(db)); + do_check_success( + db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns)); + + { + mozStorageTransaction nestedTransaction(db, false); + do_check_success(nestedTransaction.Start()); + do_check_true(has_transaction(db)); + do_check_success(db->ExecuteSimpleSQL( + "CREATE TABLE nested_test (id INTEGER PRIMARY KEY)"_ns)); + +#ifndef MOZ_DIAGNOSTIC_ASSERT_ENABLED + do_check_true(transaction.Commit() == NS_ERROR_NOT_AVAILABLE); + do_check_true(transaction.Rollback() == NS_ERROR_NOT_AVAILABLE); +#endif + } + + bool exists = false; + do_check_success(db->TableExists("nested_test"_ns, &exists)); + do_check_false(exists); + + (void)transaction.Commit(); + } + do_check_false(has_transaction(db)); + + bool exists = false; + do_check_success(db->TableExists("test"_ns, &exists)); + do_check_true(exists); +} diff --git a/storage/test/gtest/test_true_async.cpp b/storage/test/gtest/test_true_async.cpp new file mode 100644 index 0000000000..3b54b73b3a --- /dev/null +++ b/storage/test/gtest/test_true_async.cpp @@ -0,0 +1,161 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +TEST(storage_true_async, TrueAsyncStatement) +{ + HookSqliteMutex hook; + + nsCOMPtr db(getMemoryDatabase()); + + // Start watching for forbidden mutex usage. + watch_for_mutex_use_on_this_thread(); + + // - statement with nothing to bind + nsCOMPtr stmt; + db->CreateAsyncStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt)); + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(mutex_used_on_watched_thread); + + // - statement with something to bind ordinally + db->CreateAsyncStatement("INSERT INTO test (id) VALUES (?)"_ns, + getter_AddRefs(stmt)); + stmt->BindInt32ByIndex(0, 1); + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(mutex_used_on_watched_thread); + + // - statement with something to bind by name + db->CreateAsyncStatement("INSERT INTO test (id) VALUES (:id)"_ns, + getter_AddRefs(stmt)); + nsCOMPtr paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + nsCOMPtr params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName("id"_ns, 2); + paramsArray->AddParams(params); + params = nullptr; + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(mutex_used_on_watched_thread); + + // - now, make sure creating a sync statement does trigger our guard. + // (If this doesn't happen, our test is bunk and it's important to know that.) + nsCOMPtr syncStmt; + db->CreateStatement("SELECT * FROM test"_ns, getter_AddRefs(syncStmt)); + syncStmt->Finalize(); + do_check_true(mutex_used_on_watched_thread); + + blocking_async_close(db); +} + +/** + * Test that cancellation before a statement is run successfully stops the + * statement from executing. + */ +TEST(storage_true_async, AsyncCancellation) +{ + HookSqliteMutex hook; + + nsCOMPtr db(getMemoryDatabase()); + + // -- wedge the thread + nsCOMPtr target(get_conn_async_thread(db)); + do_check_true(target); + RefPtr wedger(new ThreadWedger(target)); + + // -- create statements and cancel them + // - async + nsCOMPtr asyncStmt; + db->CreateAsyncStatement( + "CREATE TABLE asyncTable (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(asyncStmt)); + + RefPtr asyncSpin(new AsyncStatementSpinner()); + nsCOMPtr asyncPend; + (void)asyncStmt->ExecuteAsync(asyncSpin, getter_AddRefs(asyncPend)); + do_check_true(asyncPend); + asyncPend->Cancel(); + + // - sync + nsCOMPtr syncStmt; + db->CreateStatement("CREATE TABLE syncTable (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(syncStmt)); + + RefPtr syncSpin(new AsyncStatementSpinner()); + nsCOMPtr syncPend; + (void)syncStmt->ExecuteAsync(syncSpin, getter_AddRefs(syncPend)); + do_check_true(syncPend); + syncPend->Cancel(); + + // -- unwedge the async thread + wedger->unwedge(); + + // -- verify that both statements report they were canceled + asyncSpin->SpinUntilCompleted(); + do_check_true(asyncSpin->completionReason == + mozIStorageStatementCallback::REASON_CANCELED); + + syncSpin->SpinUntilCompleted(); + do_check_true(syncSpin->completionReason == + mozIStorageStatementCallback::REASON_CANCELED); + + // -- verify that neither statement constructed their tables + nsresult rv; + bool exists; + rv = db->TableExists("asyncTable"_ns, &exists); + do_check_true(rv == NS_OK); + do_check_false(exists); + rv = db->TableExists("syncTable"_ns, &exists); + do_check_true(rv == NS_OK); + do_check_false(exists); + + // -- cleanup + asyncStmt->Finalize(); + syncStmt->Finalize(); + blocking_async_close(db); +} + +/** + * Test that the destructor for an asynchronous statement which has a + * sqlite3_stmt will dispatch that statement to the async thread for + * finalization rather than trying to finalize it on the main thread + * (and thereby running afoul of our mutex use detector). + */ +TEST(storage_true_async, AsyncDestructorFinalizesOnAsyncThread) +{ + HookSqliteMutex hook; + + nsCOMPtr db(getMemoryDatabase()); + watch_for_mutex_use_on_this_thread(); + + // -- create an async statement + nsCOMPtr stmt; + db->CreateAsyncStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt)); + + // -- execute it so it gets a sqlite3_stmt that needs to be finalized + blocking_async_execute(stmt); + do_check_false(mutex_used_on_watched_thread); + + // -- forget our reference + stmt = nullptr; + + // -- verify the mutex was not touched + do_check_false(mutex_used_on_watched_thread); + + // -- make sure the statement actually gets finalized / cleanup + // the close will assert if we failed to finalize! + blocking_async_close(db); +} diff --git a/storage/test/gtest/test_unlock_notify.cpp b/storage/test/gtest/test_unlock_notify.cpp new file mode 100644 index 0000000000..93cc5b09a8 --- /dev/null +++ b/storage/test/gtest/test_unlock_notify.cpp @@ -0,0 +1,234 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim set:sw=2 ts=2 et lcs=trail\:.,tab\:>~ : */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "mozilla/ReentrantMonitor.h" +#include "nsThreadUtils.h" +#include "mozIStorageStatement.h" + +/** + * This file tests that our implementation around sqlite3_unlock_notify works + * as expected. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Helpers + +enum State { STARTING, WRITE_LOCK, READ_LOCK, TEST_DONE }; + +class DatabaseLocker : public mozilla::Runnable { + public: + explicit DatabaseLocker(const char* aSQL, nsIFile* aDBFile = nullptr) + : mozilla::Runnable("DatabaseLocker"), + monitor("DatabaseLocker::monitor"), + mSQL(aSQL), + mState(STARTING), + mDBFile(aDBFile) {} + + void RunInBackground() { + (void)NS_NewNamedThread("DatabaseLocker", getter_AddRefs(mThread)); + do_check_true(mThread); + + do_check_success(mThread->Dispatch(this, NS_DISPATCH_NORMAL)); + } + + void Shutdown() { + if (mThread) { + mThread->Shutdown(); + } + } + + NS_IMETHOD Run() override { + mozilla::ReentrantMonitorAutoEnter lock(monitor); + + nsCOMPtr db(getDatabase(mDBFile)); + + nsCString sql(mSQL); + nsCOMPtr stmt; + do_check_success(db->CreateStatement(sql, getter_AddRefs(stmt))); + + bool hasResult; + do_check_success(stmt->ExecuteStep(&hasResult)); + + Notify(WRITE_LOCK); + WaitFor(TEST_DONE); + + return NS_OK; + } + + void WaitFor(State aState) { + monitor.AssertCurrentThreadIn(); + while (mState != aState) { + do_check_success(monitor.Wait()); + } + } + + void Notify(State aState) { + monitor.AssertCurrentThreadIn(); + mState = aState; + do_check_success(monitor.Notify()); + } + + mozilla::ReentrantMonitor monitor MOZ_UNANNOTATED; + + protected: + nsCOMPtr mThread; + const char* const mSQL; + State mState; + nsCOMPtr mDBFile; +}; + +class DatabaseTester : public DatabaseLocker { + public: + DatabaseTester(mozIStorageConnection* aConnection, const char* aSQL) + : DatabaseLocker(aSQL), mConnection(aConnection) {} + + NS_IMETHOD Run() override { + mozilla::ReentrantMonitorAutoEnter lock(monitor); + WaitFor(READ_LOCK); + + nsCString sql(mSQL); + nsCOMPtr stmt; + do_check_success(mConnection->CreateStatement(sql, getter_AddRefs(stmt))); + + bool hasResult; + nsresult rv = stmt->ExecuteStep(&hasResult); + do_check_eq(rv, NS_ERROR_FILE_IS_LOCKED); + + // Finalize our statement and null out our connection before notifying to + // ensure that we close on the proper thread. + rv = stmt->Finalize(); + do_check_eq(rv, NS_ERROR_FILE_IS_LOCKED); + mConnection = nullptr; + + Notify(TEST_DONE); + + return NS_OK; + } + + private: + nsCOMPtr mConnection; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Test Functions + +void setup() { + nsCOMPtr db(getDatabase()); + + // Create and populate a dummy table. + nsresult rv = db->ExecuteSimpleSQL(nsLiteralCString( + "CREATE TABLE test (id INTEGER PRIMARY KEY, data STRING)")); + do_check_success(rv); + rv = db->ExecuteSimpleSQL("INSERT INTO test (data) VALUES ('foo')"_ns); + do_check_success(rv); + rv = db->ExecuteSimpleSQL("INSERT INTO test (data) VALUES ('bar')"_ns); + do_check_success(rv); + rv = + db->ExecuteSimpleSQL("CREATE UNIQUE INDEX unique_data ON test (data)"_ns); + do_check_success(rv); +} + +void test_step_locked_does_not_block_main_thread() { + nsCOMPtr db(getDatabase()); + + // Need to prepare our statement ahead of time so we make sure to only test + // step and not prepare. + nsCOMPtr stmt; + nsresult rv = db->CreateStatement( + "INSERT INTO test (data) VALUES ('test1')"_ns, getter_AddRefs(stmt)); + do_check_success(rv); + + nsCOMPtr dbFile; + db->GetDatabaseFile(getter_AddRefs(dbFile)); + RefPtr locker( + new DatabaseLocker("SELECT * FROM test", dbFile)); + do_check_true(locker); + { + mozilla::ReentrantMonitorAutoEnter lock(locker->monitor); + locker->RunInBackground(); + + // Wait for the locker to notify us that it has locked the database + // properly. + locker->WaitFor(WRITE_LOCK); + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + do_check_eq(rv, NS_ERROR_FILE_IS_LOCKED); + + locker->Notify(TEST_DONE); + } + locker->Shutdown(); +} + +void test_drop_index_does_not_loop() { + nsCOMPtr db(getDatabase()); + + // Need to prepare our statement ahead of time so we make sure to only test + // step and not prepare. + nsCOMPtr stmt; + nsresult rv = + db->CreateStatement("SELECT * FROM test"_ns, getter_AddRefs(stmt)); + do_check_success(rv); + + RefPtr tester = + new DatabaseTester(db, "DROP INDEX unique_data"); + do_check_true(tester); + { + mozilla::ReentrantMonitorAutoEnter lock(tester->monitor); + tester->RunInBackground(); + + // Hold a read lock on the database, and then let the tester try to execute. + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + do_check_success(rv); + do_check_true(hasResult); + tester->Notify(READ_LOCK); + + // Make sure the tester finishes its test before we move on. + tester->WaitFor(TEST_DONE); + } + tester->Shutdown(); +} + +void test_drop_table_does_not_loop() { + nsCOMPtr db(getDatabase()); + + // Need to prepare our statement ahead of time so we make sure to only test + // step and not prepare. + nsCOMPtr stmt; + nsresult rv = + db->CreateStatement("SELECT * FROM test"_ns, getter_AddRefs(stmt)); + do_check_success(rv); + + RefPtr tester(new DatabaseTester(db, "DROP TABLE test")); + do_check_true(tester); + { + mozilla::ReentrantMonitorAutoEnter lock(tester->monitor); + tester->RunInBackground(); + + // Hold a read lock on the database, and then let the tester try to execute. + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + do_check_success(rv); + do_check_true(hasResult); + tester->Notify(READ_LOCK); + + // Make sure the tester finishes its test before we move on. + tester->WaitFor(TEST_DONE); + } + tester->Shutdown(); +} + +TEST(storage_unlock_notify, Test) +{ + // These must execute in order. + setup(); + test_step_locked_does_not_block_main_thread(); + test_drop_index_does_not_loop(); + test_drop_table_does_not_loop(); +} -- cgit v1.2.3