summaryrefslogtreecommitdiffstats
path: root/storage/test
diff options
context:
space:
mode:
Diffstat (limited to 'storage/test')
-rw-r--r--storage/test/gtest/moz.build40
-rw-r--r--storage/test/gtest/storage_test_harness.cpp244
-rw-r--r--storage/test/gtest/storage_test_harness.h223
-rw-r--r--storage/test/gtest/test_AsXXX_helpers.cpp111
-rw-r--r--storage/test/gtest/test_StatementCache.cpp132
-rw-r--r--storage/test/gtest/test_asyncStatementExecution_transaction.cpp445
-rw-r--r--storage/test/gtest/test_async_callbacks_with_spun_event_loops.cpp146
-rw-r--r--storage/test/gtest/test_async_thread_naming.cpp230
-rw-r--r--storage/test/gtest/test_binding_params.cpp186
-rw-r--r--storage/test/gtest/test_deadlock_detector.cpp56
-rw-r--r--storage/test/gtest/test_file_perms.cpp35
-rw-r--r--storage/test/gtest/test_interruptSynchronousConnection.cpp81
-rw-r--r--storage/test/gtest/test_mutex.cpp73
-rw-r--r--storage/test/gtest/test_spinningSynchronousClose.cpp72
-rw-r--r--storage/test/gtest/test_statement_scoper.cpp86
-rw-r--r--storage/test/gtest/test_transaction_helper.cpp184
-rw-r--r--storage/test/gtest/test_true_async.cpp161
-rw-r--r--storage/test/gtest/test_unlock_notify.cpp234
-rw-r--r--storage/test/moz.build13
-rw-r--r--storage/test/unit/VacuumParticipant.sys.mjs109
-rw-r--r--storage/test/unit/baddataDB.sqlitebin0 -> 8192 bytes
-rw-r--r--storage/test/unit/corruptDB.sqlitebin0 -> 32772 bytes
-rw-r--r--storage/test/unit/fakeDB.sqlite1
-rw-r--r--storage/test/unit/goodDB.sqlitebin0 -> 8192 bytes
-rw-r--r--storage/test/unit/head_storage.js412
-rw-r--r--storage/test/unit/locale_collation.txt174
-rw-r--r--storage/test/unit/test_bug-365166.js23
-rw-r--r--storage/test/unit/test_bug-393952.js35
-rw-r--r--storage/test/unit/test_bug-429521.js49
-rw-r--r--storage/test/unit/test_bug-444233.js54
-rw-r--r--storage/test/unit/test_cache_size.js73
-rw-r--r--storage/test/unit/test_chunk_growth.js51
-rw-r--r--storage/test/unit/test_connection_asyncClose.js128
-rw-r--r--storage/test/unit/test_connection_executeAsync.js175
-rw-r--r--storage/test/unit/test_connection_executeSimpleSQLAsync.js94
-rw-r--r--storage/test/unit/test_connection_failsafe_close.js35
-rw-r--r--storage/test/unit/test_connection_interrupt.js125
-rw-r--r--storage/test/unit/test_connection_online_backup.js211
-rw-r--r--storage/test/unit/test_default_journal_size_limit.js44
-rw-r--r--storage/test/unit/test_js_helpers.js150
-rw-r--r--storage/test/unit/test_levenshtein.js66
-rw-r--r--storage/test/unit/test_like.js199
-rw-r--r--storage/test/unit/test_like_escape.js72
-rw-r--r--storage/test/unit/test_locale_collation.js291
-rw-r--r--storage/test/unit/test_minimizeMemory.js23
-rw-r--r--storage/test/unit/test_page_size_is_32k.js31
-rw-r--r--storage/test/unit/test_persist_journal.js89
-rw-r--r--storage/test/unit/test_readonly-immutable-nolock_vfs.js47
-rw-r--r--storage/test/unit/test_retry_on_busy.js199
-rw-r--r--storage/test/unit/test_sqlite_secure_delete.js78
-rw-r--r--storage/test/unit/test_statement_executeAsync.js1045
-rw-r--r--storage/test/unit/test_statement_wrapper_automatically.js166
-rw-r--r--storage/test/unit/test_storage_connection.js997
-rw-r--r--storage/test/unit/test_storage_ext.js84
-rw-r--r--storage/test/unit/test_storage_ext_fts3.js91
-rw-r--r--storage/test/unit/test_storage_ext_fts5.js122
-rw-r--r--storage/test/unit/test_storage_function.js92
-rw-r--r--storage/test/unit/test_storage_progresshandler.js112
-rw-r--r--storage/test/unit/test_storage_service.js267
-rw-r--r--storage/test/unit/test_storage_service_special.js48
-rw-r--r--storage/test/unit/test_storage_service_unshared.js46
-rw-r--r--storage/test/unit/test_storage_statement.js183
-rw-r--r--storage/test/unit/test_storage_value_array.js194
-rw-r--r--storage/test/unit/test_unicode.js91
-rw-r--r--storage/test/unit/test_vacuum.js372
-rw-r--r--storage/test/unit/xpcshell.toml96
66 files changed, 9796 insertions, 0 deletions
diff --git a/storage/test/gtest/moz.build b/storage/test/gtest/moz.build
new file mode 100644
index 0000000000..db15c440f8
--- /dev/null
+++ b/storage/test/gtest/moz.build
@@ -0,0 +1,40 @@
+# -*- 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_async_thread_naming.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..29caa1792b
--- /dev/null
+++ b/storage/test/gtest/storage_test_harness.cpp
@@ -0,0 +1,244 @@
+/* -*- 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<mozIStorageService> getService() {
+ nsCOMPtr<mozIStorageService> ss =
+ do_CreateInstance("@mozilla.org/storage/service;1");
+ do_check_true(ss);
+ return ss.forget();
+}
+
+already_AddRefed<mozIStorageConnection> getMemoryDatabase() {
+ nsCOMPtr<mozIStorageService> ss = getService();
+ nsCOMPtr<mozIStorageConnection> conn;
+ nsresult rv = ss->OpenSpecialDatabase(
+ kMozStorageMemoryStorageKey, VoidCString(),
+ mozIStorageService::CONNECTION_DEFAULT, getter_AddRefs(conn));
+ do_check_success(rv);
+ return conn.forget();
+}
+
+already_AddRefed<mozIStorageConnection> getDatabase(nsIFile* aDBFile,
+ uint32_t aConnectionFlags) {
+ nsCOMPtr<nsIFile> 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<mozIStorageService> ss = getService();
+ nsCOMPtr<mozIStorageConnection> 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<nsIThread> 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;
+
+NS_IMPL_ISUPPORTS(AsyncCompletionSpinner, mozIStorageCompletionCallback)
+
+AsyncCompletionSpinner::AsyncCompletionSpinner()
+ : mCompletionReason(NS_OK), mCompleted(false) {}
+
+NS_IMETHODIMP
+AsyncCompletionSpinner::Complete(nsresult reason, nsISupports* value) {
+ mCompleted = true;
+ mCompletionReason = reason;
+ mCompletionValue = value;
+ return NS_OK;
+}
+
+void AsyncCompletionSpinner::SpinUntilCompleted() {
+ nsCOMPtr<nsIThread> thread(::do_GetCurrentThread());
+ nsresult rv = NS_OK;
+ bool processed = true;
+ while (!mCompleted && NS_SUCCEEDED(rv)) {
+ rv = thread->ProcessNextEvent(true, &processed);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Async Helpers
+
+/**
+ * Execute an async statement, blocking the main thread until we get the
+ * callback completion notification.
+ */
+void blocking_async_execute(mozIStorageBaseStatement* stmt) {
+ RefPtr<AsyncStatementSpinner> spinner(new AsyncStatementSpinner());
+
+ nsCOMPtr<mozIStoragePendingStatement> 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<AsyncStatementSpinner> 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<nsIThread> 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<mozIStorageAsyncStatement> stmt;
+ db->CreateAsyncStatement("SELECT 1"_ns, getter_AddRefs(stmt));
+ blocking_async_execute(stmt);
+ stmt->Finalize();
+
+ nsCOMPtr<nsIThread> 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<nsIEventTarget> target = do_GetInterface(db);
+ nsCOMPtr<nsIThread> 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..6f29dc43ad
--- /dev/null
+++ b/storage/test/gtest/storage_test_harness.h
@@ -0,0 +1,223 @@
+/* -*- 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<mozIStorageService> getService();
+
+already_AddRefed<mozIStorageConnection> getMemoryDatabase();
+
+already_AddRefed<mozIStorageConnection> 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() = default;
+ volatile bool mCompleted;
+};
+
+class AsyncCompletionSpinner : public mozIStorageCompletionCallback {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGECOMPLETIONCALLBACK
+
+ AsyncCompletionSpinner();
+
+ void SpinUntilCompleted();
+
+ nsresult mCompletionReason;
+ nsCOMPtr<nsISupports> mCompletionValue;
+
+ protected:
+ virtual ~AsyncCompletionSpinner() = default;
+ 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<nsIThread> 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<mozIStorageRow> 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<mozIStorageConnection> db(getMemoryDatabase());
+
+ nsCOMPtr<mozIStorageStatement> stmt;
+ (void)db->CreateStatement("SELECT NULL"_ns, getter_AddRefs(stmt));
+
+ nsCOMPtr<mozIStorageValueArray> 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<mozIStorageConnection> db(getMemoryDatabase());
+
+ nsCOMPtr<mozIStorageAsyncStatement> stmt;
+ (void)db->CreateAsyncStatement("SELECT NULL"_ns, getter_AddRefs(stmt));
+
+ nsCOMPtr<mozIStoragePendingStatement> pendingStmt;
+ do_check_true(
+ NS_SUCCEEDED(stmt->ExecuteAsync(nullptr, getter_AddRefs(pendingStmt))));
+ do_check_true(pendingStmt);
+ stmt->Finalize();
+ RefPtr<Spinner> 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<mozIStorageStatement> {
+ public:
+ explicit SyncCache(nsCOMPtr<mozIStorageConnection>& aConnection)
+ : StatementCache<mozIStorageStatement>(aConnection) {}
+};
+
+class AsyncCache : public StatementCache<mozIStorageAsyncStatement> {
+ public:
+ explicit AsyncCache(nsCOMPtr<mozIStorageConnection>& aConnection)
+ : StatementCache<mozIStorageAsyncStatement>(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 <typename T>
+class storage_StatementCache : public ::testing::Test {};
+typedef ::testing::Types<const char[], StringWrapper> TwoStringTypes;
+
+TYPED_TEST_SUITE(storage_StatementCache, TwoStringTypes);
+TYPED_TEST(storage_StatementCache, GetCachedStatement) {
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+ SyncCache cache(db);
+
+ TypeParam sql = "SELECT * FROM sqlite_master";
+
+ // Make sure we get a statement back with the right state.
+ nsCOMPtr<mozIStorageStatement> 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<mozIStorageStatement> 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<mozIStorageConnection> db(getMemoryDatabase());
+ SyncCache cache(db);
+
+ TypeParam sql = "SELECT * FROM sqlite_master";
+
+ // Get a statement, and then tell the cache to finalize.
+ nsCOMPtr<mozIStorageStatement> 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<mozIStorageConnection> db(getMemoryDatabase());
+ AsyncCache cache(db);
+
+ TypeParam sql = "SELECT * FROM sqlite_master";
+
+ // Make sure we get a statement back with the right state.
+ nsCOMPtr<mozIStorageAsyncStatement> 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<mozIStorageAsyncStatement> 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<mozIStorageConnection> db(getMemoryDatabase());
+ AsyncCache cache(db);
+
+ TypeParam sql = "SELECT * FROM sqlite_master";
+
+ // Get a statement, and then tell the cache to finalize.
+ nsCOMPtr<mozIStorageAsyncStatement> 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<int*>(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<RefPtr<mozIStorageBaseStatement>>& aStmts,
+ bool aTransactionExpected) {
+ // -- install a transaction commit hook.
+ int commit = 0;
+ static_cast<Connection*>(aDB)->setCommitHook(commit_hook, &commit);
+
+ RefPtr<AsyncStatementSpinner> asyncSpin(new AsyncStatementSpinner());
+ nsCOMPtr<mozIStoragePendingStatement> 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<Connection*>(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<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageAsyncStatement> stmt1;
+ db->CreateAsyncStatement("SELECT * FROM sqlite_master"_ns,
+ getter_AddRefs(stmt1));
+
+ nsCOMPtr<mozIStorageAsyncStatement> stmt2;
+ db->CreateAsyncStatement("SELECT * FROM sqlite_master"_ns,
+ getter_AddRefs(stmt2));
+
+ nsTArray<RefPtr<mozIStorageBaseStatement>> 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<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageStatement> stmt1;
+ db->CreateStatement("SELECT * FROM sqlite_master"_ns, getter_AddRefs(stmt1));
+
+ nsCOMPtr<mozIStorageStatement> stmt2;
+ db->CreateStatement("SELECT * FROM sqlite_master"_ns, getter_AddRefs(stmt2));
+
+ nsTArray<RefPtr<mozIStorageBaseStatement>> 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<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageAsyncStatement> stmt1;
+ db->CreateAsyncStatement("SELECT * FROM sqlite_master"_ns,
+ getter_AddRefs(stmt1));
+
+ nsCOMPtr<mozIStorageAsyncStatement> stmt2;
+ db->CreateAsyncStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns,
+ getter_AddRefs(stmt2));
+
+ nsTArray<RefPtr<mozIStorageBaseStatement>> 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<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageStatement> stmt1;
+ db->CreateStatement("SELECT * FROM sqlite_master"_ns, getter_AddRefs(stmt1));
+
+ nsCOMPtr<mozIStorageStatement> stmt2;
+ db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns,
+ getter_AddRefs(stmt2));
+
+ nsTArray<RefPtr<mozIStorageBaseStatement>> 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<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageAsyncStatement> stmt1;
+ db->CreateAsyncStatement("CREATE TABLE test1 (id INTEGER PRIMARY KEY)"_ns,
+ getter_AddRefs(stmt1));
+
+ nsCOMPtr<mozIStorageAsyncStatement> stmt2;
+ db->CreateAsyncStatement("CREATE TABLE test2 (id INTEGER PRIMARY KEY)"_ns,
+ getter_AddRefs(stmt2));
+
+ nsTArray<RefPtr<mozIStorageBaseStatement>> 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<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageStatement> stmt1;
+ db->CreateStatement("CREATE TABLE test1 (id INTEGER PRIMARY KEY)"_ns,
+ getter_AddRefs(stmt1));
+
+ nsCOMPtr<mozIStorageStatement> stmt2;
+ db->CreateStatement("CREATE TABLE test2 (id INTEGER PRIMARY KEY)"_ns,
+ getter_AddRefs(stmt2));
+
+ nsTArray<RefPtr<mozIStorageBaseStatement>> 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<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageAsyncStatement> stmt;
+ db->CreateAsyncStatement("SELECT * FROM sqlite_master"_ns,
+ getter_AddRefs(stmt));
+
+ nsTArray<RefPtr<mozIStorageBaseStatement>> 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<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageStatement> stmt;
+ db->CreateStatement("SELECT * FROM sqlite_master"_ns, getter_AddRefs(stmt));
+
+ nsTArray<RefPtr<mozIStorageBaseStatement>> 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<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageAsyncStatement> stmt;
+ db->CreateAsyncStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns,
+ getter_AddRefs(stmt));
+
+ nsTArray<RefPtr<mozIStorageBaseStatement>> 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<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageStatement> stmt;
+ db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns,
+ getter_AddRefs(stmt));
+
+ nsTArray<RefPtr<mozIStorageBaseStatement>> 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<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageAsyncStatement> stmt;
+ db->CreateAsyncStatement("SELECT :param FROM sqlite_master"_ns,
+ getter_AddRefs(stmt));
+
+ // -- bind multiple BindingParams
+ nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
+ stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
+ for (int32_t i = 0; i < 2; i++) {
+ nsCOMPtr<mozIStorageBindingParams> params;
+ paramsArray->NewBindingParams(getter_AddRefs(params));
+ params->BindInt32ByName("param"_ns, 1);
+ paramsArray->AddParams(params);
+ }
+ stmt->BindParameters(paramsArray);
+ paramsArray = nullptr;
+
+ nsTArray<RefPtr<mozIStorageBaseStatement>> 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<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageStatement> stmt;
+ db->CreateStatement("SELECT :param FROM sqlite_master"_ns,
+ getter_AddRefs(stmt));
+
+ // -- bind multiple BindingParams
+ nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
+ stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
+ for (int32_t i = 0; i < 2; i++) {
+ nsCOMPtr<mozIStorageBindingParams> params;
+ paramsArray->NewBindingParams(getter_AddRefs(params));
+ params->BindInt32ByName("param"_ns, 1);
+ paramsArray->AddParams(params);
+ }
+ stmt->BindParameters(paramsArray);
+ paramsArray = nullptr;
+
+ nsTArray<RefPtr<mozIStorageBaseStatement>> 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<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create a table for writes
+ nsCOMPtr<mozIStorageStatement> tableStmt;
+ db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns,
+ getter_AddRefs(tableStmt));
+ tableStmt->Execute();
+ tableStmt->Finalize();
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageAsyncStatement> stmt;
+ db->CreateAsyncStatement("DELETE FROM test WHERE id = :param"_ns,
+ getter_AddRefs(stmt));
+
+ // -- bind multiple BindingParams
+ nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
+ stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
+ for (int32_t i = 0; i < 2; i++) {
+ nsCOMPtr<mozIStorageBindingParams> params;
+ paramsArray->NewBindingParams(getter_AddRefs(params));
+ params->BindInt32ByName("param"_ns, 1);
+ paramsArray->AddParams(params);
+ }
+ stmt->BindParameters(paramsArray);
+ paramsArray = nullptr;
+
+ nsTArray<RefPtr<mozIStorageBaseStatement>> 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<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create a table for writes
+ nsCOMPtr<mozIStorageStatement> tableStmt;
+ db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns,
+ getter_AddRefs(tableStmt));
+ tableStmt->Execute();
+ tableStmt->Finalize();
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageStatement> stmt;
+ db->CreateStatement("DELETE FROM test WHERE id = :param"_ns,
+ getter_AddRefs(stmt));
+
+ // -- bind multiple BindingParams
+ nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
+ stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
+ for (int32_t i = 0; i < 2; i++) {
+ nsCOMPtr<mozIStorageBindingParams> params;
+ paramsArray->NewBindingParams(getter_AddRefs(params));
+ params->BindInt32ByName("param"_ns, 1);
+ paramsArray->AddParams(params);
+ }
+ stmt->BindParameters(paramsArray);
+ paramsArray = nullptr;
+
+ nsTArray<RefPtr<mozIStorageBaseStatement>> 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<nsIThread> 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<mozIStorageConnection> 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<mozIStorageConnection> db(getMemoryDatabase());
+
+ // Create a test table and populate it.
+ nsCOMPtr<mozIStorageStatement> 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<mozIStoragePendingStatement> 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<mozIStorageConnection> db(getMemoryDatabase());
+
+ // Create a test table and populate it.
+ nsCOMPtr<mozIStorageStatement> 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<mozIStoragePendingStatement> 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_async_thread_naming.cpp b/storage/test/gtest/test_async_thread_naming.cpp
new file mode 100644
index 0000000000..77d088f3e5
--- /dev/null
+++ b/storage/test/gtest/test_async_thread_naming.cpp
@@ -0,0 +1,230 @@
+/* -*- 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 "nsVariant.h"
+#include "storage_test_harness.h"
+#include "nsThreadUtils.h"
+#include "nsIURI.h"
+#include "nsIFileURL.h"
+#include "nsIVariant.h"
+#include "nsNetUtil.h"
+
+////////////////////////////////////////////////////////////////////////////////
+//// Tests
+
+TEST(storage_async_thread_naming, MemoryDatabase)
+{
+ HookSqliteMutex hook;
+
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ nsCOMPtr<nsIThread> target(get_conn_async_thread(db));
+ do_check_true(target);
+ PRThread* prThread;
+ target->GetPRThread(&prThread);
+ do_check_true(prThread);
+ nsAutoCString name(PR_GetThreadName(prThread));
+ printf("%s", name.get());
+ do_check_true(StringBeginsWith(name, "sqldb:memory"_ns));
+
+ blocking_async_close(db);
+}
+
+TEST(storage_async_thread_naming, FileDatabase)
+{
+ HookSqliteMutex hook;
+
+ nsAutoString filename(u"test_thread_name.sqlite"_ns);
+ nsCOMPtr<nsIFile> dbFile;
+ do_check_success(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(dbFile)));
+ do_check_success(dbFile->Append(filename));
+ nsCOMPtr<mozIStorageService> ss = getService();
+ nsCOMPtr<mozIStorageConnection> conn;
+ do_check_success(ss->OpenDatabase(dbFile, 0, getter_AddRefs(conn)));
+
+ nsCOMPtr<nsIThread> target(get_conn_async_thread(conn));
+ do_check_true(target);
+ PRThread* prThread;
+ target->GetPRThread(&prThread);
+ do_check_true(prThread);
+ nsAutoCString name(PR_GetThreadName(prThread));
+ nsAutoCString expected("sqldb:"_ns);
+ expected.Append(NS_ConvertUTF16toUTF8(filename));
+ do_check_true(StringBeginsWith(name, expected));
+
+ {
+ nsCOMPtr<mozIStorageConnection> clone;
+ do_check_success(conn->Clone(true, getter_AddRefs(clone)));
+ nsCOMPtr<nsIThread> target(get_conn_async_thread(conn));
+ do_check_true(target);
+ PRThread* prThread;
+ target->GetPRThread(&prThread);
+ do_check_true(prThread);
+ nsAutoCString name(PR_GetThreadName(prThread));
+ nsAutoCString expected("sqldb:"_ns);
+ expected.Append(NS_ConvertUTF16toUTF8(filename));
+ do_check_true(StringBeginsWith(name, expected));
+ blocking_async_close(clone);
+ }
+
+ blocking_async_close(conn);
+}
+
+TEST(storage_async_thread_naming, FileUnsharedDatabase)
+{
+ HookSqliteMutex hook;
+
+ nsAutoString filename(u"test_thread_name.sqlite"_ns);
+ nsCOMPtr<nsIFile> dbFile;
+ do_check_success(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(dbFile)));
+ do_check_success(dbFile->Append(filename));
+ nsCOMPtr<mozIStorageService> ss = getService();
+ nsCOMPtr<mozIStorageConnection> conn;
+ do_check_success(ss->OpenUnsharedDatabase(dbFile, 0, getter_AddRefs(conn)));
+
+ nsCOMPtr<nsIThread> target(get_conn_async_thread(conn));
+ do_check_true(target);
+ PRThread* prThread;
+ target->GetPRThread(&prThread);
+ do_check_true(prThread);
+ nsAutoCString name(PR_GetThreadName(prThread));
+ nsAutoCString expected("sqldb:"_ns);
+ expected.Append(NS_ConvertUTF16toUTF8(filename));
+ do_check_true(StringBeginsWith(name, expected));
+
+ blocking_async_close(conn);
+}
+
+TEST(storage_async_thread_naming, FileURLDatabase)
+{
+ HookSqliteMutex hook;
+
+ nsAutoString filename(u"test_thread_name.sqlite"_ns);
+ nsCOMPtr<nsIFile> dbFile;
+ do_check_success(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(dbFile)));
+ do_check_success(dbFile->Append(filename));
+ nsCOMPtr<nsIURI> uri;
+ do_check_success(NS_NewFileURI(getter_AddRefs(uri), dbFile));
+ nsCOMPtr<nsIFileURL> fileUrl = do_QueryInterface(uri);
+ nsCOMPtr<mozIStorageService> ss = getService();
+ nsCOMPtr<mozIStorageConnection> conn;
+ do_check_success(ss->OpenDatabaseWithFileURL(fileUrl, EmptyCString(), 0,
+ getter_AddRefs(conn)));
+
+ nsCOMPtr<nsIThread> target(get_conn_async_thread(conn));
+ do_check_true(target);
+ PRThread* prThread;
+ target->GetPRThread(&prThread);
+ do_check_true(prThread);
+ nsAutoCString name(PR_GetThreadName(prThread));
+ nsAutoCString expected("sqldb:"_ns);
+ expected.Append(NS_ConvertUTF16toUTF8(filename));
+ do_check_true(StringBeginsWith(name, expected));
+
+ blocking_async_close(conn);
+}
+
+TEST(storage_async_thread_naming, OverrideFileURLDatabase)
+{
+ HookSqliteMutex hook;
+
+ nsAutoString filename(u"test_thread_name.sqlite"_ns);
+ nsCOMPtr<nsIFile> dbFile;
+ do_check_success(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(dbFile)));
+ do_check_success(dbFile->Append(filename));
+ nsCOMPtr<nsIURI> uri;
+ do_check_success(NS_NewFileURI(getter_AddRefs(uri), dbFile));
+ nsCOMPtr<nsIFileURL> fileUrl = do_QueryInterface(uri);
+ nsCOMPtr<mozIStorageService> ss = getService();
+ nsCOMPtr<mozIStorageConnection> conn;
+ nsAutoCString override("override"_ns);
+ do_check_success(
+ ss->OpenDatabaseWithFileURL(fileUrl, override, 0, getter_AddRefs(conn)));
+
+ nsCOMPtr<nsIThread> target(get_conn_async_thread(conn));
+ do_check_true(target);
+ PRThread* prThread;
+ target->GetPRThread(&prThread);
+ do_check_true(prThread);
+ nsAutoCString name(PR_GetThreadName(prThread));
+ nsAutoCString expected("sqldb:"_ns);
+ expected.Append(override);
+ do_check_true(StringBeginsWith(name, expected));
+
+ blocking_async_close(conn);
+}
+
+TEST(storage_async_thread_naming, AsyncOpenDatabase)
+{
+ HookSqliteMutex hook;
+
+ nsAutoString filename(u"test_thread_name.sqlite"_ns);
+ nsCOMPtr<nsIFile> dbFile;
+ do_check_success(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(dbFile)));
+ do_check_success(dbFile->Append(filename));
+ nsCOMPtr<mozIStorageService> ss = getService();
+
+ RefPtr<AsyncCompletionSpinner> completionSpinner =
+ new AsyncCompletionSpinner();
+ RefPtr<nsVariant> variant = new nsVariant();
+ variant->SetAsInterface(NS_GET_IID(nsIFile), dbFile);
+ do_check_success(ss->OpenAsyncDatabase(variant, 0, 0, completionSpinner));
+ completionSpinner->SpinUntilCompleted();
+ nsCOMPtr<mozIStorageConnection> conn(
+ do_QueryInterface(completionSpinner->mCompletionValue));
+ nsCOMPtr<nsIThread> target(get_conn_async_thread(conn));
+ do_check_true(target);
+ PRThread* prThread;
+ target->GetPRThread(&prThread);
+ do_check_true(prThread);
+ nsAutoCString name(PR_GetThreadName(prThread));
+ nsAutoCString expected("sqldb:"_ns);
+ expected.Append(NS_ConvertUTF16toUTF8(filename));
+ do_check_true(StringBeginsWith(name, expected));
+
+ blocking_async_close(conn);
+}
+
+TEST(storage_async_thread_naming, AsyncClone)
+{
+ HookSqliteMutex hook;
+
+ nsAutoString filename(u"test_thread_name.sqlite"_ns);
+ nsCOMPtr<nsIFile> dbFile;
+ do_check_success(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(dbFile)));
+ do_check_success(dbFile->Append(filename));
+ nsCOMPtr<mozIStorageService> ss = getService();
+
+ nsCOMPtr<mozIStorageConnection> conn;
+ do_check_success(ss->OpenDatabase(dbFile, 0, getter_AddRefs(conn)));
+
+ RefPtr<AsyncCompletionSpinner> completionSpinner =
+ new AsyncCompletionSpinner();
+ RefPtr<nsVariant> variant = new nsVariant();
+ variant->SetAsInterface(NS_GET_IID(nsIFile), dbFile);
+ do_check_success(conn->AsyncClone(true, completionSpinner));
+ completionSpinner->SpinUntilCompleted();
+ nsCOMPtr<mozIStorageConnection> clone(
+ do_QueryInterface(completionSpinner->mCompletionValue));
+ nsCOMPtr<nsIThread> target(get_conn_async_thread(clone));
+ do_check_true(target);
+ PRThread* prThread;
+ target->GetPRThread(&prThread);
+ do_check_true(prThread);
+ nsAutoCString name(PR_GetThreadName(prThread));
+ nsAutoCString expected("sqldb:"_ns);
+ expected.Append(NS_ConvertUTF16toUTF8(filename));
+ do_check_true(StringBeginsWith(name, expected));
+
+ blocking_async_close(conn);
+ blocking_async_close(clone);
+}
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<mozIStorageConnection> 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<mozIStorageStatement> 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<mozIStorageConnection> 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<mozIStorageStatement> 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<mozIStorageConnection> 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<mozIStorageStatement> 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<mozIStorageConnection> db(getDatabase());
+ nsCOMPtr<nsIFile> 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<mozIStorageConnection> mConnection;
+
+ nsCOMPtr<nsIThread> 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<mozIStorageStatement> 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<mozIStorageConnection> db(getMemoryDatabase());
+ // Run an async statement.
+ nsCOMPtr<mozIStorageAsyncStatement> stmt;
+ do_check_success(db->CreateAsyncStatement(
+ "CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, getter_AddRefs(stmt)));
+ nsCOMPtr<mozIStoragePendingStatement> p;
+ do_check_success(stmt->ExecuteAsync(nullptr, getter_AddRefs(p)));
+ do_check_success(stmt->Finalize());
+
+ // Wrongly use Close() instead of AsyncClose().
+ RefPtr<CompletionRunnable> 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<mozIStorageConnection> db(getMemoryDatabase());
+ // Run an async statement.
+ nsCOMPtr<mozIStorageAsyncStatement> stmt;
+ do_check_success(db->CreateAsyncStatement(
+ "CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, getter_AddRefs(stmt)));
+ nsCOMPtr<mozIStoragePendingStatement> p;
+ do_check_success(stmt->ExecuteAsync(nullptr, getter_AddRefs(p)));
+ do_check_success(stmt->Finalize());
+
+ // Use the spinning close API.
+ RefPtr<CompletionRunnable> 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<mozIStorageConnection> 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<mozIStorageStatement> 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<mozIStorageConnection> 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<mozIStorageStatement> 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<Connection*>(aDB)->getAutocommit());
+}
+
+/**
+ * This file tests our Transaction helper in mozStorageHelper.h.
+ */
+
+TEST(storage_transaction_helper, Commit)
+{
+ nsCOMPtr<mozIStorageConnection> 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<mozIStorageConnection> 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<mozIStorageConnection> 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<mozIStorageConnection> 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<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- wedge the thread
+ nsCOMPtr<nsIThread> target(get_conn_async_thread(db));
+ do_check_true(target);
+ RefPtr<ThreadWedger> 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<mozIStorageAsyncStatement> 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<mozIStorageConnection> 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<mozIStorageConnection> db(getMemoryDatabase());
+
+ // Start watching for forbidden mutex usage.
+ watch_for_mutex_use_on_this_thread();
+
+ // - statement with nothing to bind
+ nsCOMPtr<mozIStorageAsyncStatement> 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<mozIStorageBindingParamsArray> paramsArray;
+ stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
+ nsCOMPtr<mozIStorageBindingParams> 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<mozIStorageStatement> 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<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- wedge the thread
+ nsCOMPtr<nsIThread> target(get_conn_async_thread(db));
+ do_check_true(target);
+ RefPtr<ThreadWedger> wedger(new ThreadWedger(target));
+
+ // -- create statements and cancel them
+ // - async
+ nsCOMPtr<mozIStorageAsyncStatement> asyncStmt;
+ db->CreateAsyncStatement(
+ "CREATE TABLE asyncTable (id INTEGER PRIMARY KEY)"_ns,
+ getter_AddRefs(asyncStmt));
+
+ RefPtr<AsyncStatementSpinner> asyncSpin(new AsyncStatementSpinner());
+ nsCOMPtr<mozIStoragePendingStatement> asyncPend;
+ (void)asyncStmt->ExecuteAsync(asyncSpin, getter_AddRefs(asyncPend));
+ do_check_true(asyncPend);
+ asyncPend->Cancel();
+
+ // - sync
+ nsCOMPtr<mozIStorageStatement> syncStmt;
+ db->CreateStatement("CREATE TABLE syncTable (id INTEGER PRIMARY KEY)"_ns,
+ getter_AddRefs(syncStmt));
+
+ RefPtr<AsyncStatementSpinner> syncSpin(new AsyncStatementSpinner());
+ nsCOMPtr<mozIStoragePendingStatement> 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<mozIStorageConnection> db(getMemoryDatabase());
+ watch_for_mutex_use_on_this_thread();
+
+ // -- create an async statement
+ nsCOMPtr<mozIStorageAsyncStatement> 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<mozIStorageConnection> db(getDatabase(mDBFile));
+
+ nsCString sql(mSQL);
+ nsCOMPtr<mozIStorageStatement> 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<nsIThread> mThread;
+ const char* const mSQL;
+ State mState;
+ nsCOMPtr<nsIFile> 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<mozIStorageStatement> 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<mozIStorageConnection> mConnection;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// Test Functions
+
+void setup() {
+ nsCOMPtr<mozIStorageConnection> 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<mozIStorageConnection> db(getDatabase());
+
+ // Need to prepare our statement ahead of time so we make sure to only test
+ // step and not prepare.
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = db->CreateStatement(
+ "INSERT INTO test (data) VALUES ('test1')"_ns, getter_AddRefs(stmt));
+ do_check_success(rv);
+
+ nsCOMPtr<nsIFile> dbFile;
+ db->GetDatabaseFile(getter_AddRefs(dbFile));
+ RefPtr<DatabaseLocker> 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<mozIStorageConnection> db(getDatabase());
+
+ // Need to prepare our statement ahead of time so we make sure to only test
+ // step and not prepare.
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv =
+ db->CreateStatement("SELECT * FROM test"_ns, getter_AddRefs(stmt));
+ do_check_success(rv);
+
+ RefPtr<DatabaseTester> 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<mozIStorageConnection> db(getDatabase());
+
+ // Need to prepare our statement ahead of time so we make sure to only test
+ // step and not prepare.
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv =
+ db->CreateStatement("SELECT * FROM test"_ns, getter_AddRefs(stmt));
+ do_check_success(rv);
+
+ RefPtr<DatabaseTester> 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();
+}
diff --git a/storage/test/moz.build b/storage/test/moz.build
new file mode 100644
index 0000000000..7dd21f5fe2
--- /dev/null
+++ b/storage/test/moz.build
@@ -0,0 +1,13 @@
+# -*- 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.toml"]
+
+TEST_DIRS += ["gtest"]
+
+TESTING_JS_MODULES += [
+ "unit/VacuumParticipant.sys.mjs",
+]
diff --git a/storage/test/unit/VacuumParticipant.sys.mjs b/storage/test/unit/VacuumParticipant.sys.mjs
new file mode 100644
index 0000000000..9f1c39826e
--- /dev/null
+++ b/storage/test/unit/VacuumParticipant.sys.mjs
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This testing component is used in test_vacuum* tests.
+
+const CAT_NAME = "vacuum-participant";
+const CONTRACT_ID = "@unit.test.com/test-vacuum-participant;1";
+
+import { MockRegistrar } from "resource://testing-common/MockRegistrar.sys.mjs";
+import { TestUtils } from "resource://testing-common/TestUtils.sys.mjs";
+
+export class VacuumParticipant {
+ #dbConn;
+ #expectedPageSize = 0;
+ #useIncrementalVacuum = false;
+ #grant = false;
+
+ /**
+ * Build a VacuumParticipant instance.
+ * Note: After creation you must await instance.promiseRegistered() to ensure
+ * Category Caches have been updated.
+ *
+ * @param {mozIStorageAsyncConnection} databaseConnection
+ * The connection to be vacuumed.
+ * @param {Number} [expectedPageSize]
+ * Used to change the database page size.
+ * @param {boolean} [useIncrementalVacuum]
+ * Whether to enable incremental vacuum on the database.
+ * @param {boolean} [grant]
+ * Whether the vacuum operation should be granted.
+ */
+ constructor(
+ databaseConnection,
+ { expectedPageSize = 0, useIncrementalVacuum = false, grant = true } = {}
+ ) {
+ this.#dbConn = databaseConnection;
+
+ // Register as the only participant.
+ this.#unregisterAllParticipants();
+ this.#registerAsParticipant();
+
+ this.#expectedPageSize = expectedPageSize;
+ this.#useIncrementalVacuum = useIncrementalVacuum;
+ this.#grant = grant;
+
+ this.QueryInterface = ChromeUtils.generateQI([
+ "mozIStorageVacuumParticipant",
+ ]);
+ }
+
+ promiseRegistered() {
+ // The category manager dispatches change notifications to the main thread,
+ // so we must wait one tick.
+ return TestUtils.waitForTick();
+ }
+
+ #registerAsParticipant() {
+ MockRegistrar.register(CONTRACT_ID, this);
+ Services.catMan.addCategoryEntry(
+ CAT_NAME,
+ "vacuumParticipant",
+ CONTRACT_ID,
+ false,
+ false
+ );
+ }
+
+ #unregisterAllParticipants() {
+ // First unregister other participants.
+ for (let { data: entry } of Services.catMan.enumerateCategory(CAT_NAME)) {
+ Services.catMan.deleteCategoryEntry("vacuum-participant", entry, false);
+ }
+ }
+
+ async dispose() {
+ this.#unregisterAllParticipants();
+ MockRegistrar.unregister(CONTRACT_ID);
+ await new Promise(resolve => {
+ this.#dbConn.asyncClose(resolve);
+ });
+ }
+
+ get expectedDatabasePageSize() {
+ return this.#expectedPageSize;
+ }
+
+ get useIncrementalVacuum() {
+ return this.#useIncrementalVacuum;
+ }
+
+ get databaseConnection() {
+ return this.#dbConn;
+ }
+
+ onBeginVacuum() {
+ if (!this.#grant) {
+ return false;
+ }
+ Services.obs.notifyObservers(null, "test-begin-vacuum");
+ return true;
+ }
+ onEndVacuum(succeeded) {
+ Services.obs.notifyObservers(
+ null,
+ succeeded ? "test-end-vacuum-success" : "test-end-vacuum-failure"
+ );
+ }
+}
diff --git a/storage/test/unit/baddataDB.sqlite b/storage/test/unit/baddataDB.sqlite
new file mode 100644
index 0000000000..5b2f9da3d6
--- /dev/null
+++ b/storage/test/unit/baddataDB.sqlite
Binary files differ
diff --git a/storage/test/unit/corruptDB.sqlite b/storage/test/unit/corruptDB.sqlite
new file mode 100644
index 0000000000..b234246cac
--- /dev/null
+++ b/storage/test/unit/corruptDB.sqlite
Binary files differ
diff --git a/storage/test/unit/fakeDB.sqlite b/storage/test/unit/fakeDB.sqlite
new file mode 100644
index 0000000000..5f7498bfc2
--- /dev/null
+++ b/storage/test/unit/fakeDB.sqlite
@@ -0,0 +1 @@
+BACON
diff --git a/storage/test/unit/goodDB.sqlite b/storage/test/unit/goodDB.sqlite
new file mode 100644
index 0000000000..b06884672f
--- /dev/null
+++ b/storage/test/unit/goodDB.sqlite
Binary files differ
diff --git a/storage/test/unit/head_storage.js b/storage/test/unit/head_storage.js
new file mode 100644
index 0000000000..ba1b76ad0b
--- /dev/null
+++ b/storage/test/unit/head_storage.js
@@ -0,0 +1,412 @@
+/* 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/. */
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+ TestUtils: "resource://testing-common/TestUtils.sys.mjs",
+});
+
+const OPEN_HISTOGRAM = "SQLITE_STORE_OPEN";
+const QUERY_HISTOGRAM = "SQLITE_STORE_QUERY";
+
+const TELEMETRY_VALUES = {
+ success: 0,
+ failure: 1,
+ access: 2,
+ diskio: 3,
+ corrupt: 4,
+ busy: 5,
+ misuse: 6,
+ diskspace: 7,
+};
+
+do_get_profile();
+var gDBConn = null;
+
+const TEST_DB_NAME = "test_storage.sqlite";
+function getTestDB() {
+ var db = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ db.append(TEST_DB_NAME);
+ return db;
+}
+
+/**
+ * Obtains a corrupt database to test against.
+ */
+function getCorruptDB() {
+ return do_get_file("corruptDB.sqlite");
+}
+
+/**
+ * Obtains a fake (non-SQLite format) database to test against.
+ */
+function getFakeDB() {
+ return do_get_file("fakeDB.sqlite");
+}
+
+/**
+ * Delete the test database file.
+ */
+function deleteTestDB() {
+ print("*** Storage Tests: Trying to remove file!");
+ var dbFile = getTestDB();
+ if (dbFile.exists()) {
+ try {
+ dbFile.remove(false);
+ } catch (e) {
+ /* stupid windows box */
+ }
+ }
+}
+
+function cleanup() {
+ // close the connection
+ print("*** Storage Tests: Trying to close!");
+ getOpenedDatabase().close();
+
+ // we need to null out the database variable to get a new connection the next
+ // time getOpenedDatabase is called
+ gDBConn = null;
+
+ // removing test db
+ deleteTestDB();
+}
+
+/**
+ * Use asyncClose to cleanup a connection. Synchronous by means of internally
+ * spinning an event loop.
+ */
+function asyncCleanup() {
+ let closed = false;
+
+ // close the connection
+ print("*** Storage Tests: Trying to asyncClose!");
+ getOpenedDatabase().asyncClose(function () {
+ closed = true;
+ });
+
+ let tm = Cc["@mozilla.org/thread-manager;1"].getService();
+ tm.spinEventLoopUntil("Test(head_storage.js:asyncCleanup)", () => closed);
+
+ // we need to null out the database variable to get a new connection the next
+ // time getOpenedDatabase is called
+ gDBConn = null;
+
+ // removing test db
+ deleteTestDB();
+}
+
+/**
+ * Get a connection to the test database. Creates and caches the connection
+ * if necessary, otherwise reuses the existing cached connection. This
+ * connection shares its cache.
+ *
+ * @returns the mozIStorageConnection for the file.
+ */
+function getOpenedDatabase(connectionFlags = 0) {
+ if (!gDBConn) {
+ gDBConn = Services.storage.openDatabase(getTestDB(), connectionFlags);
+
+ // Clear out counts for any queries that occured while opening the database.
+ TelemetryTestUtils.getAndClearKeyedHistogram(OPEN_HISTOGRAM);
+ TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);
+ }
+ return gDBConn;
+}
+
+/**
+ * Get a connection to the test database. Creates and caches the connection
+ * if necessary, otherwise reuses the existing cached connection. This
+ * connection doesn't share its cache.
+ *
+ * @returns the mozIStorageConnection for the file.
+ */
+function getOpenedUnsharedDatabase() {
+ if (!gDBConn) {
+ gDBConn = Services.storage.openUnsharedDatabase(getTestDB());
+ }
+ return gDBConn;
+}
+
+/**
+ * Obtains a specific database to use.
+ *
+ * @param aFile
+ * The nsIFile representing the db file to open.
+ * @returns the mozIStorageConnection for the file.
+ */
+function getDatabase(aFile) {
+ return Services.storage.openDatabase(aFile);
+}
+
+function createStatement(aSQL) {
+ return getOpenedDatabase().createStatement(aSQL);
+}
+
+/**
+ * Creates an asynchronous SQL statement.
+ *
+ * @param aSQL
+ * The SQL to parse into a statement.
+ * @returns a mozIStorageAsyncStatement from aSQL.
+ */
+function createAsyncStatement(aSQL) {
+ return getOpenedDatabase().createAsyncStatement(aSQL);
+}
+
+/**
+ * Invoke the given function and assert that it throws an exception expressing
+ * the provided error code in its 'result' attribute. JS function expressions
+ * can be used to do this concisely.
+ *
+ * Example:
+ * expectError(Cr.NS_ERROR_INVALID_ARG, () => explodingFunction());
+ *
+ * @param aErrorCode
+ * The error code to expect from invocation of aFunction.
+ * @param aFunction
+ * The function to invoke and expect an XPCOM-style error from.
+ */
+function expectError(aErrorCode, aFunction) {
+ let exceptionCaught = false;
+ try {
+ aFunction();
+ } catch (e) {
+ if (e.result != aErrorCode) {
+ do_throw(
+ "Got an exception, but the result code was not the expected " +
+ "one. Expected " +
+ aErrorCode +
+ ", got " +
+ e.result
+ );
+ }
+ exceptionCaught = true;
+ }
+ if (!exceptionCaught) {
+ do_throw(aFunction + " should have thrown an exception but did not!");
+ }
+}
+
+/**
+ * Run a query synchronously and verify that we get back the expected results.
+ *
+ * @param aSQLString
+ * The SQL string for the query.
+ * @param aBind
+ * The value to bind at index 0.
+ * @param aResults
+ * A list of the expected values returned in the sole result row.
+ * Express blobs as lists.
+ */
+function verifyQuery(aSQLString, aBind, aResults) {
+ let stmt = getOpenedDatabase().createStatement(aSQLString);
+ stmt.bindByIndex(0, aBind);
+ try {
+ Assert.ok(stmt.executeStep());
+ let nCols = stmt.numEntries;
+ if (aResults.length != nCols) {
+ do_throw(
+ "Expected " +
+ aResults.length +
+ " columns in result but " +
+ "there are only " +
+ aResults.length +
+ "!"
+ );
+ }
+ for (let iCol = 0; iCol < nCols; iCol++) {
+ let expectedVal = aResults[iCol];
+ let valType = stmt.getTypeOfIndex(iCol);
+ if (expectedVal === null) {
+ Assert.equal(stmt.VALUE_TYPE_NULL, valType);
+ Assert.ok(stmt.getIsNull(iCol));
+ } else if (typeof expectedVal == "number") {
+ if (Math.floor(expectedVal) == expectedVal) {
+ Assert.equal(stmt.VALUE_TYPE_INTEGER, valType);
+ Assert.equal(expectedVal, stmt.getInt32(iCol));
+ } else {
+ Assert.equal(stmt.VALUE_TYPE_FLOAT, valType);
+ Assert.equal(expectedVal, stmt.getDouble(iCol));
+ }
+ } else if (typeof expectedVal == "string") {
+ Assert.equal(stmt.VALUE_TYPE_TEXT, valType);
+ Assert.equal(expectedVal, stmt.getUTF8String(iCol));
+ } else {
+ // blob
+ Assert.equal(stmt.VALUE_TYPE_BLOB, valType);
+ let count = { value: 0 },
+ blob = { value: null };
+ stmt.getBlob(iCol, count, blob);
+ Assert.equal(count.value, expectedVal.length);
+ for (let i = 0; i < count.value; i++) {
+ Assert.equal(expectedVal[i], blob.value[i]);
+ }
+ }
+ }
+ } finally {
+ stmt.finalize();
+ }
+}
+
+/**
+ * Return the number of rows in the able with the given name using a synchronous
+ * query.
+ *
+ * @param aTableName
+ * The name of the table.
+ * @return The number of rows.
+ */
+function getTableRowCount(aTableName) {
+ var currentRows = 0;
+ var countStmt = getOpenedDatabase().createStatement(
+ "SELECT COUNT(1) AS count FROM " + aTableName
+ );
+ try {
+ Assert.ok(countStmt.executeStep());
+ currentRows = countStmt.row.count;
+ } finally {
+ countStmt.finalize();
+ }
+ return currentRows;
+}
+
+// Promise-Returning Functions
+
+function asyncClone(db, readOnly) {
+ return new Promise((resolve, reject) => {
+ db.asyncClone(readOnly, function (status, db2) {
+ if (Components.isSuccessCode(status)) {
+ resolve(db2.QueryInterface(Ci.mozIStorageAsyncConnection));
+ } else {
+ reject(status);
+ }
+ });
+ });
+}
+
+function asyncClose(db) {
+ return new Promise((resolve, reject) => {
+ db.asyncClose(function (status) {
+ if (Components.isSuccessCode(status)) {
+ resolve();
+ } else {
+ reject(status);
+ }
+ });
+ });
+}
+
+function mapOptionsToFlags(aOptions, aMapping) {
+ let result = aMapping.default;
+ Object.entries(aOptions || {}).forEach(([optionName, isTrue]) => {
+ if (aMapping.hasOwnProperty(optionName) && isTrue) {
+ result |= aMapping[optionName];
+ }
+ });
+ return result;
+}
+
+function getOpenFlagsMap() {
+ return {
+ default: Ci.mozIStorageService.OPEN_DEFAULT,
+ shared: Ci.mozIStorageService.OPEN_SHARED,
+ readOnly: Ci.mozIStorageService.OPEN_READONLY,
+ ignoreLockingMode: Ci.mozIStorageService.OPEN_IGNORE_LOCKING_MODE,
+ };
+}
+
+function getConnectionFlagsMap() {
+ return {
+ default: Ci.mozIStorageService.CONNECTION_DEFAULT,
+ interruptible: Ci.mozIStorageService.CONNECTION_INTERRUPTIBLE,
+ };
+}
+
+function openAsyncDatabase(file, options) {
+ return new Promise((resolve, reject) => {
+ const openFlags = mapOptionsToFlags(options, getOpenFlagsMap());
+ const connectionFlags = mapOptionsToFlags(options, getConnectionFlagsMap());
+
+ Services.storage.openAsyncDatabase(
+ file,
+ openFlags,
+ connectionFlags,
+ function (status, db) {
+ if (Components.isSuccessCode(status)) {
+ resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection));
+ } else {
+ reject(status);
+ }
+ }
+ );
+ });
+}
+
+function executeAsync(statement, onResult) {
+ return new Promise((resolve, reject) => {
+ statement.executeAsync({
+ handleError(error) {
+ reject(error);
+ },
+ handleResult(result) {
+ if (onResult) {
+ onResult(result);
+ }
+ },
+ handleCompletion(result) {
+ resolve(result);
+ },
+ });
+ });
+}
+
+function executeMultipleStatementsAsync(db, statements, onResult) {
+ return new Promise((resolve, reject) => {
+ db.executeAsync(statements, {
+ handleError(error) {
+ reject(error);
+ },
+ handleResult(result) {
+ if (onResult) {
+ onResult(result);
+ }
+ },
+ handleCompletion(result) {
+ resolve(result);
+ },
+ });
+ });
+}
+
+function executeSimpleSQLAsync(db, query, onResult) {
+ return new Promise((resolve, reject) => {
+ db.executeSimpleSQLAsync(query, {
+ handleError(error) {
+ reject(error);
+ },
+ handleResult(result) {
+ if (onResult) {
+ onResult(result);
+ } else {
+ do_throw("No results were expected");
+ }
+ },
+ handleCompletion(result) {
+ resolve(result);
+ },
+ });
+ });
+}
+
+cleanup();
diff --git a/storage/test/unit/locale_collation.txt b/storage/test/unit/locale_collation.txt
new file mode 100644
index 0000000000..86f50579bb
--- /dev/null
+++ b/storage/test/unit/locale_collation.txt
@@ -0,0 +1,174 @@
+
+!
+"
+#
+$
+%
+&
+'
+(
+)
+*
++
+,
+-
+.
+/
+0
+1
+2
+3
+4
+5
+6
+7
+8
+9
+:
+;
+<
+=
+>
+?
+@
+A
+B
+C
+D
+E
+F
+G
+H
+I
+J
+K
+L
+M
+N
+O
+P
+Q
+R
+S
+T
+U
+V
+W
+X
+Y
+Z
+[
+\
+]
+^
+_
+`
+a
+b
+c
+d
+e
+f
+g
+h
+i
+j
+k
+l
+m
+n
+o
+p
+q
+r
+s
+t
+u
+v
+w
+x
+y
+z
+{
+|
+}
+~
+ludwig van beethoven
+Ludwig van Beethoven
+Ludwig van beethoven
+Jane
+jane
+JANE
+jAne
+jaNe
+janE
+JAne
+JaNe
+JanE
+JANe
+JaNE
+JAnE
+jANE
+Umberto Eco
+Umberto eco
+umberto eco
+umberto Eco
+UMBERTO ECO
+ace
+bash
+*ace
+!ace
+%ace
+~ace
+#ace
+cork
+denizen
+[denizen]
+(denizen)
+{denizen}
+/denizen/
+#denizen#
+$denizen$
+@denizen
+elf
+full
+gnome
+gnomic investigation of typological factors in the grammaticalization process of Malayo-Polynesian substaratum in the protoAltaic vocabulary core in the proto-layers of pre-historic Japanese
+gnomic investigation of typological factors in the grammaticalization process of Malayo-Polynesian substaratum in the protoAltaic vocabulary core in the proto-layers of pre-historic Javanese
+hint
+Internationalization
+Zinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalization
+Zinternationalization internationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizatio
+n
+Zinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalization internationalizationinternationalizationinternationalizationinternationalizationinternationalization
+ZinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizaTioninternationalizationinternationalizationinternationalizationinternationalization
+ZinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizaTion
+jostle
+kin
+Laymen
+lumens
+Lumens
+motleycrew
+motley crew
+niven's creative talents
+nivens creative talents
+opie
+posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the Rockies
+posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the Rokkies
+posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the rockies
+quilt's
+quilts
+quilt
+Rondo
+street
+tamale oxidization and iodization in rapid progress
+tamale oxidization and iodization in rapid Progress
+until
+vera
+Wobble
+Xanadu's legenary imaginary floccinaucinihilipilification in localization of theoretical portions of glottochronological understanding of the phoneme
+Xanadu's legenary imaginary floccinaucinihilipilification in localization of theoretical portions of glottochronological understanding of the phoname
+yearn
+zodiac
+a
diff --git a/storage/test/unit/test_bug-365166.js b/storage/test/unit/test_bug-365166.js
new file mode 100644
index 0000000000..aef53e60f2
--- /dev/null
+++ b/storage/test/unit/test_bug-365166.js
@@ -0,0 +1,23 @@
+// Testcase for bug 365166 - crash [@ strlen] calling
+// mozIStorageStatement::getColumnName of a statement created with
+// "PRAGMA user_version" or "PRAGMA schema_version"
+function run_test() {
+ test("user");
+ test("schema");
+
+ function test(param) {
+ var colName = param + "_version";
+ var sql = "PRAGMA " + colName;
+
+ var file = getTestDB();
+ var conn = Services.storage.openDatabase(file);
+ var statement = conn.createStatement(sql);
+ try {
+ // This shouldn't crash:
+ Assert.equal(statement.getColumnName(0), colName);
+ } finally {
+ statement.reset();
+ statement.finalize();
+ }
+ }
+}
diff --git a/storage/test/unit/test_bug-393952.js b/storage/test/unit/test_bug-393952.js
new file mode 100644
index 0000000000..a91bcb034e
--- /dev/null
+++ b/storage/test/unit/test_bug-393952.js
@@ -0,0 +1,35 @@
+/* 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/. */
+
+// Testcase for bug 393952: crash when I try to VACUUM and one of the tables
+// has a UNIQUE text column. StorageUnicodeFunctions::likeFunction()
+// needs to handle null aArgv[0] and aArgv[1]
+
+function setup() {
+ getOpenedDatabase().createTable("t1", "x TEXT UNIQUE");
+
+ var stmt = createStatement("INSERT INTO t1 (x) VALUES ('a')");
+ stmt.execute();
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_vacuum() {
+ var stmt = createStatement("VACUUM;");
+ stmt.executeStep();
+ stmt.reset();
+ stmt.finalize();
+}
+
+var tests = [test_vacuum];
+
+function run_test() {
+ setup();
+
+ for (var i = 0; i < tests.length; i++) {
+ tests[i]();
+ }
+
+ cleanup();
+}
diff --git a/storage/test/unit/test_bug-429521.js b/storage/test/unit/test_bug-429521.js
new file mode 100644
index 0000000000..1e647e984a
--- /dev/null
+++ b/storage/test/unit/test_bug-429521.js
@@ -0,0 +1,49 @@
+/* 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/. */
+
+function setup() {
+ getOpenedDatabase().createTable("t1", "x TEXT");
+
+ var stmt = createStatement(
+ "INSERT INTO t1 (x) VALUES ('/mozilla.org/20070129_1/Europe/Berlin')"
+ );
+ stmt.execute();
+ stmt.finalize();
+}
+
+function test_bug429521() {
+ var stmt = createStatement(
+ "SELECT DISTINCT(zone) FROM (" +
+ "SELECT x AS zone FROM t1 WHERE x LIKE '/mozilla.org%'" +
+ ");"
+ );
+
+ print("*** test_bug429521: started");
+
+ try {
+ while (stmt.executeStep()) {
+ print("*** test_bug429521: step() Read wrapper.row.zone");
+
+ // BUG: the print commands after the following statement
+ // are never executed. Script stops immediately.
+ stmt.row.zone;
+
+ print("*** test_bug429521: step() Read wrapper.row.zone finished");
+ }
+ } catch (e) {
+ print("*** test_bug429521: " + e);
+ }
+
+ print("*** test_bug429521: finished");
+
+ stmt.finalize();
+}
+
+function run_test() {
+ setup();
+
+ test_bug429521();
+
+ cleanup();
+}
diff --git a/storage/test/unit/test_bug-444233.js b/storage/test/unit/test_bug-444233.js
new file mode 100644
index 0000000000..934f69ad60
--- /dev/null
+++ b/storage/test/unit/test_bug-444233.js
@@ -0,0 +1,54 @@
+/* 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/. */
+
+function setup() {
+ // Create the table
+ getOpenedDatabase().createTable(
+ "test_bug444233",
+ "id INTEGER PRIMARY KEY, value TEXT"
+ );
+
+ // Insert dummy data, using wrapper methods
+ var stmt = createStatement(
+ "INSERT INTO test_bug444233 (value) VALUES (:value)"
+ );
+ stmt.params.value = "value1";
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO test_bug444233 (value) VALUES (:value)");
+ stmt.params.value = "value2";
+ stmt.execute();
+ stmt.finalize();
+}
+
+function test_bug444233() {
+ print("*** test_bug444233: started");
+
+ // Check that there are 2 results
+ var stmt = createStatement("SELECT COUNT(*) AS number FROM test_bug444233");
+ Assert.ok(stmt.executeStep());
+ Assert.equal(2, stmt.row.number);
+ stmt.reset();
+ stmt.finalize();
+
+ print("*** test_bug444233: doing delete");
+
+ // Now try to delete using IN
+ // Cheating since we know ids are 1,2
+ try {
+ var ids = [1, 2];
+ stmt = createStatement("DELETE FROM test_bug444233 WHERE id IN (:ids)");
+ stmt.params.ids = ids;
+ } catch (e) {
+ print("*** test_bug444233: successfully caught exception");
+ }
+ stmt.finalize();
+}
+
+function run_test() {
+ setup();
+ test_bug444233();
+ cleanup();
+}
diff --git a/storage/test/unit/test_cache_size.js b/storage/test/unit/test_cache_size.js
new file mode 100644
index 0000000000..bd56de64cd
--- /dev/null
+++ b/storage/test/unit/test_cache_size.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This file tests that dbs of various page sizes are using the right cache
+// size (bug 703113).
+
+/**
+ * In order to change the cache size, we must open a DB, change the page
+ * size, create a table, close the DB, then re-open the DB. We then check
+ * the cache size after reopening.
+ *
+ * @param dbOpener
+ * function that opens the DB specified in file
+ * @param file
+ * file holding the database
+ * @param pageSize
+ * the DB's page size
+ * @param expectedCacheSize
+ * the expected cache size for the given page size
+ */
+function check_size(dbOpener, file, pageSize, expectedCacheSize) {
+ // Open the DB, immediately change its page size.
+ let db = dbOpener(file);
+ db.executeSimpleSQL("PRAGMA page_size = " + pageSize);
+
+ // Check the page size change worked.
+ let stmt = db.createStatement("PRAGMA page_size");
+ Assert.ok(stmt.executeStep());
+ Assert.equal(stmt.row.page_size, pageSize);
+ stmt.finalize();
+
+ // Create a simple table.
+ db.executeSimpleSQL("CREATE TABLE test ( id INTEGER PRIMARY KEY )");
+
+ // Close and re-open the DB.
+ db.close();
+ db = dbOpener(file);
+
+ // Check cache size is as expected.
+ stmt = db.createStatement("PRAGMA cache_size");
+ Assert.ok(stmt.executeStep());
+ Assert.equal(stmt.row.cache_size, expectedCacheSize);
+ stmt.finalize();
+}
+
+function new_file(name) {
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append(name + ".sqlite");
+ Assert.ok(!file.exists());
+ return file;
+}
+
+function run_test() {
+ const kExpectedCacheSize = -2048; // 2MiB
+
+ let pageSizes = [1024, 4096, 32768];
+
+ for (let i = 0; i < pageSizes.length; i++) {
+ let pageSize = pageSizes[i];
+ check_size(
+ getDatabase,
+ new_file("shared" + pageSize),
+ pageSize,
+ kExpectedCacheSize
+ );
+ check_size(
+ Services.storage.openUnsharedDatabase,
+ new_file("unshared" + pageSize),
+ pageSize,
+ kExpectedCacheSize
+ );
+ }
+}
diff --git a/storage/test/unit/test_chunk_growth.js b/storage/test/unit/test_chunk_growth.js
new file mode 100644
index 0000000000..4bcdf86ce3
--- /dev/null
+++ b/storage/test/unit/test_chunk_growth.js
@@ -0,0 +1,51 @@
+// This file tests SQLITE_FCNTL_CHUNK_SIZE behaves as expected
+
+function run_sql(d, sql) {
+ var stmt = d.createStatement(sql);
+ stmt.execute();
+ stmt.finalize();
+}
+
+function new_file(name) {
+ var file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append(name);
+ return file;
+}
+
+function get_size(name) {
+ return new_file(name).fileSize;
+}
+
+function run_test() {
+ const filename = "chunked.sqlite";
+ const CHUNK_SIZE = 512 * 1024;
+ var d = getDatabase(new_file(filename));
+ try {
+ d.setGrowthIncrement(CHUNK_SIZE, "");
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_FILE_TOO_BIG) {
+ throw e;
+ }
+ print("Too little free space to set CHUNK_SIZE!");
+ return;
+ }
+ run_sql(d, "CREATE TABLE bloat(data varchar)");
+
+ var orig_size = get_size(filename);
+ /* Dump in at least 32K worth of data.
+ * While writing ensure that the file size growth in chunksize set above.
+ */
+ const str1024 = new Array(1024).join("T");
+ for (var i = 0; i < 32; i++) {
+ run_sql(d, "INSERT INTO bloat VALUES('" + str1024 + "')");
+ var size = get_size(filename);
+ // Must not grow in small increments.
+ Assert.ok(size == orig_size || size >= CHUNK_SIZE);
+ }
+ /* In addition to growing in chunk-size increments, the db
+ * should shrink in chunk-size increments too.
+ */
+ run_sql(d, "DELETE FROM bloat");
+ run_sql(d, "VACUUM");
+ Assert.ok(get_size(filename) >= CHUNK_SIZE);
+}
diff --git a/storage/test/unit/test_connection_asyncClose.js b/storage/test/unit/test_connection_asyncClose.js
new file mode 100644
index 0000000000..2d89087c5c
--- /dev/null
+++ b/storage/test/unit/test_connection_asyncClose.js
@@ -0,0 +1,128 @@
+/* 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/. */
+
+/*
+ * Thorough branch coverage for asyncClose.
+ *
+ * Coverage of asyncClose by connection state at time of AsyncClose invocation:
+ * - (asyncThread && mDBConn) => AsyncCloseConnection used, actually closes
+ * - test_asyncClose_does_not_complete_before_statements
+ * - test_double_asyncClose_throws
+ * - test_asyncClose_does_not_throw_without_callback
+ * - (asyncThread && !mDBConn) => AsyncCloseConnection used, although no close
+ * is required. Note that this is only possible in the event that
+ * openAsyncDatabase was used and we failed to open the database.
+ * Additionally, the async connection will never be exposed to the caller and
+ * AsyncInitDatabase will be the one to (automatically) call AsyncClose.
+ * - test_asyncClose_failed_open
+ * - (!asyncThread && mDBConn) => Close() invoked, actually closes
+ * - test_asyncClose_on_sync_db
+ * - (!asyncThread && !mDBConn) => Close() invoked, no close needed, errors.
+ * This happens if the database has already been closed.
+ * - test_double_asyncClose_throws
+ */
+
+/**
+ * Sanity check that our close indeed happens after asynchronously executed
+ * statements scheduled during the same turn of the event loop. Note that we
+ * just care that the statement says it completed without error, we're not
+ * worried that the close will happen and then the statement will magically
+ * complete.
+ */
+add_task(async function test_asyncClose_does_not_complete_before_statements() {
+ let db = Services.storage.openDatabase(getTestDB());
+ let stmt = db.createStatement("SELECT * FROM sqlite_master");
+ // Issue the executeAsync but don't yield for it...
+ let asyncStatementPromise = executeAsync(stmt);
+ stmt.finalize();
+
+ // Issue the close. (And now the order of yielding doesn't matter.)
+ // Branch coverage: (asyncThread && mDBConn)
+ await asyncClose(db);
+ equal(
+ await asyncStatementPromise,
+ Ci.mozIStorageStatementCallback.REASON_FINISHED
+ );
+});
+
+/**
+ * Open an async database (ensures the async thread is created) and then invoke
+ * AsyncClose() twice without yielding control flow. The first will initiate
+ * the actual async close after calling setClosedState which synchronously
+ * impacts what the second call will observe. The second call will then see the
+ * async thread is not available and fall back to invoking Close() which will
+ * notice the mDBConn is already gone.
+ */
+if (!AppConstants.DEBUG) {
+ add_task(async function test_double_asyncClose_throws() {
+ let db = await openAsyncDatabase(getTestDB());
+
+ // (Don't yield control flow yet, save the promise for after we make the
+ // second call.)
+ // Branch coverage: (asyncThread && mDBConn)
+ let realClosePromise = await asyncClose(db);
+ try {
+ // Branch coverage: (!asyncThread && !mDBConn)
+ db.asyncClose();
+ ok(false, "should have thrown");
+ } catch (e) {
+ equal(e.result, Cr.NS_ERROR_NOT_INITIALIZED);
+ }
+
+ await realClosePromise;
+ });
+}
+
+/**
+ * Create a sync db connection and never take it asynchronous and then call
+ * asyncClose on it. This will bring the async thread to life to perform the
+ * shutdown to avoid blocking the main thread, although we won't be able to
+ * tell the difference between this happening and the method secretly shunting
+ * to close().
+ */
+add_task(async function test_asyncClose_on_sync_db() {
+ let db = Services.storage.openDatabase(getTestDB());
+
+ // Branch coverage: (!asyncThread && mDBConn)
+ await asyncClose(db);
+ ok(true, "closed sync connection asynchronously");
+});
+
+/**
+ * Fail to asynchronously open a DB in order to get an async thread existing
+ * without having an open database and asyncClose invoked. As per the file
+ * doc-block, note that asyncClose will automatically be invoked by the
+ * AsyncInitDatabase when it fails to open the database. We will never be
+ * provided with a reference to the connection and so cannot call AsyncClose on
+ * it ourselves.
+ */
+add_task(async function test_asyncClose_failed_open() {
+ // This will fail and the promise will be rejected.
+ let openPromise = openAsyncDatabase(getFakeDB());
+ await openPromise.then(
+ () => {
+ ok(false, "we should have failed to open the db; this test is broken!");
+ },
+ () => {
+ ok(true, "correctly failed to open db; bg asyncClose should happen");
+ }
+ );
+ // (NB: we are unable to observe the thread shutdown, but since we never open
+ // a database, this test is not going to interfere with other tests so much.)
+});
+
+// THE TEST BELOW WANTS TO BE THE LAST TEST WE RUN. DO NOT MAKE IT SAD.
+/**
+ * Verify that asyncClose without a callback does not explode. Without a
+ * callback the shutdown is not actually observable, so we run this test last
+ * in order to avoid weird overlaps.
+ */
+add_task(async function test_asyncClose_does_not_throw_without_callback() {
+ let db = await openAsyncDatabase(getTestDB());
+ // Branch coverage: (asyncThread && mDBConn)
+ db.asyncClose();
+ ok(true, "if we shutdown cleanly and do not crash, then we succeeded");
+});
+// OBEY SHOUTING UPPER-CASE COMMENTS.
+// ADD TESTS ABOVE THE FORMER TEST, NOT BELOW IT.
diff --git a/storage/test/unit/test_connection_executeAsync.js b/storage/test/unit/test_connection_executeAsync.js
new file mode 100644
index 0000000000..a77299ba3b
--- /dev/null
+++ b/storage/test/unit/test_connection_executeAsync.js
@@ -0,0 +1,175 @@
+/* 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/. */
+
+/*
+ * This file tests the functionality of mozIStorageConnection::executeAsync for
+ * both mozIStorageStatement and mozIStorageAsyncStatement.
+ *
+ * A single database connection is used for the entirety of the test, which is
+ * a legacy thing, but we otherwise use the modern promise-based driver and
+ * async helpers.
+ */
+
+const INTEGER = 1;
+const TEXT = "this is test text";
+const REAL = 3.23;
+const BLOB = [1, 2];
+
+add_task(async function test_first_create_and_add() {
+ // synchronously open the database and let gDBConn hold onto it because we
+ // use this database
+ let db = getOpenedDatabase();
+ // synchronously set up our table *that will be used for the rest of the file*
+ db.executeSimpleSQL(
+ "CREATE TABLE test (" +
+ "id INTEGER, " +
+ "string TEXT, " +
+ "number REAL, " +
+ "nuller NULL, " +
+ "blober BLOB" +
+ ")"
+ );
+
+ let stmts = [];
+ stmts[0] = db.createStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) VALUES (?, ?, ?, ?, ?)"
+ );
+ stmts[0].bindByIndex(0, INTEGER);
+ stmts[0].bindByIndex(1, TEXT);
+ stmts[0].bindByIndex(2, REAL);
+ stmts[0].bindByIndex(3, null);
+ stmts[0].bindBlobByIndex(4, BLOB, BLOB.length);
+ stmts[1] = getOpenedDatabase().createAsyncStatement(
+ "INSERT INTO test (string, number, nuller, blober) VALUES (?, ?, ?, ?)"
+ );
+ stmts[1].bindByIndex(0, TEXT);
+ stmts[1].bindByIndex(1, REAL);
+ stmts[1].bindByIndex(2, null);
+ stmts[1].bindBlobByIndex(3, BLOB, BLOB.length);
+
+ // asynchronously execute the statements
+ let execResult = await executeMultipleStatementsAsync(
+ db,
+ stmts,
+ function (aResultSet) {
+ ok(false, "we only did inserts so we should not have gotten results!");
+ }
+ );
+ equal(
+ Ci.mozIStorageStatementCallback.REASON_FINISHED,
+ execResult,
+ "execution should have finished successfully."
+ );
+
+ // Check that the result is in the table
+ let stmt = db.createStatement(
+ "SELECT string, number, nuller, blober FROM test WHERE id = ?"
+ );
+ stmt.bindByIndex(0, INTEGER);
+ try {
+ Assert.ok(stmt.executeStep());
+ Assert.equal(TEXT, stmt.getString(0));
+ Assert.equal(REAL, stmt.getDouble(1));
+ Assert.ok(stmt.getIsNull(2));
+ let count = { value: 0 };
+ let blob = { value: null };
+ stmt.getBlob(3, count, blob);
+ Assert.equal(BLOB.length, count.value);
+ for (let i = 0; i < BLOB.length; i++) {
+ Assert.equal(BLOB[i], blob.value[i]);
+ }
+ } finally {
+ stmt.finalize();
+ }
+
+ // Make sure we have two rows in the table
+ stmt = db.createStatement("SELECT COUNT(1) FROM test");
+ try {
+ Assert.ok(stmt.executeStep());
+ Assert.equal(2, stmt.getInt32(0));
+ } finally {
+ stmt.finalize();
+ }
+
+ stmts[0].finalize();
+ stmts[1].finalize();
+});
+
+add_task(async function test_last_multiple_bindings_on_statements() {
+ // This tests to make sure that we pass all the statements multiply bound
+ // parameters when we call executeAsync.
+ const AMOUNT_TO_ADD = 5;
+ const ITERATIONS = 5;
+
+ let stmts = [];
+ let db = getOpenedDatabase();
+ let sqlString =
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (:int, :text, :real, :null, :blob)";
+ // We run the same statement twice, and should insert 2 * AMOUNT_TO_ADD.
+ for (let i = 0; i < ITERATIONS; i++) {
+ // alternate the type of statement we create
+ if (i % 2) {
+ stmts[i] = db.createStatement(sqlString);
+ } else {
+ stmts[i] = db.createAsyncStatement(sqlString);
+ }
+
+ let params = stmts[i].newBindingParamsArray();
+ for (let j = 0; j < AMOUNT_TO_ADD; j++) {
+ let bp = params.newBindingParams();
+ bp.bindByName("int", INTEGER);
+ bp.bindByName("text", TEXT);
+ bp.bindByName("real", REAL);
+ bp.bindByName("null", null);
+ bp.bindBlobByName("blob", BLOB);
+ params.addParams(bp);
+ }
+ stmts[i].bindParameters(params);
+ }
+
+ // Get our current number of rows in the table.
+ let currentRows = 0;
+ let countStmt = getOpenedDatabase().createStatement(
+ "SELECT COUNT(1) AS count FROM test"
+ );
+ try {
+ Assert.ok(countStmt.executeStep());
+ currentRows = countStmt.row.count;
+ } finally {
+ countStmt.reset();
+ }
+
+ // Execute asynchronously.
+ let execResult = await executeMultipleStatementsAsync(
+ db,
+ stmts,
+ function (aResultSet) {
+ ok(false, "we only did inserts so we should not have gotten results!");
+ }
+ );
+ equal(
+ Ci.mozIStorageStatementCallback.REASON_FINISHED,
+ execResult,
+ "execution should have finished successfully."
+ );
+
+ // Check to make sure we added all of our rows.
+ try {
+ Assert.ok(countStmt.executeStep());
+ Assert.equal(currentRows + ITERATIONS * AMOUNT_TO_ADD, countStmt.row.count);
+ } finally {
+ countStmt.finalize();
+ }
+
+ stmts.forEach(stmt => stmt.finalize());
+
+ // we are the last test using this connection and since it has gone async
+ // we *must* call asyncClose on it.
+ await asyncClose(db);
+ gDBConn = null;
+});
+
+// If you add a test down here you will need to move the asyncClose or clean
+// things up a little more.
diff --git a/storage/test/unit/test_connection_executeSimpleSQLAsync.js b/storage/test/unit/test_connection_executeSimpleSQLAsync.js
new file mode 100644
index 0000000000..00bdda7e03
--- /dev/null
+++ b/storage/test/unit/test_connection_executeSimpleSQLAsync.js
@@ -0,0 +1,94 @@
+/* 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/. */
+
+/*
+ * This file tests the functionality of
+ * mozIStorageAsyncConnection::executeSimpleSQLAsync.
+ */
+
+const INTEGER = 1;
+const TEXT = "this is test text";
+const REAL = 3.23;
+
+add_task(async function test_create_and_add() {
+ let adb = await openAsyncDatabase(getTestDB());
+
+ let completion = await executeSimpleSQLAsync(
+ adb,
+ "CREATE TABLE test (id INTEGER, string TEXT, number REAL)"
+ );
+
+ Assert.equal(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion);
+
+ completion = await executeSimpleSQLAsync(
+ adb,
+ "INSERT INTO test (id, string, number) " +
+ "VALUES (" +
+ INTEGER +
+ ', "' +
+ TEXT +
+ '", ' +
+ REAL +
+ ")"
+ );
+
+ Assert.equal(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion);
+
+ let result = null;
+
+ completion = await executeSimpleSQLAsync(
+ adb,
+ "SELECT string, number FROM test WHERE id = 1",
+ function (aResultSet) {
+ result = aResultSet.getNextRow();
+ Assert.equal(2, result.numEntries);
+ Assert.equal(TEXT, result.getString(0));
+ Assert.equal(REAL, result.getDouble(1));
+ }
+ );
+
+ Assert.equal(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion);
+ Assert.notEqual(result, null);
+ result = null;
+
+ await executeSimpleSQLAsync(
+ adb,
+ "SELECT COUNT(0) FROM test",
+ function (aResultSet) {
+ result = aResultSet.getNextRow();
+ Assert.equal(1, result.getInt32(0));
+ }
+ );
+
+ Assert.notEqual(result, null);
+
+ await asyncClose(adb);
+});
+
+add_task(async function test_asyncClose_does_not_complete_before_statement() {
+ let adb = await openAsyncDatabase(getTestDB());
+ let executed = false;
+
+ let reason = await executeSimpleSQLAsync(
+ adb,
+ "SELECT * FROM test",
+ function (aResultSet) {
+ let result = aResultSet.getNextRow();
+
+ Assert.notEqual(result, null);
+ Assert.equal(3, result.numEntries);
+ Assert.equal(INTEGER, result.getInt32(0));
+ Assert.equal(TEXT, result.getString(1));
+ Assert.equal(REAL, result.getDouble(2));
+ executed = true;
+ }
+ );
+
+ Assert.equal(Ci.mozIStorageStatementCallback.REASON_FINISHED, reason);
+
+ // Ensure that the statement executed to completion.
+ Assert.ok(executed);
+
+ await asyncClose(adb);
+});
diff --git a/storage/test/unit/test_connection_failsafe_close.js b/storage/test/unit/test_connection_failsafe_close.js
new file mode 100644
index 0000000000..d4fa6d1503
--- /dev/null
+++ b/storage/test/unit/test_connection_failsafe_close.js
@@ -0,0 +1,35 @@
+/* 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/. */
+
+/*
+ * This file tests edge-cases related to mozStorageService::unregisterConnection
+ * in the face of failsafe closing at destruction time which results in
+ * SpinningSynchronousClose being invoked which can "resurrect" the connection
+ * and result in a second call to unregisterConnection.
+ *
+ * See https://bugzilla.mozilla.org/show_bug.cgi?id=1413501 for more context.
+ */
+
+add_task(async function test_failsafe_close_of_async_connection() {
+ // get the db
+ let db = getOpenedDatabase();
+
+ // do something async
+ let callbackInvoked = new Promise(resolve => {
+ db.executeSimpleSQLAsync("CREATE TABLE test (id INTEGER)", {
+ handleCompletion: resolve,
+ });
+ });
+
+ // drop our reference and force a GC so the only live reference is owned by
+ // the async statement.
+ db = gDBConn = null;
+ // (we don't need to cycle collect)
+ Cu.forceGC();
+
+ // now we need to wait for that callback to have completed.
+ await callbackInvoked;
+
+ Assert.ok(true, "if we shutdown cleanly and do not crash, then we succeeded");
+});
diff --git a/storage/test/unit/test_connection_interrupt.js b/storage/test/unit/test_connection_interrupt.js
new file mode 100644
index 0000000000..e257e9af82
--- /dev/null
+++ b/storage/test/unit/test_connection_interrupt.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This file tests the functionality of mozIStorageAsyncConnection::interrupt
+// in the asynchronous case.
+add_task(async function test_wr_async_conn() {
+ // Interrupt cannot be used on R/W async connections.
+ let db = await openAsyncDatabase(getTestDB());
+ await db.interrupt();
+ info("should be able to interrupt a R/W async connection");
+ await asyncClose(db);
+});
+
+add_task(async function test_closed_conn() {
+ let db = await openAsyncDatabase(getTestDB(), { readOnly: true });
+ await asyncClose(db);
+ Assert.throws(
+ () => db.interrupt(),
+ /NS_ERROR_NOT_INITIALIZED/,
+ "interrupt() should throw if invoked on a closed connection"
+ );
+});
+
+add_task(
+ {
+ // We use a timeout in the test that may be insufficient on Android emulators.
+ // We don't really need the Android coverage, so skip on Android.
+ skip_if: () => AppConstants.platform == "android",
+ },
+ async function test_async_conn() {
+ let db = await openAsyncDatabase(getTestDB(), { readOnly: true });
+ // This query is built to hang forever.
+ let stmt = db.createAsyncStatement(`
+ WITH RECURSIVE test(n) AS (
+ VALUES(1)
+ UNION ALL
+ SELECT n + 1 FROM test
+ )
+ SELECT t.n
+ FROM test,test AS t`);
+
+ let completePromise = new Promise((resolve, reject) => {
+ let listener = {
+ handleResult(aResultSet) {
+ reject();
+ },
+ handleError(aError) {
+ reject();
+ },
+ handleCompletion(aReason) {
+ resolve(aReason);
+ },
+ };
+ stmt.executeAsync(listener);
+ stmt.finalize();
+ });
+
+ // Wait for the statement to be executing.
+ // This is not rock-solid, see the discussion in bug 1320301. A better
+ // approach will be evaluated in a separate bug.
+ await new Promise(resolve => do_timeout(500, resolve));
+
+ db.interrupt();
+
+ Assert.equal(
+ await completePromise,
+ Ci.mozIStorageStatementCallback.REASON_CANCELED,
+ "Should have been canceled"
+ );
+
+ await asyncClose(db);
+ }
+);
+
+add_task(
+ {
+ // We use a timeout in the test that may be insufficient on Android emulators.
+ // We don't really need the Android coverage, so skip on Android.
+ skip_if: () => AppConstants.platform == "android",
+ },
+ async function test_async_conn() {
+ let db = await openAsyncDatabase(getTestDB());
+ // This query is built to hang forever.
+ let stmt = db.createAsyncStatement(`
+ WITH RECURSIVE test(n) AS (
+ VALUES(1)
+ UNION ALL
+ SELECT n + 1 FROM test
+ )
+ SELECT t.n
+ FROM test,test AS t`);
+
+ let completePromise = new Promise((resolve, reject) => {
+ let listener = {
+ handleResult(aResultSet) {
+ reject();
+ },
+ handleError(aError) {
+ reject();
+ },
+ handleCompletion(aReason) {
+ resolve(aReason);
+ },
+ };
+ stmt.executeAsync(listener);
+ stmt.finalize();
+ });
+
+ // Wait for the statement to be executing.
+ // This is not rock-solid, see the discussion in bug 1320301. A better
+ // approach will be evaluated in a separate bug.
+ await new Promise(resolve => do_timeout(500, resolve));
+
+ // We are going to interrupt a database connection
+ db.interrupt();
+
+ Assert.equal(
+ await completePromise,
+ Ci.mozIStorageStatementCallback.REASON_CANCELED,
+ "Should have been able to cancel even for R/W database"
+ );
+
+ await asyncClose(db);
+ }
+);
diff --git a/storage/test/unit/test_connection_online_backup.js b/storage/test/unit/test_connection_online_backup.js
new file mode 100644
index 0000000000..3599d8c824
--- /dev/null
+++ b/storage/test/unit/test_connection_online_backup.js
@@ -0,0 +1,211 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test file tests the backupToFileAsync function on
+ * mozIStorageAsyncConnection, which is implemented for mozStorageConnection.
+ * (but not implemented for mozilla::dom::cache::Connection).
+ */
+
+// The name of the backup database file that will be created.
+const BACKUP_FILE_NAME = "test_storage.sqlite.backup";
+// The number of rows to insert into the test table in the source
+// database.
+const TEST_ROWS = 10;
+// The page size to set on the source database. During setup, we assert that
+// this does not match the default page size.
+const TEST_PAGE_SIZE = 512;
+
+/**
+ * This setup function creates a table inside of the test database and inserts
+ * some test rows. Critically, it keeps the connection to the database _open_
+ * so that we can test the scenario where a database is copied with existing
+ * open connections.
+ *
+ * The database is closed in a cleanup function.
+ */
+add_setup(async () => {
+ let conn = await openAsyncDatabase(getTestDB());
+
+ Assert.notEqual(
+ conn.defaultPageSize,
+ TEST_PAGE_SIZE,
+ "Should not default to having the TEST_PAGE_SIZE"
+ );
+
+ await executeSimpleSQLAsync(conn, "PRAGMA page_size = " + TEST_PAGE_SIZE);
+
+ let createStmt = conn.createAsyncStatement("CREATE TABLE test(name TEXT)");
+ await executeAsync(createStmt);
+ createStmt.finalize();
+
+ registerCleanupFunction(async () => {
+ await asyncClose(conn);
+ });
+});
+
+/**
+ * Erases the test table and inserts TEST_ROWS rows into it.
+ *
+ * @param {mozIStorageAsyncConnection} connection
+ * The connection to use to prepare the database.
+ * @returns {Promise<undefined>}
+ */
+async function prepareSourceDatabase(connection) {
+ await executeSimpleSQLAsync(connection, "DELETE from test");
+ for (let i = 0; i < TEST_ROWS; ++i) {
+ let name = `Database row #${i}`;
+ let stmt = connection.createAsyncStatement(
+ "INSERT INTO test (name) VALUES (:name)"
+ );
+ stmt.params.name = name;
+ let result = await executeAsync(stmt);
+ stmt.finalize();
+ Assert.ok(Components.isSuccessCode(result), `Inserted test row #${i}`);
+ }
+}
+
+/**
+ * Gets the test DB prepared with the testing table and rows.
+ *
+ * @returns {Promise<mozIStorageAsyncConnection>}
+ */
+async function getPreparedAsyncDatabase() {
+ let connection = await openAsyncDatabase(getTestDB());
+ await prepareSourceDatabase(connection);
+ return connection;
+}
+
+/**
+ * Creates a copy of the database connected to via connection, and
+ * returns an nsIFile pointing at the created copy file once the
+ * copy is complete.
+ *
+ * @param {mozIStorageAsyncConnection} connection
+ * A connection to a database that should be copied.
+ * @returns {Promise<nsIFile>}
+ */
+async function createCopy(connection) {
+ let destFilePath = PathUtils.join(PathUtils.profileDir, BACKUP_FILE_NAME);
+ let destFile = await IOUtils.getFile(destFilePath);
+ Assert.ok(
+ !(await IOUtils.exists(destFilePath)),
+ "Backup file shouldn't exist yet."
+ );
+
+ await new Promise(resolve => {
+ connection.backupToFileAsync(destFile, result => {
+ Assert.ok(Components.isSuccessCode(result));
+ resolve(result);
+ });
+ });
+
+ return destFile;
+}
+
+/**
+ * Opens up the database at file, asserts that the page_size matches
+ * TEST_PAGE_SIZE, and that the number of rows in the test table matches
+ * expectedEntries. Closes the connection after these assertions.
+ *
+ * @param {nsIFile} file
+ * The database file to be opened and queried.
+ * @param {number} [expectedEntries=TEST_ROWS]
+ * The expected number of rows in the test table. Defaults to TEST_ROWS.
+ * @returns {Promise<undefined>}
+ */
+async function assertSuccessfulCopy(file, expectedEntries = TEST_ROWS) {
+ let conn = await openAsyncDatabase(file);
+
+ await executeSimpleSQLAsync(conn, "PRAGMA page_size", resultSet => {
+ let result = resultSet.getNextRow();
+ Assert.equal(TEST_PAGE_SIZE, result.getResultByIndex(0).getAsUint32());
+ });
+
+ let stmt = conn.createAsyncStatement("SELECT COUNT(*) FROM test");
+ let results = await new Promise(resolve => {
+ executeAsync(stmt, resolve);
+ });
+ stmt.finalize();
+ let row = results.getNextRow();
+ let count = row.getResultByName("COUNT(*)");
+ Assert.equal(count, expectedEntries, "Got the expected entries");
+
+ Assert.ok(
+ !file.leafName.endsWith(".tmp"),
+ "Should not end in .tmp extension"
+ );
+
+ await asyncClose(conn);
+}
+
+/**
+ * Test the basic behaviour of backupToFileAsync, and ensure that the copied
+ * database has the same characteristics and contents as the source database.
+ */
+add_task(async function test_backupToFileAsync() {
+ let newConnection = await getPreparedAsyncDatabase();
+ let copyFile = await createCopy(newConnection);
+ Assert.ok(
+ await IOUtils.exists(copyFile.path),
+ "A new file was created by backupToFileAsync"
+ );
+
+ await assertSuccessfulCopy(copyFile);
+ await IOUtils.remove(copyFile.path);
+ await asyncClose(newConnection);
+});
+
+/**
+ * Tests that if insertions are underway during a copy, that those insertions
+ * show up in the copied database.
+ */
+add_task(async function test_backupToFileAsync_during_insert() {
+ let newConnection = await getPreparedAsyncDatabase();
+ const NEW_ENTRIES = 5;
+
+ let copyFilePromise = createCopy(newConnection);
+ let inserts = [];
+ for (let i = 0; i < NEW_ENTRIES; ++i) {
+ let name = `New database row #${i}`;
+ let stmt = newConnection.createAsyncStatement(
+ "INSERT INTO test (name) VALUES (:name)"
+ );
+ stmt.params.name = name;
+ inserts.push(executeAsync(stmt));
+ stmt.finalize();
+ }
+ await Promise.all(inserts);
+ let copyFile = await copyFilePromise;
+
+ Assert.ok(
+ await IOUtils.exists(copyFile.path),
+ "A new file was created by backupToFileAsync"
+ );
+
+ await assertSuccessfulCopy(copyFile, TEST_ROWS + NEW_ENTRIES);
+ await IOUtils.remove(copyFile.path);
+ await asyncClose(newConnection);
+});
+
+/**
+ * Tests the behaviour of backupToFileAsync as exposed through Sqlite.sys.mjs.
+ */
+add_task(async function test_backupToFileAsync_via_Sqlite_module() {
+ let xpcomConnection = await getPreparedAsyncDatabase();
+ let moduleConnection = await Sqlite.openConnection({
+ path: xpcomConnection.databaseFile.path,
+ });
+
+ let copyFilePath = PathUtils.join(PathUtils.profileDir, BACKUP_FILE_NAME);
+ await moduleConnection.backup(copyFilePath);
+ let copyFile = await IOUtils.getFile(copyFilePath);
+ Assert.ok(await IOUtils.exists(copyFilePath), "A new file was created");
+
+ await assertSuccessfulCopy(copyFile);
+ await IOUtils.remove(copyFile.path);
+ await moduleConnection.close();
+ await asyncClose(xpcomConnection);
+});
diff --git a/storage/test/unit/test_default_journal_size_limit.js b/storage/test/unit/test_default_journal_size_limit.js
new file mode 100644
index 0000000000..f2d28b9aa4
--- /dev/null
+++ b/storage/test/unit/test_default_journal_size_limit.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests defaul journal_size_limit
+
+async function check_journal_size(db) {
+ let stmt = db.createAsyncStatement("PRAGMA journal_size_limit");
+ let value = await new Promise((resolve, reject) => {
+ stmt.executeAsync({
+ handleResult(resultSet) {
+ resolve(resultSet.getNextRow().getResultByIndex(0));
+ },
+ handleError(error) {
+ reject();
+ },
+ handleCompletion() {},
+ });
+ });
+ Assert.greater(value, 0, "There is a positive journal_size_limit");
+ stmt.finalize();
+ await new Promise(resolve => db.asyncClose(resolve));
+}
+
+async function getDbPath(name) {
+ let path = PathUtils.join(PathUtils.profileDir, name + ".sqlite");
+ Assert.ok(!(await IOUtils.exists(path)));
+ return path;
+}
+
+add_task(async function () {
+ await check_journal_size(
+ Services.storage.openDatabase(
+ new FileUtils.File(await getDbPath("journal"))
+ )
+ );
+ await check_journal_size(
+ Services.storage.openUnsharedDatabase(
+ new FileUtils.File(await getDbPath("journalUnshared"))
+ )
+ );
+ await check_journal_size(
+ await openAsyncDatabase(new FileUtils.File(await getDbPath("journalAsync")))
+ );
+});
diff --git a/storage/test/unit/test_js_helpers.js b/storage/test/unit/test_js_helpers.js
new file mode 100644
index 0000000000..da533a78fc
--- /dev/null
+++ b/storage/test/unit/test_js_helpers.js
@@ -0,0 +1,150 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sw=2 ts=2 sts=2 et : */
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests that the JS language helpers in various ways.
+ */
+
+// Test Functions
+
+function test_params_enumerate() {
+ let stmt = createStatement("SELECT * FROM test WHERE id IN (:a, :b, :c)");
+
+ // Make sure they are right.
+ let expected = [0, 1, 2, "a", "b", "c", "length"];
+ let index = 0;
+ for (let name in stmt.params) {
+ if (name == "QueryInterface") {
+ continue;
+ }
+ Assert.equal(name, expected[index++]);
+ }
+ Assert.equal(index, 7);
+}
+
+function test_params_prototype() {
+ let stmt = createStatement("SELECT * FROM sqlite_master");
+
+ // Set a property on the prototype and make sure it exist (will not be a
+ // bindable parameter, however).
+ Object.getPrototypeOf(stmt.params).test = 2;
+ Assert.equal(stmt.params.test, 2);
+
+ delete Object.getPrototypeOf(stmt.params).test;
+ stmt.finalize();
+}
+
+function test_row_prototype() {
+ let stmt = createStatement("SELECT * FROM sqlite_master");
+
+ Assert.ok(stmt.executeStep());
+
+ // Set a property on the prototype and make sure it exists (will not be in the
+ // results, however).
+ Object.getPrototypeOf(stmt.row).test = 2;
+ Assert.equal(stmt.row.test, 2);
+
+ // Clean up after ourselves.
+ delete Object.getPrototypeOf(stmt.row).test;
+ stmt.finalize();
+}
+
+function test_row_enumerate() {
+ let stmt = createStatement("SELECT * FROM test");
+
+ Assert.ok(stmt.executeStep());
+
+ let expected = ["id", "string"];
+ let expected_values = [123, "foo"];
+ let index = 0;
+ for (let name in stmt.row) {
+ Assert.equal(name, expected[index]);
+ Assert.equal(stmt.row[name], expected_values[index]);
+ index++;
+ }
+ Assert.equal(index, 2);
+
+ // Save off the row helper, then forget the statement and trigger a GC. We
+ // want to ensure that if the row helper is retained but the statement is
+ // destroyed, that no crash occurs and that the late access attempt simply
+ // throws an error.
+ let savedOffRow = stmt.row;
+ stmt = null;
+ Cu.forceGC();
+ Assert.throws(
+ () => {
+ return savedOffRow.string;
+ },
+ /NS_ERROR_NOT_INITIALIZED/,
+ "GC'ed statement should throw"
+ );
+}
+
+function test_params_gets_sync() {
+ // Added for bug 562866.
+ /*
+ let stmt = createStatement(
+ "SELECT * FROM test WHERE id IN (:a, :b, :c)"
+ );
+
+ // Make sure we do not assert in getting the value.
+ let originalCount = Object.getOwnPropertyNames(stmt.params).length;
+ let expected = ["a", "b", "c"];
+ for (let name of expected) {
+ stmt.params[name];
+ }
+
+ // Now make sure we didn't magically get any additional properties.
+ let finalCount = Object.getOwnPropertyNames(stmt.params).length;
+ do_check_eq(originalCount + expected.length, finalCount);
+ */
+}
+
+function test_params_gets_async() {
+ // Added for bug 562866.
+ /*
+ let stmt = createAsyncStatement(
+ "SELECT * FROM test WHERE id IN (:a, :b, :c)"
+ );
+
+ // Make sure we do not assert in getting the value.
+ let originalCount = Object.getOwnPropertyNames(stmt.params).length;
+ let expected = ["a", "b", "c"];
+ for (let name of expected) {
+ stmt.params[name];
+ }
+
+ // Now make sure we didn't magically get any additional properties.
+ let finalCount = Object.getOwnPropertyNames(stmt.params).length;
+ do_check_eq(originalCount + expected.length, finalCount);
+ */
+}
+
+// Test Runner
+
+var tests = [
+ test_params_enumerate,
+ test_params_prototype,
+ test_row_enumerate,
+ test_row_prototype,
+ test_params_gets_sync,
+ test_params_gets_async,
+];
+function run_test() {
+ cleanup();
+
+ // Create our database.
+ getOpenedDatabase().executeSimpleSQL(
+ "CREATE TABLE test (id INTEGER PRIMARY KEY, string TEXT)"
+ );
+ getOpenedDatabase().executeSimpleSQL(
+ "INSERT INTO test (id, string) VALUES (123, 'foo')"
+ );
+
+ // Run the tests.
+ tests.forEach(test => test());
+}
diff --git a/storage/test/unit/test_levenshtein.js b/storage/test/unit/test_levenshtein.js
new file mode 100644
index 0000000000..a92b3a9559
--- /dev/null
+++ b/storage/test/unit/test_levenshtein.js
@@ -0,0 +1,66 @@
+/* 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/. */
+
+// This file tests the Levenshtein Distance function we've registered.
+
+function createUtf16Database() {
+ print("Creating the in-memory UTF-16-encoded database.");
+ let conn = Services.storage.openSpecialDatabase("memory");
+ conn.executeSimpleSQL("PRAGMA encoding = 'UTF-16'");
+
+ print("Make sure the encoding was set correctly and is now UTF-16.");
+ let stmt = conn.createStatement("PRAGMA encoding");
+ Assert.ok(stmt.executeStep());
+ let enc = stmt.getString(0);
+ stmt.finalize();
+
+ // The value returned will actually be UTF-16le or UTF-16be.
+ Assert.ok(enc === "UTF-16le" || enc === "UTF-16be");
+
+ return conn;
+}
+
+function check_levenshtein(db, s, t, expectedDistance) {
+ var stmt = db.createStatement("SELECT levenshteinDistance(:s, :t) AS result");
+ stmt.params.s = s;
+ stmt.params.t = t;
+ try {
+ Assert.ok(stmt.executeStep());
+ Assert.equal(expectedDistance, stmt.row.result);
+ } finally {
+ stmt.reset();
+ stmt.finalize();
+ }
+}
+
+function testLevenshtein(db) {
+ // Basic tests.
+ check_levenshtein(db, "", "", 0);
+ check_levenshtein(db, "foo", "", 3);
+ check_levenshtein(db, "", "bar", 3);
+ check_levenshtein(db, "yellow", "hello", 2);
+ check_levenshtein(db, "gumbo", "gambol", 2);
+ check_levenshtein(db, "kitten", "sitten", 1);
+ check_levenshtein(db, "sitten", "sittin", 1);
+ check_levenshtein(db, "sittin", "sitting", 1);
+ check_levenshtein(db, "kitten", "sitting", 3);
+ check_levenshtein(db, "Saturday", "Sunday", 3);
+ check_levenshtein(db, "YHCQPGK", "LAHYQQKPGKA", 6);
+
+ // Test SQL NULL handling.
+ check_levenshtein(db, "foo", null, null);
+ check_levenshtein(db, null, "bar", null);
+ check_levenshtein(db, null, null, null);
+
+ // The levenshteinDistance function allocates temporary memory on the stack
+ // if it can. Test some strings long enough to force a heap allocation.
+ var dots1000 = Array(1001).join(".");
+ var dashes1000 = Array(1001).join("-");
+ check_levenshtein(db, dots1000, dashes1000, 1000);
+}
+
+function run_test() {
+ testLevenshtein(getOpenedDatabase());
+ testLevenshtein(createUtf16Database());
+}
diff --git a/storage/test/unit/test_like.js b/storage/test/unit/test_like.js
new file mode 100644
index 0000000000..91674a8816
--- /dev/null
+++ b/storage/test/unit/test_like.js
@@ -0,0 +1,199 @@
+/* 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/. */
+
+// This file tests our LIKE implementation since we override it for unicode
+
+function setup() {
+ getOpenedDatabase().createTable("t1", "x TEXT");
+
+ var stmt = createStatement("INSERT INTO t1 (x) VALUES ('a')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('ab')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('abc')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('abcd')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('acd')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('abd')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('bc')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('bcd')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('xyz')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('ABC')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('CDE')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('ABC abc xyz')");
+ stmt.execute();
+ stmt.finalize();
+}
+
+function test_count() {
+ var stmt = createStatement("SELECT count(*) FROM t1;");
+ Assert.ok(stmt.executeStep());
+ Assert.equal(stmt.getInt32(0), 12);
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_like_1() {
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;");
+ stmt.bindByIndex(0, "abc");
+ var solutions = ["abc", "ABC"];
+ Assert.ok(stmt.executeStep());
+ Assert.ok(solutions.includes(stmt.getString(0)));
+ Assert.ok(stmt.executeStep());
+ Assert.ok(solutions.includes(stmt.getString(0)));
+ Assert.ok(!stmt.executeStep());
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_like_2() {
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;");
+ stmt.bindByIndex(0, "ABC");
+ var solutions = ["abc", "ABC"];
+ Assert.ok(stmt.executeStep());
+ Assert.ok(solutions.includes(stmt.getString(0)));
+ Assert.ok(stmt.executeStep());
+ Assert.ok(solutions.includes(stmt.getString(0)));
+ Assert.ok(!stmt.executeStep());
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_like_3() {
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;");
+ stmt.bindByIndex(0, "aBc");
+ var solutions = ["abc", "ABC"];
+ Assert.ok(stmt.executeStep());
+ Assert.ok(solutions.includes(stmt.getString(0)));
+ Assert.ok(stmt.executeStep());
+ Assert.ok(solutions.includes(stmt.getString(0)));
+ Assert.ok(!stmt.executeStep());
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_like_4() {
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;");
+ stmt.bindByIndex(0, "abc%");
+ var solutions = ["abc", "abcd", "ABC", "ABC abc xyz"];
+ Assert.ok(stmt.executeStep());
+ Assert.ok(solutions.includes(stmt.getString(0)));
+ Assert.ok(stmt.executeStep());
+ Assert.ok(solutions.includes(stmt.getString(0)));
+ Assert.ok(stmt.executeStep());
+ Assert.ok(solutions.includes(stmt.getString(0)));
+ Assert.ok(stmt.executeStep());
+ Assert.ok(solutions.includes(stmt.getString(0)));
+ Assert.ok(!stmt.executeStep());
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_like_5() {
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;");
+ stmt.bindByIndex(0, "a_c");
+ var solutions = ["abc", "ABC"];
+ Assert.ok(stmt.executeStep());
+ Assert.ok(solutions.includes(stmt.getString(0)));
+ Assert.ok(stmt.executeStep());
+ Assert.ok(solutions.includes(stmt.getString(0)));
+ Assert.ok(!stmt.executeStep());
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_like_6() {
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;");
+ stmt.bindByIndex(0, "ab%d");
+ var solutions = ["abcd", "abd"];
+ Assert.ok(stmt.executeStep());
+ Assert.ok(solutions.includes(stmt.getString(0)));
+ Assert.ok(stmt.executeStep());
+ Assert.ok(solutions.includes(stmt.getString(0)));
+ Assert.ok(!stmt.executeStep());
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_like_7() {
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;");
+ stmt.bindByIndex(0, "a_c%");
+ var solutions = ["abc", "abcd", "ABC", "ABC abc xyz"];
+ Assert.ok(stmt.executeStep());
+ Assert.ok(solutions.includes(stmt.getString(0)));
+ Assert.ok(stmt.executeStep());
+ Assert.ok(solutions.includes(stmt.getString(0)));
+ Assert.ok(stmt.executeStep());
+ Assert.ok(solutions.includes(stmt.getString(0)));
+ Assert.ok(stmt.executeStep());
+ Assert.ok(solutions.includes(stmt.getString(0)));
+ Assert.ok(!stmt.executeStep());
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_like_8() {
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;");
+ stmt.bindByIndex(0, "%bcd");
+ var solutions = ["abcd", "bcd"];
+ Assert.ok(stmt.executeStep());
+ Assert.ok(solutions.includes(stmt.getString(0)));
+ Assert.ok(stmt.executeStep());
+ Assert.ok(solutions.includes(stmt.getString(0)));
+ Assert.ok(!stmt.executeStep());
+ stmt.reset();
+ stmt.finalize();
+}
+
+var tests = [
+ test_count,
+ test_like_1,
+ test_like_2,
+ test_like_3,
+ test_like_4,
+ test_like_5,
+ test_like_6,
+ test_like_7,
+ test_like_8,
+];
+
+function run_test() {
+ setup();
+
+ for (var i = 0; i < tests.length; i++) {
+ tests[i]();
+ }
+
+ cleanup();
+}
diff --git a/storage/test/unit/test_like_escape.js b/storage/test/unit/test_like_escape.js
new file mode 100644
index 0000000000..66d6d1d583
--- /dev/null
+++ b/storage/test/unit/test_like_escape.js
@@ -0,0 +1,72 @@
+/* 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/. */
+
+const LATIN1_AE = "\xc6";
+const LATIN1_ae = "\xe6";
+
+function setup() {
+ getOpenedDatabase().createTable("t1", "x TEXT");
+
+ var stmt = createStatement(
+ "INSERT INTO t1 (x) VALUES ('foo/bar_baz%20cheese')"
+ );
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES (?1)");
+ // insert LATIN_ae, but search on LATIN_AE
+ stmt.bindByIndex(0, "foo%20" + LATIN1_ae + "/_bar");
+ stmt.execute();
+ stmt.finalize();
+}
+
+function test_escape_for_like_ascii() {
+ const paramForLike = "oo/bar_baz%20chees";
+ const escapeChar = "/";
+
+ for (const utf8 of [false, true]) {
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?1 ESCAPE '/'");
+ var paramForLikeEscaped = utf8
+ ? stmt.escapeUTF8StringForLIKE(paramForLike, escapeChar)
+ : stmt.escapeStringForLIKE(paramForLike, escapeChar);
+ // verify that we escaped / _ and %
+ Assert.equal(paramForLikeEscaped, "oo//bar/_baz/%20chees");
+ // prepend and append with % for "contains"
+ stmt.bindByIndex(0, "%" + paramForLikeEscaped + "%");
+ stmt.executeStep();
+ Assert.equal("foo/bar_baz%20cheese", stmt.getString(0));
+ stmt.finalize();
+ }
+}
+
+function test_escape_for_like_non_ascii() {
+ const paramForLike = "oo%20" + LATIN1_AE + "/_ba";
+ const escapeChar = "/";
+
+ for (const utf8 of [false, true]) {
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?1 ESCAPE '/'");
+ var paramForLikeEscaped = utf8
+ ? stmt.escapeUTF8StringForLIKE(paramForLike, escapeChar)
+ : stmt.escapeStringForLIKE(paramForLike, escapeChar);
+ // verify that we escaped / _ and %
+ Assert.equal(paramForLikeEscaped, "oo/%20" + LATIN1_AE + "///_ba");
+ // prepend and append with % for "contains"
+ stmt.bindByIndex(0, "%" + paramForLikeEscaped + "%");
+ stmt.executeStep();
+ Assert.equal("foo%20" + LATIN1_ae + "/_bar", stmt.getString(0));
+ stmt.finalize();
+ }
+}
+
+var tests = [test_escape_for_like_ascii, test_escape_for_like_non_ascii];
+
+function run_test() {
+ setup();
+
+ for (var i = 0; i < tests.length; i++) {
+ tests[i]();
+ }
+
+ cleanup();
+}
diff --git a/storage/test/unit/test_locale_collation.js b/storage/test/unit/test_locale_collation.js
new file mode 100644
index 0000000000..96f2415451
--- /dev/null
+++ b/storage/test/unit/test_locale_collation.js
@@ -0,0 +1,291 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+/**
+ * Bug 499990 - Locale-aware collation
+ *
+ * Tests our custom, locale-aware collating sequences.
+ */
+
+// The name of the file containing the strings we'll sort during this test.
+// The file's data is taken from intl/locale/tests/sort/us-ascii_base.txt and
+// and intl/locale/tests/sort/us-ascii_sort.txt.
+const DATA_BASENAME = "locale_collation.txt";
+
+// The test data from DATA_BASENAME is read into this array.
+var gStrings;
+
+// A connection to our in-memory UTF-16-encoded database.
+var gUtf16Conn;
+
+// Helper Functions
+
+/**
+ * Since we create a UTF-16 database we have to clean it up, in addition to
+ * the normal cleanup of Storage tests.
+ */
+function cleanupLocaleTests() {
+ print("-- Cleaning up test_locale_collation.js suite.");
+ gUtf16Conn.close();
+ cleanup();
+}
+
+/**
+ * Creates a test database similar to the default one created in
+ * head_storage.js, except that this one uses UTF-16 encoding.
+ *
+ * @return A connection to the database.
+ */
+function createUtf16Database() {
+ print("Creating the in-memory UTF-16-encoded database.");
+ let conn = Services.storage.openSpecialDatabase("memory");
+ conn.executeSimpleSQL("PRAGMA encoding = 'UTF-16'");
+
+ print("Make sure the encoding was set correctly and is now UTF-16.");
+ let stmt = conn.createStatement("PRAGMA encoding");
+ Assert.ok(stmt.executeStep());
+ let enc = stmt.getString(0);
+ stmt.finalize();
+
+ // The value returned will actually be UTF-16le or UTF-16be.
+ Assert.ok(enc === "UTF-16le" || enc === "UTF-16be");
+
+ return conn;
+}
+
+/**
+ * Compares aActual to aExpected, ensuring that the numbers and orderings of
+ * the two arrays' elements are the same.
+ *
+ * @param aActual
+ * An array of strings retrieved from the database.
+ * @param aExpected
+ * An array of strings to which aActual should be equivalent.
+ */
+function ensureResultsAreCorrect(aActual, aExpected) {
+ print("Actual results: " + aActual);
+ print("Expected results: " + aExpected);
+
+ Assert.equal(aActual.length, aExpected.length);
+ for (let i = 0; i < aActual.length; i++) {
+ Assert.equal(aActual[i], aExpected[i]);
+ }
+}
+
+/**
+ * Synchronously SELECTs all rows from the test table of the given database
+ * using the given collation.
+ *
+ * @param aCollation
+ * The name of one of our custom locale collations. The rows are
+ * ordered by this collation.
+ * @param aConn
+ * A connection to either the UTF-8 database or the UTF-16 database.
+ * @return The resulting strings in an array.
+ */
+function getResults(aCollation, aConn) {
+ let results = [];
+ let stmt = aConn.createStatement(
+ "SELECT t FROM test ORDER BY t COLLATE " + aCollation + " ASC"
+ );
+ while (stmt.executeStep()) {
+ results.push(stmt.row.t);
+ }
+ stmt.finalize();
+ return results;
+}
+
+/**
+ * Inserts strings into our test table of the given database in the order given.
+ *
+ * @param aStrings
+ * An array of strings.
+ * @param aConn
+ * A connection to either the UTF-8 database or the UTF-16 database.
+ */
+function initTableWithStrings(aStrings, aConn) {
+ print("Initializing test table.");
+
+ aConn.executeSimpleSQL("DROP TABLE IF EXISTS test");
+ aConn.createTable("test", "t TEXT");
+ let stmt = aConn.createStatement("INSERT INTO test (t) VALUES (:t)");
+ aStrings.forEach(function (str) {
+ stmt.params.t = str;
+ stmt.execute();
+ stmt.reset();
+ });
+ stmt.finalize();
+}
+
+/**
+ * Returns a sorting function suitable for passing to Array.prototype.sort().
+ * The returned function uses the application's locale to compare strings.
+ *
+ * @param aCollation
+ * The name of one of our custom locale collations. The sorting
+ * strength is computed from this value.
+ * @return A function to use as a sorting callback.
+ */
+function localeCompare(aCollation) {
+ let sensitivity;
+
+ switch (aCollation) {
+ case "locale":
+ sensitivity = "base";
+ break;
+ case "locale_case_sensitive":
+ sensitivity = "case";
+ break;
+ case "locale_accent_sensitive":
+ sensitivity = "accent";
+ break;
+ case "locale_case_accent_sensitive":
+ sensitivity = "variant";
+ break;
+ default:
+ do_throw("Error in test: unknown collation '" + aCollation + "'");
+ break;
+ }
+ const collation = new Intl.Collator("en", { sensitivity });
+ return function (aStr1, aStr2) {
+ return collation.compare(aStr1, aStr2);
+ };
+}
+
+/**
+ * Reads in the test data from the file DATA_BASENAME and returns it as an array
+ * of strings.
+ *
+ * @return The test data as an array of strings.
+ */
+function readTestData() {
+ print("Reading in test data.");
+
+ let file = do_get_file(DATA_BASENAME);
+
+ let istream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ istream.init(file, -1, -1, 0);
+ istream.QueryInterface(Ci.nsILineInputStream);
+
+ let line = {};
+ let lines = [];
+ while (istream.readLine(line)) {
+ lines.push(line.value);
+ }
+ istream.close();
+
+ return lines;
+}
+
+/**
+ * Gets the results from the given database using the given collation and
+ * ensures that they match gStrings sorted by the same collation.
+ *
+ * @param aCollation
+ * The name of one of our custom locale collations. The rows from the
+ * database and the expected results are ordered by this collation.
+ * @param aConn
+ * A connection to either the UTF-8 database or the UTF-16 database.
+ */
+function runTest(aCollation, aConn) {
+ ensureResultsAreCorrect(
+ getResults(aCollation, aConn),
+ gStrings.slice(0).sort(localeCompare(aCollation))
+ );
+}
+
+/**
+ * Gets the results from the UTF-8 database using the given collation and
+ * ensures that they match gStrings sorted by the same collation.
+ *
+ * @param aCollation
+ * The name of one of our custom locale collations. The rows from the
+ * database and the expected results are ordered by this collation.
+ */
+function runUtf8Test(aCollation) {
+ runTest(aCollation, getOpenedDatabase());
+}
+
+/**
+ * Gets the results from the UTF-16 database using the given collation and
+ * ensures that they match gStrings sorted by the same collation.
+ *
+ * @param aCollation
+ * The name of one of our custom locale collations. The rows from the
+ * database and the expected results are ordered by this collation.
+ */
+function runUtf16Test(aCollation) {
+ runTest(aCollation, gUtf16Conn);
+}
+
+/**
+ * Sets up the test suite.
+ */
+function setup() {
+ print("-- Setting up the test_locale_collation.js suite.");
+
+ gStrings = readTestData();
+
+ initTableWithStrings(gStrings, getOpenedDatabase());
+
+ gUtf16Conn = createUtf16Database();
+ initTableWithStrings(gStrings, gUtf16Conn);
+}
+
+// Test Runs
+
+var gTests = [
+ {
+ desc: "Case and accent sensitive UTF-8",
+ run: () => runUtf8Test("locale_case_accent_sensitive"),
+ },
+
+ {
+ desc: "Case sensitive, accent insensitive UTF-8",
+ run: () => runUtf8Test("locale_case_sensitive"),
+ },
+
+ {
+ desc: "Case insensitive, accent sensitive UTF-8",
+ run: () => runUtf8Test("locale_accent_sensitive"),
+ },
+
+ {
+ desc: "Case and accent insensitive UTF-8",
+ run: () => runUtf8Test("locale"),
+ },
+
+ {
+ desc: "Case and accent sensitive UTF-16",
+ run: () => runUtf16Test("locale_case_accent_sensitive"),
+ },
+
+ {
+ desc: "Case sensitive, accent insensitive UTF-16",
+ run: () => runUtf16Test("locale_case_sensitive"),
+ },
+
+ {
+ desc: "Case insensitive, accent sensitive UTF-16",
+ run: () => runUtf16Test("locale_accent_sensitive"),
+ },
+
+ {
+ desc: "Case and accent insensitive UTF-16",
+ run: () => runUtf16Test("locale"),
+ },
+];
+
+function run_test() {
+ setup();
+ gTests.forEach(function (test) {
+ print("-- Running test: " + test.desc);
+ test.run();
+ });
+ cleanupLocaleTests();
+}
diff --git a/storage/test/unit/test_minimizeMemory.js b/storage/test/unit/test_minimizeMemory.js
new file mode 100644
index 0000000000..e694ddc186
--- /dev/null
+++ b/storage/test/unit/test_minimizeMemory.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This file tests that invoking `Service::minimizeMemory` succeeds for sync
+// and async connections.
+
+function minimizeMemory() {
+ Services.storage
+ .QueryInterface(Ci.nsIObserver)
+ .observe(null, "memory-pressure", null);
+}
+
+add_task(async function test_minimizeMemory_async_connection() {
+ let db = await openAsyncDatabase(getTestDB());
+ minimizeMemory();
+ await asyncClose(db);
+});
+
+add_task(async function test_minimizeMemory_sync_connection() {
+ let db = getOpenedDatabase();
+ minimizeMemory();
+ db.close();
+});
diff --git a/storage/test/unit/test_page_size_is_32k.js b/storage/test/unit/test_page_size_is_32k.js
new file mode 100644
index 0000000000..4a91d13fdc
--- /dev/null
+++ b/storage/test/unit/test_page_size_is_32k.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This file tests that dbs are using 32k pagesize
+
+const kExpectedPageSize = 32768; // 32K
+const kExpectedCacheSize = -2048; // 2MiB
+
+function check_size(db) {
+ var stmt = db.createStatement("PRAGMA page_size");
+ stmt.executeStep();
+ Assert.equal(stmt.getInt32(0), kExpectedPageSize);
+ stmt.finalize();
+ stmt = db.createStatement("PRAGMA cache_size");
+ stmt.executeStep();
+ Assert.equal(stmt.getInt32(0), kExpectedCacheSize);
+ stmt.finalize();
+}
+
+function new_file(name) {
+ var file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append(name + ".sqlite");
+ Assert.ok(!file.exists());
+ return file;
+}
+
+function run_test() {
+ check_size(getDatabase(new_file("shared32k")));
+ check_size(Services.storage.openUnsharedDatabase(new_file("unshared32k")));
+}
diff --git a/storage/test/unit/test_persist_journal.js b/storage/test/unit/test_persist_journal.js
new file mode 100644
index 0000000000..1eea5c703e
--- /dev/null
+++ b/storage/test/unit/test_persist_journal.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the journal persists on close.
+
+async function check_journal_persists(db, journal) {
+ let path = db.databaseFile.path;
+ info(`testing ${path}`);
+ await new Promise(resolve => {
+ db.executeSimpleSQLAsync(`PRAGMA journal_mode = ${journal}`, {
+ handleCompletion: resolve,
+ });
+ });
+
+ await new Promise(resolve => {
+ db.executeSimpleSQLAsync("CREATE TABLE test (id INTEGER PRIMARY KEY)", {
+ handleCompletion: resolve,
+ });
+ });
+
+ if (journal == "wal") {
+ Assert.ok(await IOUtils.exists(path + "-wal"), "-wal exists before close");
+ Assert.greater(
+ (await IOUtils.stat(path + "-wal")).size,
+ 0,
+ "-wal size is non-zero"
+ );
+ } else {
+ Assert.ok(
+ await IOUtils.exists(path + "-journal"),
+ "-journal exists before close"
+ );
+ Assert.equal(
+ (await IOUtils.stat(path + "-journal")).size,
+ 0,
+ "-journal is truncated after every transaction"
+ );
+ }
+
+ await new Promise(resolve => db.asyncClose(resolve));
+
+ if (journal == "wal") {
+ Assert.ok(await IOUtils.exists(path + "-wal"), "-wal persists after close");
+ Assert.equal(
+ (await IOUtils.stat(path + "-wal")).size,
+ 0,
+ "-wal has been truncated"
+ );
+ } else {
+ Assert.ok(
+ await IOUtils.exists(path + "-journal"),
+ "-journal persists after close"
+ );
+ Assert.equal(
+ (await IOUtils.stat(path + "-journal")).size,
+ 0,
+ "-journal has been truncated"
+ );
+ }
+}
+
+async function getDbPath(name) {
+ let path = PathUtils.join(PathUtils.profileDir, name + ".sqlite");
+ Assert.ok(!(await IOUtils.exists(path)), "database should not exist");
+ return path;
+}
+
+add_task(async function () {
+ for (let journal of ["truncate", "wal"]) {
+ await check_journal_persists(
+ Services.storage.openDatabase(
+ new FileUtils.File(await getDbPath(`shared-${journal}`))
+ ),
+ journal
+ );
+ await check_journal_persists(
+ Services.storage.openUnsharedDatabase(
+ new FileUtils.File(await getDbPath(`unshared-${journal}`))
+ ),
+ journal
+ );
+ await check_journal_persists(
+ await openAsyncDatabase(
+ new FileUtils.File(await getDbPath(`async-${journal}`))
+ ),
+ journal
+ );
+ }
+});
diff --git a/storage/test/unit/test_readonly-immutable-nolock_vfs.js b/storage/test/unit/test_readonly-immutable-nolock_vfs.js
new file mode 100644
index 0000000000..5c11150b0a
--- /dev/null
+++ b/storage/test/unit/test_readonly-immutable-nolock_vfs.js
@@ -0,0 +1,47 @@
+/* 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/. */
+
+// This file tests the readonly-immutable-nolock VFS.
+
+add_task(async function test() {
+ const path = PathUtils.join(PathUtils.profileDir, "ro");
+ await IOUtils.makeDirectory(path);
+ const dbpath = PathUtils.join(path, "test-immutable.sqlite");
+
+ let conn = await Sqlite.openConnection({ path: dbpath });
+ await conn.execute("PRAGMA journal_mode = WAL");
+ await conn.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)");
+ Assert.ok(await IOUtils.exists(dbpath + "-wal"), "wal journal exists");
+ await conn.close();
+
+ // The wal should have been merged at this point, but just in case...
+ info("Remove auxiliary files and set the folder as readonly");
+ await IOUtils.remove(dbpath + "-wal", { ignoreAbsent: true });
+ await IOUtils.setPermissions(path, 0o555);
+ registerCleanupFunction(async () => {
+ await IOUtils.setPermissions(path, 0o777);
+ await IOUtils.remove(path, { recursive: true });
+ });
+
+ // Windows doesn't disallow creating files in read only folders.
+ if (AppConstants.platform == "macosx" || AppConstants.platform == "linux") {
+ await Assert.rejects(
+ Sqlite.openConnection({ path: dbpath, readOnly: true }),
+ /NS_ERROR_FILE/,
+ "Should not be able to open the db because it can't create a wal journal"
+ );
+ }
+
+ // Open the database with ignoreLockingMode.
+ let conn2 = await Sqlite.openConnection({
+ path: dbpath,
+ ignoreLockingMode: true,
+ });
+ await conn2.execute("SELECT * FROM sqlite_master");
+ Assert.ok(
+ !(await IOUtils.exists(dbpath + "-wal")),
+ "wal journal was not created"
+ );
+ await conn2.close();
+});
diff --git a/storage/test/unit/test_retry_on_busy.js b/storage/test/unit/test_retry_on_busy.js
new file mode 100644
index 0000000000..bf7cdafdad
--- /dev/null
+++ b/storage/test/unit/test_retry_on_busy.js
@@ -0,0 +1,199 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+function getProfileFile(name) {
+ let file = do_get_profile();
+ file.append(name);
+ return file;
+}
+
+function promiseAsyncDatabase(name, openOptions = 0) {
+ return new Promise((resolve, reject) => {
+ let file = getProfileFile(name);
+ const connOptions = Ci.mozIStorageService.CONNECTION_DEFAULT;
+ Services.storage.openAsyncDatabase(
+ file,
+ openOptions,
+ connOptions,
+ (status, connection) => {
+ if (!Components.isSuccessCode(status)) {
+ reject(new Error(`Failed to open database: ${status}`));
+ } else {
+ connection.QueryInterface(Ci.mozIStorageAsyncConnection);
+ resolve(connection);
+ }
+ }
+ );
+ });
+}
+
+function promiseClone(db, readOnly = false) {
+ return new Promise((resolve, reject) => {
+ db.asyncClone(readOnly, (status, clone) => {
+ if (!Components.isSuccessCode(status)) {
+ reject(new Error(`Failed to clone connection: ${status}`));
+ } else {
+ clone.QueryInterface(Ci.mozIStorageAsyncConnection);
+ resolve(clone);
+ }
+ });
+ });
+}
+
+function promiseClose(db) {
+ return new Promise((resolve, reject) => {
+ db.asyncClose(status => {
+ if (!Components.isSuccessCode(status)) {
+ reject(new Error(`Failed to close connection: ${status}`));
+ } else {
+ resolve();
+ }
+ });
+ });
+}
+
+function promiseExecuteStatement(statement) {
+ return new Promise((resolve, reject) => {
+ let rows = [];
+ statement.executeAsync({
+ handleResult(resultSet) {
+ let row = null;
+ do {
+ row = resultSet.getNextRow();
+ if (row) {
+ rows.push(row);
+ }
+ } while (row);
+ },
+ handleError(error) {
+ reject(new Error(`Failed to execute statement: ${error.message}`));
+ },
+ handleCompletion(reason) {
+ if (reason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ resolve(rows);
+ } else {
+ reject(new Error("Statement failed to execute or was cancelled"));
+ }
+ },
+ });
+ });
+}
+
+add_task(async function test_retry_on_busy() {
+ info("Open first writer in WAL mode and set up schema");
+ let db1 = await promiseAsyncDatabase("retry-on-busy.sqlite");
+
+ let walStmt = db1.createAsyncStatement(`PRAGMA journal_mode = WAL`);
+ await promiseExecuteStatement(walStmt);
+ let createAStmt = db1.createAsyncStatement(`CREATE TABLE a(
+ b INTEGER PRIMARY KEY
+ )`);
+ await promiseExecuteStatement(createAStmt);
+ let createPrevAStmt = db1.createAsyncStatement(`CREATE TEMP TABLE prevA(
+ curB INTEGER PRIMARY KEY,
+ prevB INTEGER NOT NULL
+ )`);
+ await promiseExecuteStatement(createPrevAStmt);
+ let createATriggerStmt = db1.createAsyncStatement(`
+ CREATE TEMP TRIGGER a_afterinsert_trigger
+ AFTER UPDATE ON a FOR EACH ROW
+ BEGIN
+ REPLACE INTO prevA(curB, prevB) VALUES(NEW.b, OLD.b);
+ END`);
+ await promiseExecuteStatement(createATriggerStmt);
+
+ info("Open second writer");
+ let db2 = await promiseClone(db1);
+
+ info("Attach second writer to new database");
+ let attachStmt = db2.createAsyncStatement(`ATTACH :path AS newDB`);
+ attachStmt.bindByName(
+ "path",
+ getProfileFile("retry-on-busy-attach.sqlite").path
+ );
+ await promiseExecuteStatement(attachStmt);
+
+ info("Create triggers on second writer");
+ let createCStmt = db2.createAsyncStatement(`CREATE TABLE newDB.c(
+ d INTEGER PRIMARY KEY
+ )`);
+ await promiseExecuteStatement(createCStmt);
+ let createCTriggerStmt = db2.createAsyncStatement(`
+ CREATE TEMP TRIGGER c_afterdelete_trigger
+ AFTER DELETE ON c FOR EACH ROW
+ BEGIN
+ INSERT INTO a(b) VALUES(OLD.d);
+ END`);
+ await promiseExecuteStatement(createCTriggerStmt);
+
+ info("Begin transaction on second writer");
+ let begin2Stmt = db2.createAsyncStatement("BEGIN IMMEDIATE");
+ await promiseExecuteStatement(begin2Stmt);
+
+ info(
+ "Begin transaction on first writer; should busy-wait until second writer is done"
+ );
+ let begin1Stmt = db1.createAsyncStatement("BEGIN IMMEDIATE");
+ let promise1Began = promiseExecuteStatement(begin1Stmt);
+ let update1Stmt = db1.createAsyncStatement(`UPDATE a SET b = 3 WHERE b = 1`);
+ let promise1Updated = promiseExecuteStatement(update1Stmt);
+
+ info("Wait 5 seconds");
+ await new Promise(resolve => setTimeout(resolve, 5000));
+
+ info("Commit transaction on second writer");
+ let insertIntoA2Stmt = db2.createAsyncStatement(`INSERT INTO a(b) VALUES(1)`);
+ await promiseExecuteStatement(insertIntoA2Stmt);
+ let insertIntoC2Stmt = db2.createAsyncStatement(`INSERT INTO c(d) VALUES(2)`);
+ await promiseExecuteStatement(insertIntoC2Stmt);
+ let deleteFromC2Stmt = db2.createAsyncStatement(`DELETE FROM c`);
+ await promiseExecuteStatement(deleteFromC2Stmt);
+ let commit2Stmt = db2.createAsyncStatement("COMMIT");
+ await promiseExecuteStatement(commit2Stmt);
+
+ info("Await and commit transaction on first writer");
+ await promise1Began;
+ await promise1Updated;
+
+ let commit1Stmt = db1.createAsyncStatement("COMMIT");
+ await promiseExecuteStatement(commit1Stmt);
+
+ info("Verify our writes succeeded");
+ let select1Stmt = db2.createAsyncStatement("SELECT b FROM a");
+ let rows = await promiseExecuteStatement(select1Stmt);
+ deepEqual(
+ rows.map(row => row.getResultByName("b")),
+ [2, 3]
+ );
+
+ info("Clean up");
+ for (let stmt of [
+ walStmt,
+ createAStmt,
+ createPrevAStmt,
+ createATriggerStmt,
+ attachStmt,
+ createCStmt,
+ createCTriggerStmt,
+ begin2Stmt,
+ begin1Stmt,
+ insertIntoA2Stmt,
+ insertIntoC2Stmt,
+ deleteFromC2Stmt,
+
+ commit2Stmt,
+ update1Stmt,
+ commit1Stmt,
+ select1Stmt,
+ ]) {
+ stmt.finalize();
+ }
+ await promiseClose(db1);
+ await promiseClose(db2);
+});
diff --git a/storage/test/unit/test_sqlite_secure_delete.js b/storage/test/unit/test_sqlite_secure_delete.js
new file mode 100644
index 0000000000..7beb1f1764
--- /dev/null
+++ b/storage/test/unit/test_sqlite_secure_delete.js
@@ -0,0 +1,78 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+/**
+ * This file tests to make sure that SQLite was compiled with
+ * SQLITE_SECURE_DELETE=1.
+ */
+
+// Helper Methods
+
+/**
+ * Reads the contents of a file and returns it as a string.
+ *
+ * @param aFile
+ * The file to return from.
+ * @return the contents of the file in the form of a string.
+ */
+function getFileContents(aFile) {
+ let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ fstream.init(aFile, -1, 0, 0);
+
+ let bstream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ bstream.setInputStream(fstream);
+ return bstream.readBytes(bstream.available());
+}
+
+// Tests
+
+add_test(function test_delete_removes_data() {
+ const TEST_STRING = "SomeRandomStringToFind";
+
+ let file = getTestDB();
+ let db = Services.storage.openDatabase(file);
+
+ // Create the table and insert the data.
+ db.createTable("test", "data TEXT");
+ let stmt = db.createStatement("INSERT INTO test VALUES(:data)");
+ stmt.params.data = TEST_STRING;
+ try {
+ stmt.execute();
+ } finally {
+ stmt.finalize();
+ }
+
+ // Make sure this test is actually testing what it thinks by making sure the
+ // string shows up in the database. Because the previous statement was
+ // automatically wrapped in a transaction, the contents are already on disk.
+ let contents = getFileContents(file);
+ Assert.notEqual(-1, contents.indexOf(TEST_STRING));
+
+ // Delete the data, and then close the database.
+ stmt = db.createStatement("DELETE FROM test WHERE data = :data");
+ stmt.params.data = TEST_STRING;
+ try {
+ stmt.execute();
+ } finally {
+ stmt.finalize();
+ }
+ db.close();
+
+ // Check the file to see if the string can be found.
+ contents = getFileContents(file);
+ Assert.equal(-1, contents.indexOf(TEST_STRING));
+
+ run_next_test();
+});
+
+function run_test() {
+ cleanup();
+ run_next_test();
+}
diff --git a/storage/test/unit/test_statement_executeAsync.js b/storage/test/unit/test_statement_executeAsync.js
new file mode 100644
index 0000000000..dab15121a5
--- /dev/null
+++ b/storage/test/unit/test_statement_executeAsync.js
@@ -0,0 +1,1045 @@
+/* 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/. */
+
+/*
+ * This file tests the functionality of mozIStorageBaseStatement::executeAsync
+ * for both mozIStorageStatement and mozIStorageAsyncStatement.
+ */
+
+// This file uses the internal _quit from testing/xpcshell/head.js */
+/* global _quit */
+
+const INTEGER = 1;
+const TEXT = "this is test text";
+const REAL = 3.23;
+const BLOB = [1, 2];
+
+/**
+ * Execute the given statement asynchronously, spinning an event loop until the
+ * async statement completes.
+ *
+ * @param aStmt
+ * The statement to execute.
+ * @param [aOptions={}]
+ * @param [aOptions.error=false]
+ * If true we should expect an error whose code we do not care about. If
+ * a numeric value, that's the error code we expect and require. If we
+ * are expecting an error, we expect a completion reason of REASON_ERROR.
+ * Otherwise we expect no error notification and a completion reason of
+ * REASON_FINISHED.
+ * @param [aOptions.cancel]
+ * If true we cancel the pending statement and additionally return the
+ * pending statement in case you want to further manipulate it.
+ * @param [aOptions.returnPending=false]
+ * If true we keep the pending statement around and return it to you. We
+ * normally avoid doing this to try and minimize the amount of time a
+ * reference is held to the returned pending statement.
+ * @param [aResults]
+ * If omitted, we assume no results rows are expected. If it is a
+ * number, we assume it is the number of results rows expected. If it is
+ * a function, we assume it is a function that takes the 1) result row
+ * number, 2) result tuple, 3) call stack for the original call to
+ * execAsync as arguments. If it is a list, we currently assume it is a
+ * list of functions where each function is intended to evaluate the
+ * result row at that ordinal position and takes the result tuple and
+ * the call stack for the original call.
+ */
+function execAsync(aStmt, aOptions, aResults) {
+ let caller = Components.stack.caller;
+ if (aOptions == null) {
+ aOptions = {};
+ }
+
+ let resultsExpected;
+ let resultsChecker;
+ if (aResults == null) {
+ resultsExpected = 0;
+ } else if (typeof aResults == "number") {
+ resultsExpected = aResults;
+ } else if (typeof aResults == "function") {
+ resultsChecker = aResults;
+ } else {
+ // array
+ resultsExpected = aResults.length;
+ resultsChecker = function (aResultNum, aTup, aCaller) {
+ aResults[aResultNum](aTup, aCaller);
+ };
+ }
+ let resultsSeen = 0;
+
+ let errorCodeExpected = false;
+ let reasonExpected = Ci.mozIStorageStatementCallback.REASON_FINISHED;
+ let altReasonExpected = null;
+ if ("error" in aOptions) {
+ errorCodeExpected = aOptions.error;
+ if (errorCodeExpected) {
+ reasonExpected = Ci.mozIStorageStatementCallback.REASON_ERROR;
+ }
+ }
+ let errorCodeSeen = false;
+
+ if ("cancel" in aOptions && aOptions.cancel) {
+ altReasonExpected = Ci.mozIStorageStatementCallback.REASON_CANCELED;
+ }
+
+ let completed = false;
+
+ let listener = {
+ handleResult(aResultSet) {
+ let row,
+ resultsSeenThisCall = 0;
+ while ((row = aResultSet.getNextRow()) != null) {
+ if (resultsChecker) {
+ resultsChecker(resultsSeen, row, caller);
+ }
+ resultsSeen++;
+ resultsSeenThisCall++;
+ }
+
+ if (!resultsSeenThisCall) {
+ do_throw("handleResult invoked with 0 result rows!");
+ }
+ },
+ handleError(aError) {
+ if (errorCodeSeen) {
+ do_throw("handleError called when we already had an error!");
+ }
+ errorCodeSeen = aError.result;
+ },
+ handleCompletion(aReason) {
+ if (completed) {
+ // paranoia check
+ do_throw("Received a second handleCompletion notification!", caller);
+ }
+
+ if (resultsSeen != resultsExpected) {
+ do_throw(
+ "Expected " +
+ resultsExpected +
+ " rows of results but " +
+ "got " +
+ resultsSeen +
+ " rows!",
+ caller
+ );
+ }
+
+ if (errorCodeExpected && !errorCodeSeen) {
+ do_throw("Expected an error, but did not see one.", caller);
+ } else if (errorCodeExpected != errorCodeSeen) {
+ do_throw(
+ "Expected error code " +
+ errorCodeExpected +
+ " but got " +
+ errorCodeSeen,
+ caller
+ );
+ }
+
+ if (aReason != reasonExpected && aReason != altReasonExpected) {
+ do_throw(
+ "Expected reason " +
+ reasonExpected +
+ (altReasonExpected ? " or " + altReasonExpected : "") +
+ " but got " +
+ aReason,
+ caller
+ );
+ }
+
+ completed = true;
+ },
+ };
+
+ let pending;
+ // Only get a pending reference if we're supposed to do.
+ // (note: This does not stop XPConnect from holding onto one currently.)
+ if (
+ ("cancel" in aOptions && aOptions.cancel) ||
+ ("returnPending" in aOptions && aOptions.returnPending)
+ ) {
+ pending = aStmt.executeAsync(listener);
+ } else {
+ aStmt.executeAsync(listener);
+ }
+
+ if ("cancel" in aOptions && aOptions.cancel) {
+ pending.cancel();
+ }
+
+ Services.tm.spinEventLoopUntil(
+ "Test(test_statement_executeAsync.js:execAsync)",
+ () => completed || _quit
+ );
+
+ return pending;
+}
+
+/**
+ * Make sure that illegal SQL generates the expected runtime error and does not
+ * result in any crashes. Async-only since the synchronous case generates the
+ * error synchronously (and is tested elsewhere).
+ */
+function test_illegal_sql_async_deferred() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);
+
+ // gibberish
+ let stmt = makeTestStatement("I AM A ROBOT. DO AS I SAY.");
+ execAsync(stmt, { error: Ci.mozIStorageError.ERROR });
+ stmt.finalize();
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ TEST_DB_NAME,
+ TELEMETRY_VALUES.failure,
+ 1
+ );
+ histogram.clear();
+
+ // legal SQL syntax, but with semantics issues.
+ stmt = makeTestStatement("SELECT destination FROM funkytown");
+ execAsync(stmt, { error: Ci.mozIStorageError.ERROR });
+ stmt.finalize();
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ TEST_DB_NAME,
+ TELEMETRY_VALUES.failure,
+ 1
+ );
+ histogram.clear();
+
+ run_next_test();
+}
+test_illegal_sql_async_deferred.asyncOnly = true;
+
+function test_create_table() {
+ // Ensure our table doesn't exist
+ Assert.ok(!getOpenedDatabase().tableExists("test"));
+
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);
+
+ var stmt = makeTestStatement(
+ "CREATE TABLE test (" +
+ "id INTEGER, " +
+ "string TEXT, " +
+ "number REAL, " +
+ "nuller NULL, " +
+ "blober BLOB" +
+ ")"
+ );
+ execAsync(stmt);
+ stmt.finalize();
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ TEST_DB_NAME,
+ TELEMETRY_VALUES.success,
+ 1
+ );
+ histogram.clear();
+
+ // Check that the table has been created
+ Assert.ok(getOpenedDatabase().tableExists("test"));
+
+ histogram.clear();
+
+ // Verify that it's created correctly (this will throw if it wasn't)
+ let checkStmt = getOpenedDatabase().createStatement(
+ "SELECT id, string, number, nuller, blober FROM test"
+ );
+ checkStmt.finalize();
+
+ // Nothing has executed so the histogram should be empty.
+ Assert.ok(!histogram.snapshot().values);
+
+ run_next_test();
+}
+
+function test_add_data() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);
+
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (?, ?, ?, ?, ?)"
+ );
+ stmt.bindBlobByIndex(4, BLOB, BLOB.length);
+ stmt.bindByIndex(3, null);
+ stmt.bindByIndex(2, REAL);
+ stmt.bindByIndex(1, TEXT);
+ stmt.bindByIndex(0, INTEGER);
+
+ execAsync(stmt);
+ stmt.finalize();
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ TEST_DB_NAME,
+ TELEMETRY_VALUES.success,
+ 1
+ );
+ histogram.clear();
+
+ // Check that the result is in the table
+ verifyQuery(
+ "SELECT string, number, nuller, blober FROM test WHERE id = ?",
+ INTEGER,
+ [TEXT, REAL, null, BLOB]
+ );
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ TEST_DB_NAME,
+ TELEMETRY_VALUES.success,
+ 1
+ );
+ histogram.clear();
+
+ run_next_test();
+}
+
+function test_get_data() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);
+
+ var stmt = makeTestStatement(
+ "SELECT string, number, nuller, blober, id FROM test WHERE id = ?"
+ );
+ stmt.bindByIndex(0, INTEGER);
+ execAsync(stmt, {}, [
+ function (tuple) {
+ Assert.notEqual(null, tuple);
+
+ // Check that it's what we expect
+ Assert.ok(!tuple.getIsNull(0));
+ Assert.equal(tuple.getResultByName("string"), tuple.getResultByIndex(0));
+ Assert.equal(TEXT, tuple.getResultByName("string"));
+ Assert.equal(
+ Ci.mozIStorageValueArray.VALUE_TYPE_TEXT,
+ tuple.getTypeOfIndex(0)
+ );
+
+ Assert.ok(!tuple.getIsNull(1));
+ Assert.equal(tuple.getResultByName("number"), tuple.getResultByIndex(1));
+ Assert.equal(REAL, tuple.getResultByName("number"));
+ Assert.equal(
+ Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT,
+ tuple.getTypeOfIndex(1)
+ );
+
+ Assert.ok(tuple.getIsNull(2));
+ Assert.equal(tuple.getResultByName("nuller"), tuple.getResultByIndex(2));
+ Assert.equal(null, tuple.getResultByName("nuller"));
+ Assert.equal(
+ Ci.mozIStorageValueArray.VALUE_TYPE_NULL,
+ tuple.getTypeOfIndex(2)
+ );
+
+ Assert.ok(!tuple.getIsNull(3));
+ var blobByName = tuple.getResultByName("blober");
+ Assert.equal(BLOB.length, blobByName.length);
+ var blobByIndex = tuple.getResultByIndex(3);
+ Assert.equal(BLOB.length, blobByIndex.length);
+ for (let i = 0; i < BLOB.length; i++) {
+ Assert.equal(BLOB[i], blobByName[i]);
+ Assert.equal(BLOB[i], blobByIndex[i]);
+ }
+ var count = { value: 0 };
+ var blob = { value: null };
+ tuple.getBlob(3, count, blob);
+ Assert.equal(BLOB.length, count.value);
+ for (let i = 0; i < BLOB.length; i++) {
+ Assert.equal(BLOB[i], blob.value[i]);
+ }
+ Assert.equal(
+ Ci.mozIStorageValueArray.VALUE_TYPE_BLOB,
+ tuple.getTypeOfIndex(3)
+ );
+
+ Assert.ok(!tuple.getIsNull(4));
+ Assert.equal(tuple.getResultByName("id"), tuple.getResultByIndex(4));
+ Assert.equal(INTEGER, tuple.getResultByName("id"));
+ Assert.equal(
+ Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER,
+ tuple.getTypeOfIndex(4)
+ );
+ },
+ ]);
+ stmt.finalize();
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ TEST_DB_NAME,
+ TELEMETRY_VALUES.success,
+ 1
+ );
+ histogram.clear();
+
+ run_next_test();
+}
+
+function test_tuple_out_of_bounds() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);
+
+ var stmt = makeTestStatement("SELECT string FROM test");
+ execAsync(stmt, {}, [
+ function (tuple) {
+ Assert.notEqual(null, tuple);
+
+ // Check all out of bounds - should throw
+ var methods = [
+ "getTypeOfIndex",
+ "getInt32",
+ "getInt64",
+ "getDouble",
+ "getUTF8String",
+ "getString",
+ "getIsNull",
+ ];
+ for (var i in methods) {
+ try {
+ tuple[methods[i]](tuple.numEntries);
+ do_throw("did not throw :(");
+ } catch (e) {
+ Assert.equal(Cr.NS_ERROR_ILLEGAL_VALUE, e.result);
+ }
+ }
+
+ // getBlob requires more args...
+ try {
+ var blob = { value: null };
+ var size = { value: 0 };
+ tuple.getBlob(tuple.numEntries, blob, size);
+ do_throw("did not throw :(");
+ } catch (e) {
+ Assert.equal(Cr.NS_ERROR_ILLEGAL_VALUE, e.result);
+ }
+ },
+ ]);
+ stmt.finalize();
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ TEST_DB_NAME,
+ TELEMETRY_VALUES.success,
+ 1
+ );
+ histogram.clear();
+
+ run_next_test();
+}
+
+function test_no_listener_works_on_success() {
+ var stmt = makeTestStatement("DELETE FROM test WHERE id = ?");
+ stmt.bindByIndex(0, 0);
+ stmt.executeAsync();
+ stmt.finalize();
+
+ // Run the next test.
+ run_next_test();
+}
+
+function test_no_listener_works_on_results() {
+ var stmt = makeTestStatement("SELECT ?");
+ stmt.bindByIndex(0, 1);
+ stmt.executeAsync();
+ stmt.finalize();
+
+ // Run the next test.
+ run_next_test();
+}
+
+function test_no_listener_works_on_error() {
+ // commit without a transaction will trigger an error
+ var stmt = makeTestStatement("COMMIT");
+ stmt.executeAsync();
+ stmt.finalize();
+
+ // Run the next test.
+ run_next_test();
+}
+
+function test_partial_listener_works() {
+ var stmt = makeTestStatement("DELETE FROM test WHERE id = ?");
+ stmt.bindByIndex(0, 0);
+ stmt.executeAsync({
+ handleResult(aResultSet) {},
+ });
+ stmt.executeAsync({
+ handleError(aError) {},
+ });
+ stmt.executeAsync({
+ handleCompletion(aReason) {},
+ });
+ stmt.finalize();
+
+ // Run the next test.
+ run_next_test();
+}
+
+/**
+ * Dubious cancellation test that depends on system loading may or may not
+ * succeed in canceling things. It does at least test if calling cancel blows
+ * up. test_AsyncCancellation in test_true_async.cpp is our test that canceling
+ * actually works correctly.
+ */
+function test_immediate_cancellation() {
+ var stmt = makeTestStatement("DELETE FROM test WHERE id = ?");
+ stmt.bindByIndex(0, 0);
+ execAsync(stmt, { cancel: true });
+ stmt.finalize();
+ run_next_test();
+}
+
+/**
+ * Test that calling cancel twice throws the second time.
+ */
+function test_double_cancellation() {
+ var stmt = makeTestStatement("DELETE FROM test WHERE id = ?");
+ stmt.bindByIndex(0, 0);
+ let pendingStatement = execAsync(stmt, { cancel: true });
+ // And cancel again - expect an exception
+ expectError(Cr.NS_ERROR_UNEXPECTED, () => pendingStatement.cancel());
+
+ stmt.finalize();
+ run_next_test();
+}
+
+/**
+ * Verify that nothing untoward happens if we try and cancel something after it
+ * has fully run to completion.
+ */
+function test_cancellation_after_execution() {
+ var stmt = makeTestStatement("DELETE FROM test WHERE id = ?");
+ stmt.bindByIndex(0, 0);
+ let pendingStatement = execAsync(stmt, { returnPending: true });
+ // (the statement has fully executed at this point)
+ // canceling after the statement has run to completion should not throw!
+ pendingStatement.cancel();
+
+ stmt.finalize();
+ run_next_test();
+}
+
+/**
+ * Verifies that a single statement can be executed more than once. Might once
+ * have been intended to also ensure that callback notifications were not
+ * incorrectly interleaved, but that part was brittle (it's totally fine for
+ * handleResult to get called multiple times) and not comprehensive.
+ */
+function test_double_execute() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);
+
+ var stmt = makeTestStatement("SELECT 1");
+ execAsync(stmt, null, 1);
+ execAsync(stmt, null, 1);
+ stmt.finalize();
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ TEST_DB_NAME,
+ TELEMETRY_VALUES.success,
+ 2
+ );
+ histogram.clear();
+
+ run_next_test();
+}
+
+function test_finalized_statement_does_not_crash() {
+ var stmt = makeTestStatement("SELECT * FROM TEST");
+ stmt.finalize();
+ // we are concerned about a crash here; an error is fine.
+ try {
+ stmt.executeAsync();
+ } catch (ex) {
+ // Do nothing.
+ }
+
+ // Run the next test.
+ run_next_test();
+}
+
+/**
+ * Bind by mozIStorageBindingParams on the mozIStorageBaseStatement by index.
+ */
+function test_bind_direct_binding_params_by_index() {
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (?, ?, ?, ?, ?)"
+ );
+ let insertId = nextUniqueId++;
+ stmt.bindByIndex(0, insertId);
+ stmt.bindByIndex(1, TEXT);
+ stmt.bindByIndex(2, REAL);
+ stmt.bindByIndex(3, null);
+ stmt.bindBlobByIndex(4, BLOB, BLOB.length);
+ execAsync(stmt);
+ stmt.finalize();
+ verifyQuery(
+ "SELECT string, number, nuller, blober FROM test WHERE id = ?",
+ insertId,
+ [TEXT, REAL, null, BLOB]
+ );
+ run_next_test();
+}
+
+/**
+ * Bind by mozIStorageBindingParams on the mozIStorageBaseStatement by name.
+ */
+function test_bind_direct_binding_params_by_name() {
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (:int, :text, :real, :null, :blob)"
+ );
+ let insertId = nextUniqueId++;
+ stmt.bindByName("int", insertId);
+ stmt.bindByName("text", TEXT);
+ stmt.bindByName("real", REAL);
+ stmt.bindByName("null", null);
+ stmt.bindBlobByName("blob", BLOB);
+ execAsync(stmt);
+ stmt.finalize();
+ verifyQuery(
+ "SELECT string, number, nuller, blober FROM test WHERE id = ?",
+ insertId,
+ [TEXT, REAL, null, BLOB]
+ );
+ run_next_test();
+}
+
+function test_bind_js_params_helper_by_index() {
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (?, ?, ?, ?, NULL)"
+ );
+ let insertId = nextUniqueId++;
+ // we cannot bind blobs this way; no blober
+ stmt.params[3] = null;
+ stmt.params[2] = REAL;
+ stmt.params[1] = TEXT;
+ stmt.params[0] = insertId;
+ execAsync(stmt);
+ stmt.finalize();
+ verifyQuery(
+ "SELECT string, number, nuller FROM test WHERE id = ?",
+ insertId,
+ [TEXT, REAL, null]
+ );
+ run_next_test();
+}
+
+function test_bind_js_params_helper_by_name() {
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (:int, :text, :real, :null, NULL)"
+ );
+ let insertId = nextUniqueId++;
+ // we cannot bind blobs this way; no blober
+ stmt.params.null = null;
+ stmt.params.real = REAL;
+ stmt.params.text = TEXT;
+ stmt.params.int = insertId;
+ execAsync(stmt);
+ stmt.finalize();
+ verifyQuery(
+ "SELECT string, number, nuller FROM test WHERE id = ?",
+ insertId,
+ [TEXT, REAL, null]
+ );
+ run_next_test();
+}
+
+function test_bind_multiple_rows_by_index() {
+ const AMOUNT_TO_ADD = 5;
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (?, ?, ?, ?, ?)"
+ );
+ var array = stmt.newBindingParamsArray();
+ for (let i = 0; i < AMOUNT_TO_ADD; i++) {
+ let bp = array.newBindingParams();
+ bp.bindByIndex(0, INTEGER);
+ bp.bindByIndex(1, TEXT);
+ bp.bindByIndex(2, REAL);
+ bp.bindByIndex(3, null);
+ bp.bindBlobByIndex(4, BLOB, BLOB.length);
+ array.addParams(bp);
+ Assert.equal(array.length, i + 1);
+ }
+ stmt.bindParameters(array);
+
+ let rowCount = getTableRowCount("test");
+ execAsync(stmt);
+ Assert.equal(rowCount + AMOUNT_TO_ADD, getTableRowCount("test"));
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_bind_multiple_rows_by_name() {
+ const AMOUNT_TO_ADD = 5;
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (:int, :text, :real, :null, :blob)"
+ );
+ var array = stmt.newBindingParamsArray();
+ for (let i = 0; i < AMOUNT_TO_ADD; i++) {
+ let bp = array.newBindingParams();
+ bp.bindByName("int", INTEGER);
+ bp.bindByName("text", TEXT);
+ bp.bindByName("real", REAL);
+ bp.bindByName("null", null);
+ bp.bindBlobByName("blob", BLOB);
+ array.addParams(bp);
+ Assert.equal(array.length, i + 1);
+ }
+ stmt.bindParameters(array);
+
+ let rowCount = getTableRowCount("test");
+ execAsync(stmt);
+ Assert.equal(rowCount + AMOUNT_TO_ADD, getTableRowCount("test"));
+ stmt.finalize();
+ run_next_test();
+}
+
+/**
+ * Verify that a mozIStorageStatement instance throws immediately when we
+ * try and bind to an illegal index.
+ */
+function test_bind_out_of_bounds_sync_immediate() {
+ let stmt = makeTestStatement("INSERT INTO test (id) VALUES (?)");
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+
+ // Check variant binding.
+ expectError(Cr.NS_ERROR_INVALID_ARG, () => bp.bindByIndex(1, INTEGER));
+ // Check blob binding.
+ expectError(Cr.NS_ERROR_INVALID_ARG, () =>
+ bp.bindBlobByIndex(1, BLOB, BLOB.length)
+ );
+
+ stmt.finalize();
+ run_next_test();
+}
+test_bind_out_of_bounds_sync_immediate.syncOnly = true;
+
+/**
+ * Verify that a mozIStorageAsyncStatement reports an error asynchronously when
+ * we bind to an illegal index.
+ */
+function test_bind_out_of_bounds_async_deferred() {
+ let stmt = makeTestStatement("INSERT INTO test (id) VALUES (?)");
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+
+ // There is no difference between variant and blob binding for async purposes.
+ bp.bindByIndex(1, INTEGER);
+ array.addParams(bp);
+ stmt.bindParameters(array);
+ execAsync(stmt, { error: Ci.mozIStorageError.RANGE });
+
+ stmt.finalize();
+ run_next_test();
+}
+test_bind_out_of_bounds_async_deferred.asyncOnly = true;
+
+function test_bind_no_such_name_sync_immediate() {
+ let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:foo)");
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+
+ // Check variant binding.
+ expectError(Cr.NS_ERROR_INVALID_ARG, () =>
+ bp.bindByName("doesnotexist", INTEGER)
+ );
+ // Check blob binding.
+ expectError(Cr.NS_ERROR_INVALID_ARG, () =>
+ bp.bindBlobByName("doesnotexist", BLOB)
+ );
+
+ stmt.finalize();
+ run_next_test();
+}
+test_bind_no_such_name_sync_immediate.syncOnly = true;
+
+function test_bind_no_such_name_async_deferred() {
+ let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:foo)");
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+
+ bp.bindByName("doesnotexist", INTEGER);
+ array.addParams(bp);
+ stmt.bindParameters(array);
+ execAsync(stmt, { error: Ci.mozIStorageError.RANGE });
+
+ stmt.finalize();
+ run_next_test();
+}
+test_bind_no_such_name_async_deferred.asyncOnly = true;
+
+function test_bind_bogus_type_by_index() {
+ // We try to bind a JS Object here that should fail to bind.
+ let stmt = makeTestStatement("INSERT INTO test (blober) VALUES (?)");
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+ Assert.throws(() => bp.bindByIndex(0, run_test), /NS_ERROR_UNEXPECTED/);
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_bind_bogus_type_by_name() {
+ // We try to bind a JS Object here that should fail to bind.
+ let stmt = makeTestStatement("INSERT INTO test (blober) VALUES (:blob)");
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+ Assert.throws(() => bp.bindByName("blob", run_test), /NS_ERROR_UNEXPECTED/);
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_bind_params_already_locked() {
+ let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)");
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+ bp.bindByName("int", INTEGER);
+ array.addParams(bp);
+
+ // We should get an error after we call addParams and try to bind again.
+ expectError(Cr.NS_ERROR_UNEXPECTED, () => bp.bindByName("int", INTEGER));
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_bind_params_array_already_locked() {
+ let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)");
+
+ let array = stmt.newBindingParamsArray();
+ let bp1 = array.newBindingParams();
+ bp1.bindByName("int", INTEGER);
+ array.addParams(bp1);
+ let bp2 = array.newBindingParams();
+ stmt.bindParameters(array);
+ bp2.bindByName("int", INTEGER);
+
+ // We should get an error after we have bound the array to the statement.
+ expectError(Cr.NS_ERROR_UNEXPECTED, () => array.addParams(bp2));
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_no_binding_params_from_locked_array() {
+ let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)");
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+ bp.bindByName("int", INTEGER);
+ array.addParams(bp);
+ stmt.bindParameters(array);
+
+ // We should not be able to get a new BindingParams object after we have bound
+ // to the statement.
+ expectError(Cr.NS_ERROR_UNEXPECTED, () => array.newBindingParams());
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_not_right_owning_array() {
+ let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)");
+
+ let array1 = stmt.newBindingParamsArray();
+ let array2 = stmt.newBindingParamsArray();
+ let bp = array1.newBindingParams();
+ bp.bindByName("int", INTEGER);
+
+ // We should not be able to add bp to array2 since it was created from array1.
+ expectError(Cr.NS_ERROR_UNEXPECTED, () => array2.addParams(bp));
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_not_right_owning_statement() {
+ let stmt1 = makeTestStatement("INSERT INTO test (id) VALUES (:int)");
+ let stmt2 = makeTestStatement("INSERT INTO test (id) VALUES (:int)");
+
+ let array1 = stmt1.newBindingParamsArray();
+ stmt2.newBindingParamsArray();
+ let bp = array1.newBindingParams();
+ bp.bindByName("int", INTEGER);
+ array1.addParams(bp);
+
+ // We should not be able to bind array1 since it was created from stmt1.
+ expectError(Cr.NS_ERROR_UNEXPECTED, () => stmt2.bindParameters(array1));
+
+ stmt1.finalize();
+ stmt2.finalize();
+ run_next_test();
+}
+
+function test_bind_empty_array() {
+ let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)");
+
+ let paramsArray = stmt.newBindingParamsArray();
+
+ // We should not be able to bind this array to the statement because it is
+ // empty.
+ expectError(Cr.NS_ERROR_UNEXPECTED, () => stmt.bindParameters(paramsArray));
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_multiple_results() {
+ let expectedResults = getTableRowCount("test");
+ // Sanity check - we should have more than one result, but let's be sure.
+ Assert.ok(expectedResults > 1);
+
+ // Now check that we get back two rows of data from our async query.
+ let stmt = makeTestStatement("SELECT * FROM test");
+ execAsync(stmt, {}, expectedResults);
+
+ stmt.finalize();
+ run_next_test();
+}
+
+// Test Runner
+
+const TEST_PASS_SYNC = 0;
+const TEST_PASS_ASYNC = 1;
+/**
+ * We run 2 passes against the test. One where makeTestStatement generates
+ * synchronous (mozIStorageStatement) statements and one where it generates
+ * asynchronous (mozIStorageAsyncStatement) statements.
+ *
+ * Because of differences in the ability to know the number of parameters before
+ * dispatching, some tests are sync/async specific. These functions are marked
+ * with 'syncOnly' or 'asyncOnly' attributes and run_next_test knows what to do.
+ */
+var testPass = TEST_PASS_SYNC;
+
+/**
+ * Create a statement of the type under test per testPass.
+ *
+ * @param aSQL
+ * The SQL string from which to build a statement.
+ * @return a statement of the type under test per testPass.
+ */
+function makeTestStatement(aSQL) {
+ if (testPass == TEST_PASS_SYNC) {
+ return getOpenedDatabase().createStatement(aSQL);
+ }
+ return getOpenedDatabase().createAsyncStatement(aSQL);
+}
+
+var tests = [
+ test_illegal_sql_async_deferred,
+ test_create_table,
+ test_add_data,
+ test_get_data,
+ test_tuple_out_of_bounds,
+ test_no_listener_works_on_success,
+ test_no_listener_works_on_results,
+ test_no_listener_works_on_error,
+ test_partial_listener_works,
+ test_immediate_cancellation,
+ test_double_cancellation,
+ test_cancellation_after_execution,
+ test_double_execute,
+ test_finalized_statement_does_not_crash,
+ test_bind_direct_binding_params_by_index,
+ test_bind_direct_binding_params_by_name,
+ test_bind_js_params_helper_by_index,
+ test_bind_js_params_helper_by_name,
+ test_bind_multiple_rows_by_index,
+ test_bind_multiple_rows_by_name,
+ test_bind_out_of_bounds_sync_immediate,
+ test_bind_out_of_bounds_async_deferred,
+ test_bind_no_such_name_sync_immediate,
+ test_bind_no_such_name_async_deferred,
+ test_bind_bogus_type_by_index,
+ test_bind_bogus_type_by_name,
+ test_bind_params_already_locked,
+ test_bind_params_array_already_locked,
+ test_bind_empty_array,
+ test_no_binding_params_from_locked_array,
+ test_not_right_owning_array,
+ test_not_right_owning_statement,
+ test_multiple_results,
+];
+var index = 0;
+
+const STARTING_UNIQUE_ID = 2;
+var nextUniqueId = STARTING_UNIQUE_ID;
+
+function run_next_test() {
+ function _run_next_test() {
+ // use a loop so we can skip tests...
+ while (index < tests.length) {
+ let test = tests[index++];
+ // skip tests not appropriate to the current test pass
+ if (
+ (testPass == TEST_PASS_SYNC && "asyncOnly" in test) ||
+ (testPass == TEST_PASS_ASYNC && "syncOnly" in test)
+ ) {
+ continue;
+ }
+
+ // Asynchronous tests means that exceptions don't kill the test.
+ try {
+ print("****** Running the next test: " + test.name);
+ test();
+ return;
+ } catch (e) {
+ do_throw(e);
+ }
+ }
+
+ // if we only completed the first pass, move to the next pass
+ if (testPass == TEST_PASS_SYNC) {
+ print("********* Beginning mozIStorageAsyncStatement pass.");
+ testPass++;
+ index = 0;
+ // a new pass demands a new database
+ asyncCleanup();
+ nextUniqueId = STARTING_UNIQUE_ID;
+ _run_next_test();
+ return;
+ }
+
+ // we did some async stuff; we need to clean up.
+ asyncCleanup();
+ do_test_finished();
+ }
+
+ // Don't actually schedule another test if we're quitting.
+ if (!_quit) {
+ // For saner stacks, we execute this code RSN.
+ executeSoon(_run_next_test);
+ }
+}
+
+function run_test() {
+ // Thunderbird doesn't have one or more of the probes used in this test.
+ // Ensure the data is collected anyway.
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+
+ cleanup();
+
+ do_test_pending();
+ run_next_test();
+}
diff --git a/storage/test/unit/test_statement_wrapper_automatically.js b/storage/test/unit/test_statement_wrapper_automatically.js
new file mode 100644
index 0000000000..8ff203f7c7
--- /dev/null
+++ b/storage/test/unit/test_statement_wrapper_automatically.js
@@ -0,0 +1,166 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ vim:set ts=2 sw=2 sts=2 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/. */
+
+// This file tests the functions of mozIStorageStatementWrapper
+
+function setup() {
+ getOpenedDatabase().createTable(
+ "test",
+ "id INTEGER PRIMARY KEY, val NONE,alt_val NONE"
+ );
+}
+
+/**
+ * A convenience wrapper for do_check_eq. Calls do_check_eq on aActualVal
+ * and aReturnedVal, with one caveat.
+ *
+ * Date objects are converted before parameter binding to PRTime's (microsecs
+ * since epoch). They are not reconverted when retrieved from the database.
+ * This function abstracts away this reconversion so that you can pass in,
+ * for example:
+ *
+ * checkVal(new Date(), aReturnedVal) // this
+ * checkVal(new Date().valueOf() * 1000.0, aReturnedVal) // instead of this
+ *
+ * Should any other types require conversion in the future, their conversions
+ * may also be abstracted away here.
+ *
+ * @param aActualVal
+ * the value inserted into the database
+ * @param aReturnedVal
+ * the value retrieved from the database
+ */
+function checkVal(aActualVal, aReturnedVal) {
+ if (aActualVal instanceof Date) {
+ aActualVal = aActualVal.valueOf() * 1000.0;
+ }
+ Assert.equal(aActualVal, aReturnedVal);
+}
+
+/**
+ * Removes all rows from our test table.
+ */
+function clearTable() {
+ var stmt = createStatement("DELETE FROM test");
+ stmt.execute();
+ stmt.finalize();
+ ensureNumRows(0);
+}
+
+/**
+ * Ensures that the number of rows in our test table is equal to aNumRows.
+ * Calls do_check_eq on aNumRows and the value retrieved by SELECT'ing COUNT(*).
+ *
+ * @param aNumRows
+ * the number of rows our test table should contain
+ */
+function ensureNumRows(aNumRows) {
+ var stmt = createStatement("SELECT COUNT(*) AS number FROM test");
+ Assert.ok(stmt.step());
+ Assert.equal(aNumRows, stmt.row.number);
+ stmt.reset();
+ stmt.finalize();
+}
+
+/**
+ * Inserts aVal into our test table and checks that insertion was successful by
+ * retrieving the newly inserted value from the database and comparing it
+ * against aVal. aVal is bound to a single parameter.
+ *
+ * @param aVal
+ * value to insert into our test table and check
+ */
+function insertAndCheckSingleParam(aVal) {
+ clearTable();
+
+ var stmt = createStatement("INSERT INTO test (val) VALUES (:val)");
+ stmt.params.val = aVal;
+ stmt.execute();
+ stmt.finalize();
+
+ ensureNumRows(1);
+
+ stmt = createStatement("SELECT val FROM test WHERE id = 1");
+ Assert.ok(stmt.step());
+ checkVal(aVal, stmt.row.val);
+ stmt.reset();
+ stmt.finalize();
+}
+
+/**
+ * Inserts aVal into our test table and checks that insertion was successful by
+ * retrieving the newly inserted value from the database and comparing it
+ * against aVal. aVal is bound to two separate parameters, both of which are
+ * checked against aVal.
+ *
+ * @param aVal
+ * value to insert into our test table and check
+ */
+function insertAndCheckMultipleParams(aVal) {
+ clearTable();
+
+ var stmt = createStatement(
+ "INSERT INTO test (val, alt_val) VALUES (:val, :val)"
+ );
+ stmt.params.val = aVal;
+ stmt.execute();
+ stmt.finalize();
+
+ ensureNumRows(1);
+
+ stmt = createStatement("SELECT val, alt_val FROM test WHERE id = 1");
+ Assert.ok(stmt.step());
+ checkVal(aVal, stmt.row.val);
+ checkVal(aVal, stmt.row.alt_val);
+ stmt.reset();
+ stmt.finalize();
+}
+
+/**
+ * A convenience function that prints out a description of aVal using
+ * aVal.toString and aVal.toSource. Output is useful when the test fails.
+ *
+ * @param aVal
+ * a value inserted or to be inserted into our test table
+ */
+function printValDesc(aVal) {
+ try {
+ var toSource = aVal.toSource();
+ } catch (ex) {
+ toSource = "";
+ }
+ print(
+ "Testing value: toString=" +
+ aVal +
+ (toSource ? " toSource=" + toSource : "")
+ );
+}
+
+function run_test() {
+ setup();
+
+ // function JSValStorageStatementBinder in
+ // storage/mozStorageStatementParams.cpp tells us that the following types
+ // and only the following types are valid as statement parameters:
+ var vals = [
+ 1337, // int
+ 3.1337, // double
+ "foo", // string
+ true, // boolean
+ null, // null
+ new Date(), // Date object
+ ];
+
+ vals.forEach(function (val) {
+ printValDesc(val);
+ print("Single parameter");
+ insertAndCheckSingleParam(val);
+ print("Multiple parameters");
+ insertAndCheckMultipleParams(val);
+ });
+
+ cleanup();
+}
diff --git a/storage/test/unit/test_storage_connection.js b/storage/test/unit/test_storage_connection.js
new file mode 100644
index 0000000000..e7175a9613
--- /dev/null
+++ b/storage/test/unit/test_storage_connection.js
@@ -0,0 +1,997 @@
+/* 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/. */
+
+// This file tests the functions of mozIStorageConnection
+
+function fetchAllNames(conn) {
+ let names = [];
+ let stmt = conn.createStatement(`SELECT name FROM test ORDER BY name`);
+ while (stmt.executeStep()) {
+ names.push(stmt.getUTF8String(0));
+ }
+ stmt.finalize();
+ return names;
+}
+
+// Test Functions
+
+add_task(async function test_connectionReady_open() {
+ // there doesn't seem to be a way for the connection to not be ready (unless
+ // we close it with mozIStorageConnection::Close(), but we don't for this).
+ // It can only fail if GetPath fails on the database file, or if we run out
+ // of memory trying to use an in-memory database
+
+ var msc = getOpenedDatabase();
+ Assert.ok(msc.connectionReady);
+});
+
+add_task(async function test_connectionReady_closed() {
+ // This also tests mozIStorageConnection::Close()
+
+ var msc = getOpenedDatabase();
+ msc.close();
+ Assert.ok(!msc.connectionReady);
+ gDBConn = null; // this is so later tests don't start to fail.
+});
+
+add_task(async function test_databaseFile() {
+ var msc = getOpenedDatabase();
+ Assert.ok(getTestDB().equals(msc.databaseFile));
+});
+
+add_task(async function test_tableExists_not_created() {
+ var msc = getOpenedDatabase();
+ Assert.ok(!msc.tableExists("foo"));
+});
+
+add_task(async function test_indexExists_not_created() {
+ var msc = getOpenedDatabase();
+ Assert.ok(!msc.indexExists("foo"));
+});
+
+add_task(async function test_temp_tableExists_and_indexExists() {
+ var msc = getOpenedDatabase();
+ msc.executeSimpleSQL(
+ "CREATE TEMP TABLE test_temp(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)"
+ );
+ Assert.ok(msc.tableExists("test_temp"));
+
+ msc.executeSimpleSQL("CREATE INDEX test_temp_ind ON test_temp (name)");
+ Assert.ok(msc.indexExists("test_temp_ind"));
+
+ msc.executeSimpleSQL("DROP INDEX test_temp_ind");
+ msc.executeSimpleSQL("DROP TABLE test_temp");
+});
+
+add_task(async function test_createTable_not_created() {
+ var msc = getOpenedDatabase();
+ msc.createTable("test", "id INTEGER PRIMARY KEY, name TEXT");
+ Assert.ok(msc.tableExists("test"));
+});
+
+add_task(async function test_indexExists_created() {
+ var msc = getOpenedDatabase();
+ msc.executeSimpleSQL("CREATE INDEX name_ind ON test (name)");
+ Assert.ok(msc.indexExists("name_ind"));
+});
+
+add_task(async function test_createTable_already_created() {
+ var msc = getOpenedDatabase();
+ Assert.ok(msc.tableExists("test"));
+ Assert.throws(
+ () => msc.createTable("test", "id INTEGER PRIMARY KEY, name TEXT"),
+ /NS_ERROR_FAILURE/
+ );
+});
+
+add_task(async function test_attach_createTable_tableExists_indexExists() {
+ var msc = getOpenedDatabase();
+ var file = do_get_file("storage_attach.sqlite", true);
+ var msc2 = getDatabase(file);
+ msc.executeSimpleSQL("ATTACH DATABASE '" + file.path + "' AS sample");
+
+ Assert.ok(!msc.tableExists("sample.test"));
+ msc.createTable("sample.test", "id INTEGER PRIMARY KEY, name TEXT");
+ Assert.ok(msc.tableExists("sample.test"));
+ Assert.throws(
+ () => msc.createTable("sample.test", "id INTEGER PRIMARY KEY, name TEXT"),
+ /NS_ERROR_FAILURE/
+ );
+
+ Assert.ok(!msc.indexExists("sample.test_ind"));
+ msc.executeSimpleSQL("CREATE INDEX sample.test_ind ON test (name)");
+ Assert.ok(msc.indexExists("sample.test_ind"));
+
+ msc.executeSimpleSQL("DETACH DATABASE sample");
+ msc2.close();
+ try {
+ file.remove(false);
+ } catch (e) {
+ // Do nothing.
+ }
+});
+
+add_task(async function test_lastInsertRowID() {
+ var msc = getOpenedDatabase();
+ msc.executeSimpleSQL("INSERT INTO test (name) VALUES ('foo')");
+ Assert.equal(1, msc.lastInsertRowID);
+});
+
+add_task(async function test_transactionInProgress_no() {
+ var msc = getOpenedDatabase();
+ Assert.ok(!msc.transactionInProgress);
+});
+
+add_task(async function test_transactionInProgress_yes() {
+ var msc = getOpenedDatabase();
+ msc.beginTransaction();
+ Assert.ok(msc.transactionInProgress);
+ msc.commitTransaction();
+ Assert.ok(!msc.transactionInProgress);
+
+ msc.beginTransaction();
+ Assert.ok(msc.transactionInProgress);
+ msc.rollbackTransaction();
+ Assert.ok(!msc.transactionInProgress);
+});
+
+add_task(async function test_commitTransaction_no_transaction() {
+ var msc = getOpenedDatabase();
+ Assert.ok(!msc.transactionInProgress);
+ Assert.throws(() => msc.commitTransaction(), /NS_ERROR_UNEXPECTED/);
+});
+
+add_task(async function test_rollbackTransaction_no_transaction() {
+ var msc = getOpenedDatabase();
+ Assert.ok(!msc.transactionInProgress);
+ Assert.throws(() => msc.rollbackTransaction(), /NS_ERROR_UNEXPECTED/);
+});
+
+add_task(async function test_get_schemaVersion_not_set() {
+ Assert.equal(0, getOpenedDatabase().schemaVersion);
+});
+
+add_task(async function test_set_schemaVersion() {
+ var msc = getOpenedDatabase();
+ const version = 1;
+ msc.schemaVersion = version;
+ Assert.equal(version, msc.schemaVersion);
+});
+
+add_task(async function test_set_schemaVersion_same() {
+ var msc = getOpenedDatabase();
+ const version = 1;
+ msc.schemaVersion = version; // should still work ok
+ Assert.equal(version, msc.schemaVersion);
+});
+
+add_task(async function test_set_schemaVersion_negative() {
+ var msc = getOpenedDatabase();
+ const version = -1;
+ msc.schemaVersion = version;
+ Assert.equal(version, msc.schemaVersion);
+});
+
+add_task(async function test_createTable() {
+ var temp = getTestDB().parent;
+ temp.append("test_db_table");
+ try {
+ var con = Services.storage.openDatabase(temp);
+ con.createTable("a", "");
+ } catch (e) {
+ if (temp.exists()) {
+ try {
+ temp.remove(false);
+ } catch (e2) {
+ // Do nothing.
+ }
+ }
+ Assert.ok(
+ e.result == Cr.NS_ERROR_NOT_INITIALIZED || e.result == Cr.NS_ERROR_FAILURE
+ );
+ } finally {
+ if (con) {
+ con.close();
+ }
+ }
+});
+
+add_task(async function test_defaultSynchronousAtNormal() {
+ getOpenedDatabase();
+ var stmt = createStatement("PRAGMA synchronous;");
+ try {
+ stmt.executeStep();
+ Assert.equal(1, stmt.getInt32(0));
+ } finally {
+ stmt.reset();
+ stmt.finalize();
+ }
+});
+
+// must be ran before executeAsync tests
+add_task(async function test_close_does_not_spin_event_loop() {
+ // We want to make sure that the event loop on the calling thread does not
+ // spin when close is called.
+ let event = {
+ ran: false,
+ run() {
+ this.ran = true;
+ },
+ };
+
+ // Post the event before we call close, so it would run if the event loop was
+ // spun during close.
+ Services.tm.dispatchToMainThread(event);
+
+ // Sanity check, then close the database. Afterwards, we should not have ran!
+ Assert.ok(!event.ran);
+ getOpenedDatabase().close();
+ Assert.ok(!event.ran);
+
+ // Reset gDBConn so that later tests will get a new connection object.
+ gDBConn = null;
+});
+
+add_task(
+ async function test_asyncClose_succeeds_with_finalized_async_statement() {
+ // XXX this test isn't perfect since we can't totally control when events will
+ // run. If this paticular function fails randomly, it means we have a
+ // real bug.
+
+ // We want to make sure we create a cached async statement to make sure that
+ // when we finalize our statement, we end up finalizing the async one too so
+ // close will succeed.
+ let stmt = createStatement("SELECT * FROM test");
+ stmt.executeAsync();
+ stmt.finalize();
+
+ await asyncClose(getOpenedDatabase());
+ // Reset gDBConn so that later tests will get a new connection object.
+ gDBConn = null;
+ }
+);
+
+// Would assert on debug builds.
+if (!AppConstants.DEBUG) {
+ add_task(async function test_close_then_release_statement() {
+ // Testing the behavior in presence of a bad client that finalizes
+ // statements after the database has been closed (typically by
+ // letting the gc finalize the statement).
+ let db = getOpenedDatabase();
+ let stmt = createStatement(
+ "SELECT * FROM test -- test_close_then_release_statement"
+ );
+ db.close();
+ stmt.finalize(); // Finalize too late - this should not crash
+
+ // Reset gDBConn so that later tests will get a new connection object.
+ gDBConn = null;
+ });
+
+ add_task(async function test_asyncClose_then_release_statement() {
+ // Testing the behavior in presence of a bad client that finalizes
+ // statements after the database has been async closed (typically by
+ // letting the gc finalize the statement).
+ let db = getOpenedDatabase();
+ let stmt = createStatement(
+ "SELECT * FROM test -- test_asyncClose_then_release_statement"
+ );
+ await asyncClose(db);
+ stmt.finalize(); // Finalize too late - this should not crash
+
+ // Reset gDBConn so that later tests will get a new connection object.
+ gDBConn = null;
+ });
+}
+
+// In debug builds this would cause a fatal assertion.
+if (!AppConstants.DEBUG) {
+ add_task(async function test_close_fails_with_async_statement_ran() {
+ let stmt = createStatement("SELECT * FROM test");
+ stmt.executeAsync();
+ stmt.finalize();
+
+ let db = getOpenedDatabase();
+ Assert.throws(() => db.close(), /NS_ERROR_UNEXPECTED/);
+ // Reset gDBConn so that later tests will get a new connection object.
+ gDBConn = null;
+ });
+}
+
+add_task(async function test_clone_optional_param() {
+ let db1 = Services.storage.openUnsharedDatabase(getTestDB());
+ let db2 = db1.clone();
+ Assert.ok(db2.connectionReady);
+
+ // A write statement should not fail here.
+ let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)");
+ stmt.params.name = "dwitte";
+ stmt.execute();
+ stmt.finalize();
+
+ // And a read statement should succeed.
+ stmt = db2.createStatement("SELECT * FROM test");
+ Assert.ok(stmt.executeStep());
+ stmt.finalize();
+
+ // Additionally check that it is a connection on the same database.
+ Assert.ok(db1.databaseFile.equals(db2.databaseFile));
+
+ db1.close();
+ db2.close();
+});
+
+async function standardAsyncTest(promisedDB, name, shouldInit = false) {
+ info("Performing standard async test " + name);
+
+ let adb = await promisedDB;
+ Assert.ok(adb instanceof Ci.mozIStorageAsyncConnection);
+ Assert.ok(adb instanceof Ci.mozIStorageConnection);
+
+ if (shouldInit) {
+ let stmt = adb.createAsyncStatement("CREATE TABLE test(name TEXT)");
+ await executeAsync(stmt);
+ stmt.finalize();
+ }
+
+ // Generate a name to insert and fetch back
+ name = "worker bee " + Math.random() + " (" + name + ")";
+
+ let stmt = adb.createAsyncStatement("INSERT INTO test (name) VALUES (:name)");
+ stmt.params.name = name;
+ let result = await executeAsync(stmt);
+ info("Request complete");
+ stmt.finalize();
+ Assert.ok(Components.isSuccessCode(result));
+ info("Extracting data");
+ stmt = adb.createAsyncStatement("SELECT * FROM test");
+ let found = false;
+ await executeAsync(stmt, function (results) {
+ info("Data has been extracted");
+ for (
+ let row = results.getNextRow();
+ row != null;
+ row = results.getNextRow()
+ ) {
+ if (row.getResultByName("name") == name) {
+ found = true;
+ break;
+ }
+ }
+ });
+ Assert.ok(found);
+ stmt.finalize();
+ await asyncClose(adb);
+
+ info("Standard async test " + name + " complete");
+}
+
+add_task(async function test_open_async() {
+ await standardAsyncTest(openAsyncDatabase(getTestDB(), null), "default");
+ await standardAsyncTest(openAsyncDatabase(getTestDB()), "no optional arg");
+ await standardAsyncTest(
+ openAsyncDatabase(getTestDB(), { shared: false, interruptible: true }),
+ "non-default options"
+ );
+ await standardAsyncTest(
+ openAsyncDatabase("memory"),
+ "in-memory database",
+ true
+ );
+ await standardAsyncTest(
+ openAsyncDatabase("memory", { shared: false }),
+ "in-memory database and options",
+ true
+ );
+
+ info("Testing async opening with readonly option");
+ const impliedReadOnlyOption = { ignoreLockingMode: true };
+
+ let raised = false;
+ let adb = await openAsyncDatabase(getTestDB(), impliedReadOnlyOption);
+ let stmt = adb.createAsyncStatement("CREATE TABLE test(name TEXT)");
+ try {
+ await executeAsync(stmt); // This should throw
+ } catch (e) {
+ raised = true;
+ } finally {
+ if (stmt) {
+ stmt.finalize();
+ }
+ if (adb) {
+ await asyncClose(adb);
+ }
+ }
+
+ Assert.ok(raised);
+});
+
+add_task(async function test_async_open_with_shared_cache() {
+ info("Testing that opening with a shared cache doesn't break stuff");
+ let adb = await openAsyncDatabase(getTestDB(), { shared: true });
+
+ let stmt = adb.createAsyncStatement("INSERT INTO test (name) VALUES (:name)");
+ stmt.params.name = "clockworker";
+ let result = await executeAsync(stmt);
+ info("Request complete");
+ stmt.finalize();
+ Assert.ok(Components.isSuccessCode(result));
+ info("Extracting data");
+ stmt = adb.createAsyncStatement("SELECT * FROM test");
+ let found = false;
+ await executeAsync(stmt, function (results) {
+ info("Data has been extracted");
+ for (
+ let row = results.getNextRow();
+ row != null;
+ row = results.getNextRow()
+ ) {
+ if (row.getResultByName("name") == "clockworker") {
+ found = true;
+ break;
+ }
+ }
+ });
+ Assert.ok(found);
+ stmt.finalize();
+ await asyncClose(adb);
+});
+
+add_task(async function test_clone_trivial_async() {
+ info("Open connection");
+ let db = Services.storage.openDatabase(getTestDB());
+ Assert.ok(db instanceof Ci.mozIStorageAsyncConnection);
+ info("AsyncClone connection");
+ let clone = await asyncClone(db, true);
+ Assert.ok(clone instanceof Ci.mozIStorageAsyncConnection);
+ Assert.ok(clone instanceof Ci.mozIStorageConnection);
+ info("Close connection");
+ await asyncClose(db);
+ info("Close clone");
+ await asyncClose(clone);
+});
+
+add_task(async function test_clone_no_optional_param_async() {
+ "use strict";
+ info("Testing async cloning");
+ let adb1 = await openAsyncDatabase(getTestDB(), null);
+ Assert.ok(adb1 instanceof Ci.mozIStorageAsyncConnection);
+ Assert.ok(adb1 instanceof Ci.mozIStorageConnection);
+
+ info("Cloning database");
+
+ let adb2 = await asyncClone(adb1);
+ info(
+ "Testing that the cloned db is a mozIStorageAsyncConnection " +
+ "and not a mozIStorageConnection"
+ );
+ Assert.ok(adb2 instanceof Ci.mozIStorageAsyncConnection);
+ Assert.ok(adb2 instanceof Ci.mozIStorageConnection);
+
+ info("Inserting data into source db");
+ let stmt = adb1.createAsyncStatement(
+ "INSERT INTO test (name) VALUES (:name)"
+ );
+
+ stmt.params.name = "yoric";
+ let result = await executeAsync(stmt);
+ info("Request complete");
+ stmt.finalize();
+ Assert.ok(Components.isSuccessCode(result));
+ info("Extracting data from clone db");
+ stmt = adb2.createAsyncStatement("SELECT * FROM test");
+ let found = false;
+ await executeAsync(stmt, function (results) {
+ info("Data has been extracted");
+ for (
+ let row = results.getNextRow();
+ row != null;
+ row = results.getNextRow()
+ ) {
+ if (row.getResultByName("name") == "yoric") {
+ found = true;
+ break;
+ }
+ }
+ });
+ Assert.ok(found);
+ stmt.finalize();
+ info("Closing databases");
+ await asyncClose(adb2);
+ info("First db closed");
+
+ await asyncClose(adb1);
+ info("Second db closed");
+});
+
+add_task(async function test_clone_readonly() {
+ let db1 = Services.storage.openUnsharedDatabase(getTestDB());
+ let db2 = db1.clone(true);
+ Assert.ok(db2.connectionReady);
+
+ // A write statement should fail here.
+ let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)");
+ stmt.params.name = "reed";
+ expectError(Cr.NS_ERROR_FILE_READ_ONLY, () => stmt.execute());
+ stmt.finalize();
+
+ // And a read statement should succeed.
+ stmt = db2.createStatement("SELECT * FROM test");
+ Assert.ok(stmt.executeStep());
+ stmt.finalize();
+
+ db1.close();
+ db2.close();
+});
+
+add_task(async function test_clone_shared_readonly() {
+ let db1 = Services.storage.openDatabase(getTestDB());
+ let db2 = db1.clone(true);
+ Assert.ok(db2.connectionReady);
+
+ let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)");
+ stmt.params.name = "parker";
+ // TODO currently SQLite does not actually work correctly here. The behavior
+ // we want is commented out, and the current behavior is being tested
+ // for. Our IDL comments will have to be updated when this starts to
+ // work again.
+ stmt.execute();
+ // expectError(Components.results.NS_ERROR_FILE_READ_ONLY, () => stmt.execute());
+ stmt.finalize();
+
+ // And a read statement should succeed.
+ stmt = db2.createStatement("SELECT * FROM test");
+ Assert.ok(stmt.executeStep());
+ stmt.finalize();
+
+ db1.close();
+ db2.close();
+});
+
+add_task(async function test_close_clone_fails() {
+ let calls = ["openDatabase", "openUnsharedDatabase"];
+ calls.forEach(function (methodName) {
+ let db = Services.storage[methodName](getTestDB());
+ db.close();
+ expectError(Cr.NS_ERROR_NOT_INITIALIZED, () => db.clone());
+ });
+});
+
+add_task(async function test_clone_copies_functions() {
+ const FUNC_NAME = "test_func";
+ let calls = ["openDatabase", "openUnsharedDatabase"];
+ let functionMethods = ["createFunction"];
+ calls.forEach(function (methodName) {
+ [true, false].forEach(function (readOnly) {
+ functionMethods.forEach(function (functionMethod) {
+ let db1 = Services.storage[methodName](getTestDB());
+ // Create a function for db1.
+ db1[functionMethod](FUNC_NAME, 1, {
+ onFunctionCall: () => 0,
+ onStep: () => 0,
+ onFinal: () => 0,
+ });
+
+ // Clone it, and make sure the function exists still.
+ let db2 = db1.clone(readOnly);
+ // Note: this would fail if the function did not exist.
+ let stmt = db2.createStatement(
+ "SELECT " + FUNC_NAME + "(id) FROM test"
+ );
+ stmt.finalize();
+ db1.close();
+ db2.close();
+ });
+ });
+ });
+});
+
+add_task(async function test_clone_copies_overridden_functions() {
+ const FUNC_NAME = "lower";
+ function test_func() {
+ this.called = false;
+ }
+ test_func.prototype = {
+ onFunctionCall() {
+ this.called = true;
+ },
+ onStep() {
+ this.called = true;
+ },
+ onFinal: () => 0,
+ };
+
+ let calls = ["openDatabase", "openUnsharedDatabase"];
+ let functionMethods = ["createFunction"];
+ calls.forEach(function (methodName) {
+ [true, false].forEach(function (readOnly) {
+ functionMethods.forEach(function (functionMethod) {
+ let db1 = Services.storage[methodName](getTestDB());
+ // Create a function for db1.
+ let func = new test_func();
+ db1[functionMethod](FUNC_NAME, 1, func);
+ Assert.ok(!func.called);
+
+ // Clone it, and make sure the function gets called.
+ let db2 = db1.clone(readOnly);
+ let stmt = db2.createStatement(
+ "SELECT " + FUNC_NAME + "(id) FROM test"
+ );
+ stmt.executeStep();
+ Assert.ok(func.called);
+ stmt.finalize();
+ db1.close();
+ db2.close();
+ });
+ });
+ });
+});
+
+add_task(async function test_clone_copies_pragmas() {
+ const PRAGMAS = [
+ { name: "cache_size", value: 500, copied: true },
+ { name: "temp_store", value: 2, copied: true },
+ { name: "foreign_keys", value: 1, copied: true },
+ { name: "journal_size_limit", value: 524288, copied: true },
+ { name: "synchronous", value: 2, copied: true },
+ { name: "wal_autocheckpoint", value: 16, copied: true },
+ { name: "busy_timeout", value: 50, copied: true },
+ { name: "ignore_check_constraints", value: 1, copied: false },
+ ];
+
+ let db1 = Services.storage.openUnsharedDatabase(getTestDB());
+
+ // Sanity check initial values are different from enforced ones.
+ PRAGMAS.forEach(function (pragma) {
+ let stmt = db1.createStatement("PRAGMA " + pragma.name);
+ Assert.ok(stmt.executeStep());
+ Assert.notEqual(pragma.value, stmt.getInt32(0));
+ stmt.finalize();
+ });
+ // Execute pragmas.
+ PRAGMAS.forEach(function (pragma) {
+ db1.executeSimpleSQL("PRAGMA " + pragma.name + " = " + pragma.value);
+ });
+
+ let db2 = db1.clone();
+ Assert.ok(db2.connectionReady);
+
+ // Check cloned connection inherited pragma values.
+ PRAGMAS.forEach(function (pragma) {
+ let stmt = db2.createStatement("PRAGMA " + pragma.name);
+ Assert.ok(stmt.executeStep());
+ let validate = pragma.copied ? "equal" : "notEqual";
+ Assert[validate](pragma.value, stmt.getInt32(0));
+ stmt.finalize();
+ });
+
+ db1.close();
+ db2.close();
+});
+
+add_task(async function test_readonly_clone_copies_pragmas() {
+ const PRAGMAS = [
+ { name: "cache_size", value: 500, copied: true },
+ { name: "temp_store", value: 2, copied: true },
+ { name: "foreign_keys", value: 1, copied: false },
+ { name: "journal_size_limit", value: 524288, copied: false },
+ { name: "synchronous", value: 2, copied: false },
+ { name: "wal_autocheckpoint", value: 16, copied: false },
+ { name: "busy_timeout", value: 50, copied: false },
+ { name: "ignore_check_constraints", value: 1, copied: false },
+ ];
+
+ let db1 = Services.storage.openUnsharedDatabase(getTestDB());
+
+ // Sanity check initial values are different from enforced ones.
+ PRAGMAS.forEach(function (pragma) {
+ let stmt = db1.createStatement("PRAGMA " + pragma.name);
+ Assert.ok(stmt.executeStep());
+ Assert.notEqual(pragma.value, stmt.getInt32(0));
+ stmt.finalize();
+ });
+ // Execute pragmas.
+ PRAGMAS.forEach(function (pragma) {
+ db1.executeSimpleSQL("PRAGMA " + pragma.name + " = " + pragma.value);
+ });
+
+ let db2 = db1.clone(true);
+ Assert.ok(db2.connectionReady);
+
+ // Check cloned connection inherited pragma values.
+ PRAGMAS.forEach(function (pragma) {
+ let stmt = db2.createStatement("PRAGMA " + pragma.name);
+ Assert.ok(stmt.executeStep());
+ let validate = pragma.copied ? "equal" : "notEqual";
+ Assert[validate](pragma.value, stmt.getInt32(0));
+ stmt.finalize();
+ });
+
+ db1.close();
+ db2.close();
+});
+
+add_task(async function test_clone_attach_database() {
+ let db1 = Services.storage.openUnsharedDatabase(getTestDB());
+
+ let c = 0;
+ function attachDB(conn, name) {
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append("test_storage_" + ++c + ".sqlite");
+ let db = Services.storage.openUnsharedDatabase(file);
+ conn.executeSimpleSQL(
+ `ATTACH DATABASE '${db.databaseFile.path}' AS ${name}`
+ );
+ db.executeSimpleSQL(`CREATE TABLE test_${name}(name TEXT);`);
+ db.close();
+ }
+ attachDB(db1, "attached_1");
+ attachDB(db1, "attached_2");
+ db1.executeSimpleSQL(`
+ CREATE TEMP TRIGGER test_temp_afterinsert_trigger
+ AFTER DELETE ON test_attached_1 FOR EACH ROW
+ BEGIN
+ INSERT INTO test(name) VALUES(OLD.name);
+ END`);
+
+ // These should not throw.
+ let stmt = db1.createStatement("SELECT * FROM attached_1.sqlite_master");
+ stmt.finalize();
+ stmt = db1.createStatement("SELECT * FROM attached_2.sqlite_master");
+ stmt.finalize();
+ db1.executeSimpleSQL("INSERT INTO test_attached_1(name) VALUES('asuth')");
+ db1.executeSimpleSQL("DELETE FROM test_attached_1");
+ Assert.ok(fetchAllNames(db1).includes("asuth"));
+
+ // R/W clone.
+ let db2 = db1.clone();
+ Assert.ok(db2.connectionReady);
+
+ // These should not throw.
+ stmt = db2.createStatement("SELECT * FROM attached_1.sqlite_master");
+ stmt.finalize();
+ stmt = db2.createStatement("SELECT * FROM attached_2.sqlite_master");
+ stmt.finalize();
+ db2.executeSimpleSQL("INSERT INTO test_attached_1(name) VALUES('past')");
+ db2.executeSimpleSQL("DELETE FROM test_attached_1");
+ let newNames = fetchAllNames(db2);
+ Assert.ok(newNames.includes("past"));
+ Assert.deepEqual(fetchAllNames(db1), newNames);
+
+ // R/O clone.
+ let db3 = db1.clone(true);
+ Assert.ok(db3.connectionReady);
+
+ // These should not throw.
+ stmt = db3.createStatement("SELECT * FROM attached_1.sqlite_master");
+ stmt.finalize();
+ stmt = db3.createStatement("SELECT * FROM attached_2.sqlite_master");
+ stmt.finalize();
+
+ db1.close();
+ db2.close();
+ db3.close();
+});
+
+add_task(async function test_async_clone_with_temp_trigger_and_table() {
+ info("Open connection");
+ let db = Services.storage.openDatabase(getTestDB());
+ Assert.ok(db instanceof Ci.mozIStorageAsyncConnection);
+
+ info("Set up tables on original connection");
+ let createQueries = [
+ `CREATE TEMP TABLE test_temp(name TEXT)`,
+ `CREATE INDEX test_temp_idx ON test_temp(name)`,
+ `CREATE TEMP TRIGGER test_temp_afterdelete_trigger
+ AFTER DELETE ON test_temp FOR EACH ROW
+ BEGIN
+ INSERT INTO test(name) VALUES(OLD.name);
+ END`,
+ ];
+ for (let query of createQueries) {
+ let stmt = db.createAsyncStatement(query);
+ await executeAsync(stmt);
+ stmt.finalize();
+ }
+
+ info("Create read-write clone with temp tables");
+ let readWriteClone = await asyncClone(db, false);
+ Assert.ok(readWriteClone instanceof Ci.mozIStorageAsyncConnection);
+
+ info("Insert into temp table on read-write clone");
+ let insertStmt = readWriteClone.createAsyncStatement(`
+ INSERT INTO test_temp(name) VALUES('mak'), ('standard8'), ('markh')`);
+ await executeAsync(insertStmt);
+ insertStmt.finalize();
+
+ info("Fire temp trigger on read-write clone");
+ let deleteStmt = readWriteClone.createAsyncStatement(`
+ DELETE FROM test_temp`);
+ await executeAsync(deleteStmt);
+ deleteStmt.finalize();
+
+ info("Read from original connection");
+ let names = fetchAllNames(db);
+ Assert.ok(names.includes("mak"));
+ Assert.ok(names.includes("standard8"));
+ Assert.ok(names.includes("markh"));
+
+ info("Create read-only clone");
+ let readOnlyClone = await asyncClone(db, true);
+ Assert.ok(readOnlyClone instanceof Ci.mozIStorageAsyncConnection);
+
+ info("Read-only clone shouldn't have temp entities");
+ let badStmt = readOnlyClone.createAsyncStatement(`SELECT 1 FROM test_temp`);
+ await Assert.rejects(executeAsync(badStmt), Ci.mozIStorageError);
+ badStmt.finalize();
+
+ info("Clean up");
+ for (let conn of [db, readWriteClone, readOnlyClone]) {
+ await asyncClose(conn);
+ }
+});
+
+add_task(async function test_sync_clone_in_transaction() {
+ info("Open connection");
+ let db = Services.storage.openDatabase(getTestDB());
+ Assert.ok(db instanceof Ci.mozIStorageAsyncConnection);
+
+ info("Begin transaction on main connection");
+ db.beginTransaction();
+
+ info("Create temp table and trigger in transaction");
+ let createQueries = [
+ `CREATE TEMP TABLE test_temp(name TEXT)`,
+ `CREATE TEMP TRIGGER test_temp_afterdelete_trigger
+ AFTER DELETE ON test_temp FOR EACH ROW
+ BEGIN
+ INSERT INTO test(name) VALUES(OLD.name);
+ END`,
+ ];
+ for (let query of createQueries) {
+ db.executeSimpleSQL(query);
+ }
+
+ info("Clone main connection while transaction is in progress");
+ let clone = db.clone(/* aReadOnly */ false);
+
+ // Dropping the table also drops `test_temp_afterdelete_trigger`.
+ info("Drop temp table on main connection");
+ db.executeSimpleSQL(`DROP TABLE test_temp`);
+
+ info("Commit transaction");
+ db.commitTransaction();
+
+ info("Clone connection should still have temp entities");
+ let readTempStmt = clone.createStatement(`SELECT 1 FROM test_temp`);
+ readTempStmt.execute();
+ readTempStmt.finalize();
+
+ info("Clean up");
+
+ db.close();
+ clone.close();
+});
+
+add_task(async function test_sync_clone_with_function() {
+ info("Open connection");
+ let db = Services.storage.openDatabase(getTestDB());
+ Assert.ok(db instanceof Ci.mozIStorageAsyncConnection);
+
+ info("Create SQL function");
+ function storeLastInsertedNameFunc() {
+ this.name = null;
+ }
+ storeLastInsertedNameFunc.prototype = {
+ onFunctionCall(args) {
+ this.name = args.getUTF8String(0);
+ },
+ };
+ let func = new storeLastInsertedNameFunc();
+ db.createFunction("store_last_inserted_name", 1, func);
+
+ info("Create temp trigger on main connection");
+ db.executeSimpleSQL(`
+ CREATE TEMP TRIGGER test_afterinsert_trigger
+ AFTER INSERT ON test FOR EACH ROW
+ BEGIN
+ SELECT store_last_inserted_name(NEW.name);
+ END`);
+
+ info("Clone main connection");
+ let clone = db.clone(/* aReadOnly */ false);
+
+ info("Write to clone");
+ clone.executeSimpleSQL(`INSERT INTO test(name) VALUES('kit')`);
+
+ Assert.equal(func.name, "kit");
+
+ info("Clean up");
+ db.close();
+ clone.close();
+});
+
+add_task(async function test_defaultTransactionType() {
+ info("Open connection");
+ let db = Services.storage.openDatabase(getTestDB());
+ Assert.ok(db instanceof Ci.mozIStorageAsyncConnection);
+
+ info("Verify default transaction type");
+ Assert.equal(
+ db.defaultTransactionType,
+ Ci.mozIStorageConnection.TRANSACTION_DEFERRED
+ );
+
+ info("Test other transaction types");
+ for (let type of [
+ Ci.mozIStorageConnection.TRANSACTION_IMMEDIATE,
+ Ci.mozIStorageConnection.TRANSACTION_EXCLUSIVE,
+ ]) {
+ db.defaultTransactionType = type;
+ Assert.equal(db.defaultTransactionType, type);
+ }
+
+ info("Should reject unknown transaction types");
+ Assert.throws(
+ () =>
+ (db.defaultTransactionType =
+ Ci.mozIStorageConnection.TRANSACTION_DEFAULT),
+ /NS_ERROR_ILLEGAL_VALUE/
+ );
+
+ db.defaultTransactionType = Ci.mozIStorageConnection.TRANSACTION_IMMEDIATE;
+
+ info("Clone should inherit default transaction type");
+ let clone = await asyncClone(db, true);
+ Assert.ok(clone instanceof Ci.mozIStorageAsyncConnection);
+ Assert.equal(
+ clone.defaultTransactionType,
+ Ci.mozIStorageConnection.TRANSACTION_IMMEDIATE
+ );
+
+ info("Begin immediate transaction on main connection");
+ db.beginTransaction();
+
+ info("Queue immediate transaction on clone");
+ let stmts = [
+ clone.createAsyncStatement(`BEGIN IMMEDIATE TRANSACTION`),
+ clone.createAsyncStatement(`DELETE FROM test WHERE name = 'new'`),
+ clone.createAsyncStatement(`COMMIT`),
+ ];
+ let promiseStmtsRan = stmts.map(stmt => executeAsync(stmt));
+
+ info("Commit immediate transaction on main connection");
+ db.executeSimpleSQL(`INSERT INTO test(name) VALUES('new')`);
+ db.commitTransaction();
+
+ info("Wait for transaction to succeed on clone");
+ await Promise.all(promiseStmtsRan);
+
+ info("Clean up");
+ for (let stmt of stmts) {
+ stmt.finalize();
+ }
+ await asyncClose(clone);
+ await asyncClose(db);
+});
+
+add_task(async function test_variableLimit() {
+ info("Open connection");
+ let db = Services.storage.openDatabase(getTestDB());
+ Assert.equal(db.variableLimit, 32766, "Should return default limit");
+ await asyncClose(db);
+});
+
+add_task(async function test_getInterface() {
+ let db = getOpenedDatabase();
+ let target = db
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEventTarget);
+ // Just check that target is non-null. Other tests will ensure that it has
+ // the correct value.
+ Assert.ok(target != null);
+
+ await asyncClose(db);
+ gDBConn = null;
+});
diff --git a/storage/test/unit/test_storage_ext.js b/storage/test/unit/test_storage_ext.js
new file mode 100644
index 0000000000..748ac86750
--- /dev/null
+++ b/storage/test/unit/test_storage_ext.js
@@ -0,0 +1,84 @@
+/* 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/. */
+
+// This file tests basics of loading SQLite extension.
+
+const VALID_EXTENSION_NAME = "fts5";
+
+add_setup(async function () {
+ cleanup();
+});
+
+add_task(async function test_valid_call() {
+ info("Testing valid call");
+ let conn = getOpenedUnsharedDatabase();
+
+ await new Promise((resolve, reject) => {
+ conn.loadExtension(VALID_EXTENSION_NAME, status => {
+ if (Components.isSuccessCode(status)) {
+ resolve();
+ } else {
+ reject(status);
+ }
+ });
+ });
+
+ cleanup();
+});
+
+add_task(async function test_invalid_calls() {
+ info("Testing invalid calls");
+ let conn = getOpenedUnsharedDatabase();
+
+ await Assert.rejects(
+ new Promise((resolve, reject) => {
+ conn.loadExtension("unknown", status => {
+ if (Components.isSuccessCode(status)) {
+ resolve();
+ } else {
+ reject(status);
+ }
+ });
+ }),
+ /NS_ERROR_ILLEGAL_VALUE/,
+ "Should fail loading unknown extension"
+ );
+
+ cleanup();
+
+ await Assert.rejects(
+ new Promise((resolve, reject) => {
+ conn.loadExtension(VALID_EXTENSION_NAME, status => {
+ if (Components.isSuccessCode(status)) {
+ resolve();
+ } else {
+ reject(status);
+ }
+ });
+ }),
+ /NS_ERROR_NOT_INITIALIZED/,
+ "Should fail loading extension on a closed connection"
+ );
+});
+
+add_task(async function test_more_invalid_calls() {
+ let conn = getOpenedUnsharedDatabase();
+ let promiseClosed = asyncClose(conn);
+
+ await Assert.rejects(
+ new Promise((resolve, reject) => {
+ conn.loadExtension(VALID_EXTENSION_NAME, status => {
+ if (Components.isSuccessCode(status)) {
+ resolve();
+ } else {
+ reject(status);
+ }
+ });
+ }),
+ /NS_ERROR_NOT_INITIALIZED/,
+ "Should fail loading extension on a closing connection"
+ );
+
+ await promiseClosed;
+});
diff --git a/storage/test/unit/test_storage_ext_fts3.js b/storage/test/unit/test_storage_ext_fts3.js
new file mode 100644
index 0000000000..bcc9491a93
--- /dev/null
+++ b/storage/test/unit/test_storage_ext_fts3.js
@@ -0,0 +1,91 @@
+/* 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/. */
+
+// This file tests support for the fts3 (full-text index) module.
+
+// Example statements in these tests are taken from the Full Text Index page
+// on the SQLite wiki: http://www.sqlite.org/cvstrac/wiki?p=FullTextIndex
+
+function test_table_creation() {
+ var msc = getOpenedUnsharedDatabase();
+
+ msc.executeSimpleSQL(
+ "CREATE VIRTUAL TABLE recipe USING fts3(name, ingredients)"
+ );
+
+ Assert.ok(msc.tableExists("recipe"));
+}
+
+function test_insertion() {
+ var msc = getOpenedUnsharedDatabase();
+
+ msc.executeSimpleSQL(
+ "INSERT INTO recipe (name, ingredients) VALUES " +
+ "('broccoli stew', 'broccoli peppers cheese tomatoes')"
+ );
+ msc.executeSimpleSQL(
+ "INSERT INTO recipe (name, ingredients) VALUES " +
+ "('pumpkin stew', 'pumpkin onions garlic celery')"
+ );
+ msc.executeSimpleSQL(
+ "INSERT INTO recipe (name, ingredients) VALUES " +
+ "('broccoli pie', 'broccoli cheese onions flour')"
+ );
+ msc.executeSimpleSQL(
+ "INSERT INTO recipe (name, ingredients) VALUES " +
+ "('pumpkin pie', 'pumpkin sugar flour butter')"
+ );
+
+ var stmt = msc.createStatement("SELECT COUNT(*) FROM recipe");
+ stmt.executeStep();
+
+ Assert.equal(stmt.getInt32(0), 4);
+
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_selection() {
+ var msc = getOpenedUnsharedDatabase();
+
+ var stmt = msc.createStatement(
+ "SELECT rowid, name, ingredients FROM recipe WHERE name MATCH 'pie'"
+ );
+
+ Assert.ok(stmt.executeStep());
+ Assert.equal(stmt.getInt32(0), 3);
+ Assert.equal(stmt.getString(1), "broccoli pie");
+ Assert.equal(stmt.getString(2), "broccoli cheese onions flour");
+
+ Assert.ok(stmt.executeStep());
+ Assert.equal(stmt.getInt32(0), 4);
+ Assert.equal(stmt.getString(1), "pumpkin pie");
+ Assert.equal(stmt.getString(2), "pumpkin sugar flour butter");
+
+ Assert.ok(!stmt.executeStep());
+
+ stmt.reset();
+ stmt.finalize();
+}
+
+var tests = [test_table_creation, test_insertion, test_selection];
+
+function run_test() {
+ // It's extra important to start from scratch, since these tests won't work
+ // with an existing shared cache connection, so we do it even though the last
+ // test probably did it already.
+ cleanup();
+
+ try {
+ for (var i = 0; i < tests.length; i++) {
+ tests[i]();
+ }
+ } finally {
+ // It's extra important to clean up afterwards, since later tests that use
+ // a shared cache connection will not be able to read the database we create,
+ // so we do this in a finally block to ensure it happens even if some of our
+ // tests fail.
+ cleanup();
+ }
+}
diff --git a/storage/test/unit/test_storage_ext_fts5.js b/storage/test/unit/test_storage_ext_fts5.js
new file mode 100644
index 0000000000..7adf4e8b83
--- /dev/null
+++ b/storage/test/unit/test_storage_ext_fts5.js
@@ -0,0 +1,122 @@
+/* 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/. */
+
+// This file tests support for the fts5 extension.
+
+// Some example statements in this tests are taken from the SQLite FTS5
+// documentation page: https://sqlite.org/fts5.html
+
+add_setup(async function () {
+ cleanup();
+});
+
+add_task(async function test_synchronous() {
+ info("Testing synchronous connection");
+ let conn = getOpenedUnsharedDatabase();
+ Assert.throws(
+ () =>
+ conn.executeSimpleSQL(
+ "CREATE VIRTUAL TABLE email USING fts5(sender, title, body);"
+ ),
+ /NS_ERROR_FAILURE/,
+ "Should not be able to use FTS5 without loading the extension"
+ );
+
+ await loadFTS5Extension(conn);
+
+ conn.executeSimpleSQL(
+ "CREATE VIRTUAL TABLE email USING fts5(sender, title, body, tokenize='trigram');"
+ );
+
+ conn.executeSimpleSQL(
+ `INSERT INTO email(sender, title, body) VALUES
+ ('Mark', 'Fox', 'The quick brown fox jumps over the lazy dog.'),
+ ('Marco', 'Cat', 'The quick brown cat jumps over the lazy dog.'),
+ ('James', 'Hamster', 'The quick brown hamster jumps over the lazy dog.')`
+ );
+
+ var stmt = conn.createStatement(
+ `SELECT sender, title, highlight(email, 2, '<', '>')
+ FROM email
+ WHERE email MATCH 'ham'
+ ORDER BY bm25(email)`
+ );
+ Assert.ok(stmt.executeStep());
+ Assert.equal(stmt.getString(0), "James");
+ Assert.equal(stmt.getString(1), "Hamster");
+ Assert.equal(
+ stmt.getString(2),
+ "The quick brown <ham>ster jumps over the lazy dog."
+ );
+ stmt.reset();
+ stmt.finalize();
+
+ cleanup();
+});
+
+add_task(async function test_asynchronous() {
+ info("Testing asynchronous connection");
+ let conn = await openAsyncDatabase(getTestDB());
+
+ await Assert.rejects(
+ executeSimpleSQLAsync(
+ conn,
+ "CREATE VIRTUAL TABLE email USING fts5(sender, title, body);"
+ ),
+ err => err.message.startsWith("no such module"),
+ "Should not be able to use FTS5 without loading the extension"
+ );
+
+ await loadFTS5Extension(conn);
+
+ await executeSimpleSQLAsync(
+ conn,
+ "CREATE VIRTUAL TABLE email USING fts5(sender, title, body);"
+ );
+
+ await asyncClose(conn);
+ await IOUtils.remove(getTestDB().path, { ignoreAbsent: true });
+});
+
+add_task(async function test_clone() {
+ info("Testing cloning synchronous connection loads extensions in clone");
+ let conn1 = getOpenedUnsharedDatabase();
+ await loadFTS5Extension(conn1);
+
+ let conn2 = conn1.clone(false);
+ conn2.executeSimpleSQL(
+ "CREATE VIRTUAL TABLE email USING fts5(sender, title, body);"
+ );
+
+ conn2.close();
+ cleanup();
+});
+
+add_task(async function test_asyncClone() {
+ info("Testing asynchronously cloning connection loads extensions in clone");
+ let conn1 = getOpenedUnsharedDatabase();
+ await loadFTS5Extension(conn1);
+
+ let conn2 = await asyncClone(conn1, false);
+ await executeSimpleSQLAsync(
+ conn2,
+ "CREATE VIRTUAL TABLE email USING fts5(sender, title, body);"
+ );
+
+ await asyncClose(conn2);
+ await asyncClose(conn1);
+ await IOUtils.remove(getTestDB().path, { ignoreAbsent: true });
+});
+
+async function loadFTS5Extension(conn) {
+ await new Promise((resolve, reject) => {
+ conn.loadExtension("fts5", status => {
+ if (Components.isSuccessCode(status)) {
+ resolve();
+ } else {
+ reject(status);
+ }
+ });
+ });
+}
diff --git a/storage/test/unit/test_storage_function.js b/storage/test/unit/test_storage_function.js
new file mode 100644
index 0000000000..1b11aa4a52
--- /dev/null
+++ b/storage/test/unit/test_storage_function.js
@@ -0,0 +1,92 @@
+/* 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/. */
+
+// This file tests the custom functions
+
+var testNums = [1, 2, 3, 4];
+
+function setup() {
+ getOpenedDatabase().createTable("function_tests", "id INTEGER PRIMARY KEY");
+
+ var stmt = createStatement("INSERT INTO function_tests (id) VALUES(?1)");
+ for (let i = 0; i < testNums.length; ++i) {
+ stmt.bindByIndex(0, testNums[i]);
+ stmt.execute();
+ }
+ stmt.reset();
+ stmt.finalize();
+}
+
+var testSquareFunction = {
+ calls: 0,
+
+ onFunctionCall(val) {
+ ++this.calls;
+ return val.getInt32(0) * val.getInt32(0);
+ },
+};
+
+function test_function_registration() {
+ var msc = getOpenedDatabase();
+ msc.createFunction("test_square", 1, testSquareFunction);
+}
+
+function test_function_no_double_registration() {
+ var msc = getOpenedDatabase();
+ try {
+ msc.createFunction("test_square", 2, testSquareFunction);
+ do_throw("We shouldn't get here!");
+ } catch (e) {
+ Assert.equal(Cr.NS_ERROR_FAILURE, e.result);
+ }
+}
+
+function test_function_removal() {
+ var msc = getOpenedDatabase();
+ msc.removeFunction("test_square");
+ // Should be Ok now
+ msc.createFunction("test_square", 1, testSquareFunction);
+}
+
+function test_function_aliases() {
+ var msc = getOpenedDatabase();
+ msc.createFunction("test_square2", 1, testSquareFunction);
+}
+
+function test_function_call() {
+ var stmt = createStatement("SELECT test_square(id) FROM function_tests");
+ while (stmt.executeStep()) {
+ // Do nothing.
+ }
+ Assert.equal(testNums.length, testSquareFunction.calls);
+ testSquareFunction.calls = 0;
+ stmt.finalize();
+}
+
+function test_function_result() {
+ var stmt = createStatement("SELECT test_square(42) FROM function_tests");
+ stmt.executeStep();
+ Assert.equal(42 * 42, stmt.getInt32(0));
+ testSquareFunction.calls = 0;
+ stmt.finalize();
+}
+
+var tests = [
+ test_function_registration,
+ test_function_no_double_registration,
+ test_function_removal,
+ test_function_aliases,
+ test_function_call,
+ test_function_result,
+];
+
+function run_test() {
+ setup();
+
+ for (var i = 0; i < tests.length; i++) {
+ tests[i]();
+ }
+
+ cleanup();
+}
diff --git a/storage/test/unit/test_storage_progresshandler.js b/storage/test/unit/test_storage_progresshandler.js
new file mode 100644
index 0000000000..e2e01eb2ff
--- /dev/null
+++ b/storage/test/unit/test_storage_progresshandler.js
@@ -0,0 +1,112 @@
+/* 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/. */
+
+// This file tests the custom progress handlers
+
+function setup() {
+ var msc = getOpenedDatabase();
+ msc.createTable("handler_tests", "id INTEGER PRIMARY KEY, num INTEGER");
+ msc.beginTransaction();
+
+ var stmt = createStatement(
+ "INSERT INTO handler_tests (id, num) VALUES(?1, ?2)"
+ );
+ for (let i = 0; i < 100; ++i) {
+ stmt.bindByIndex(0, i);
+ stmt.bindByIndex(1, Math.floor(Math.random() * 1000));
+ stmt.execute();
+ }
+ stmt.reset();
+ msc.commitTransaction();
+ stmt.finalize();
+}
+
+var testProgressHandler = {
+ calls: 0,
+ abort: false,
+
+ onProgress(comm) {
+ ++this.calls;
+ return this.abort;
+ },
+};
+
+function test_handler_registration() {
+ var msc = getOpenedDatabase();
+ msc.setProgressHandler(10, testProgressHandler);
+}
+
+function test_handler_return() {
+ var msc = getOpenedDatabase();
+ var oldH = msc.setProgressHandler(5, testProgressHandler);
+ Assert.ok(oldH instanceof Ci.mozIStorageProgressHandler);
+}
+
+function test_handler_removal() {
+ var msc = getOpenedDatabase();
+ msc.removeProgressHandler();
+ var oldH = msc.removeProgressHandler();
+ Assert.equal(oldH, null);
+}
+
+function test_handler_call() {
+ var msc = getOpenedDatabase();
+ msc.setProgressHandler(50, testProgressHandler);
+ // Some long-executing request
+ var stmt = createStatement(
+ "SELECT SUM(t1.num * t2.num) FROM handler_tests AS t1, handler_tests AS t2"
+ );
+ while (stmt.executeStep()) {
+ // Do nothing.
+ }
+ Assert.ok(testProgressHandler.calls > 0);
+ stmt.finalize();
+}
+
+function test_handler_abort() {
+ var msc = getOpenedDatabase();
+ testProgressHandler.abort = true;
+ msc.setProgressHandler(50, testProgressHandler);
+ // Some long-executing request
+ var stmt = createStatement(
+ "SELECT SUM(t1.num * t2.num) FROM handler_tests AS t1, handler_tests AS t2"
+ );
+
+ const SQLITE_INTERRUPT = 9;
+ try {
+ while (stmt.executeStep()) {
+ // Do nothing.
+ }
+ do_throw("We shouldn't get here!");
+ } catch (e) {
+ Assert.equal(Cr.NS_ERROR_ABORT, e.result);
+ Assert.equal(SQLITE_INTERRUPT, msc.lastError);
+ }
+ try {
+ stmt.finalize();
+ do_throw("We shouldn't get here!");
+ } catch (e) {
+ // finalize should return the error code since we encountered an error
+ Assert.equal(Cr.NS_ERROR_ABORT, e.result);
+ Assert.equal(SQLITE_INTERRUPT, msc.lastError);
+ }
+}
+
+var tests = [
+ test_handler_registration,
+ test_handler_return,
+ test_handler_removal,
+ test_handler_call,
+ test_handler_abort,
+];
+
+function run_test() {
+ setup();
+
+ for (var i = 0; i < tests.length; i++) {
+ tests[i]();
+ }
+
+ cleanup();
+}
diff --git a/storage/test/unit/test_storage_service.js b/storage/test/unit/test_storage_service.js
new file mode 100644
index 0000000000..62a933cfbf
--- /dev/null
+++ b/storage/test/unit/test_storage_service.js
@@ -0,0 +1,267 @@
+/* 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/. */
+
+// This file tests the functions of mozIStorageService except for
+// openSpecialDatabase, which is tested by test_storage_service_special.js and
+// openUnsharedDatabase, which is tested by test_storage_service_unshared.js.
+
+function test_openDatabase_null_file() {
+ try {
+ Services.storage.openDatabase(null);
+ do_throw("We should not get here!");
+ } catch (e) {
+ print(e);
+ print("e.result is " + e.result);
+ Assert.equal(Cr.NS_ERROR_INVALID_ARG, e.result);
+ }
+}
+
+function test_openDatabase_file_DNE() {
+ // the file should be created after calling
+
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(OPEN_HISTOGRAM);
+
+ var db = getTestDB();
+ Assert.ok(!db.exists());
+ Services.storage.openDatabase(db);
+ Assert.ok(db.exists());
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ db.leafName,
+ TELEMETRY_VALUES.success,
+ 1
+ );
+}
+
+function test_openDatabase_file_exists() {
+ // it should already exist from our last test
+
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(OPEN_HISTOGRAM);
+
+ var db = getTestDB();
+ Assert.ok(db.exists());
+ Services.storage.openDatabase(db);
+ Assert.ok(db.exists());
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ db.leafName,
+ TELEMETRY_VALUES.success,
+ 1
+ );
+}
+
+function test_corrupt_db_throws_with_openDatabase() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(OPEN_HISTOGRAM);
+
+ let db = getCorruptDB();
+
+ try {
+ getDatabase(db);
+ do_throw("should not be here");
+ } catch (e) {
+ Assert.equal(Cr.NS_ERROR_FILE_CORRUPTED, e.result);
+ }
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ db.leafName,
+ TELEMETRY_VALUES.corrupt,
+ 1
+ );
+}
+
+function test_fake_db_throws_with_openDatabase() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(OPEN_HISTOGRAM);
+
+ let db = getFakeDB();
+
+ try {
+ getDatabase(db);
+ do_throw("should not be here");
+ } catch (e) {
+ Assert.equal(Cr.NS_ERROR_FILE_CORRUPTED, e.result);
+ }
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ db.leafName,
+ TELEMETRY_VALUES.corrupt,
+ 1
+ );
+}
+
+function test_openDatabase_directory() {
+ let dir = getTestDB().parent;
+ dir.append("test_storage_temp");
+ if (dir.exists()) {
+ dir.remove(true);
+ }
+ dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ Assert.ok(dir.exists());
+
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(OPEN_HISTOGRAM);
+
+ try {
+ getDatabase(dir);
+ do_throw("should not be here");
+ } catch (e) {
+ Assert.equal(Cr.NS_ERROR_FILE_ACCESS_DENIED, e.result);
+ }
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ dir.leafName,
+ TELEMETRY_VALUES.access,
+ 1
+ );
+
+ dir.remove(true);
+}
+
+function test_read_gooddb() {
+ let file = do_get_file("goodDB.sqlite");
+ let db = getDatabase(file);
+
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);
+
+ db.executeSimpleSQL("SELECT * FROM Foo;");
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ file.leafName,
+ TELEMETRY_VALUES.success,
+ 1
+ );
+
+ histogram.clear();
+
+ let stmt = db.createStatement("SELECT id from Foo");
+
+ while (true) {
+ if (!stmt.executeStep()) {
+ break;
+ }
+ }
+
+ stmt.finalize();
+
+ // A single statement should count as a single access.
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ file.leafName,
+ TELEMETRY_VALUES.success,
+ 1
+ );
+
+ histogram.clear();
+
+ Assert.throws(
+ () => db.executeSimpleSQL("INSERT INTO Foo (rowid) VALUES ('test');"),
+ /NS_ERROR_FAILURE/,
+ "Executing sql should fail."
+ );
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ file.leafName,
+ TELEMETRY_VALUES.misuse,
+ 1
+ );
+}
+
+function test_read_baddb() {
+ let file = do_get_file("baddataDB.sqlite");
+ let db = getDatabase(file);
+
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);
+
+ Assert.throws(
+ () => db.executeSimpleSQL("SELECT * FROM Foo"),
+ /NS_ERROR_FILE_CORRUPTED/,
+ "Executing sql should fail."
+ );
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ file.leafName,
+ TELEMETRY_VALUES.corrupt,
+ 1
+ );
+
+ histogram.clear();
+
+ let stmt = db.createStatement("SELECT * FROM Foo");
+ Assert.throws(
+ () => stmt.executeStep(),
+ /NS_ERROR_FILE_CORRUPTED/,
+ "Executing a statement should fail."
+ );
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ file.leafName,
+ TELEMETRY_VALUES.corrupt,
+ 1
+ );
+}
+
+function test_busy_telemetry() {
+ // Thunderbird doesn't have one or more of the probes used in this test.
+ // Ensure the data is collected anyway.
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+
+ let file = do_get_file("goodDB.sqlite");
+ let conn1 = Services.storage.openUnsharedDatabase(file);
+ let conn2 = Services.storage.openUnsharedDatabase(file);
+
+ conn1.beginTransaction();
+ conn1.executeSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)");
+
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);
+ Assert.throws(
+ () =>
+ conn2.executeSimpleSQL("CREATE TABLE test_busy (id INTEGER PRIMARY KEY)"),
+ /NS_ERROR_STORAGE_BUSY/,
+ "Nested transaction on second connection should fail"
+ );
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ file.leafName,
+ TELEMETRY_VALUES.busy,
+ 1
+ );
+
+ conn1.rollbackTransaction();
+}
+
+var tests = [
+ test_openDatabase_null_file,
+ test_openDatabase_file_DNE,
+ test_openDatabase_file_exists,
+ test_corrupt_db_throws_with_openDatabase,
+ test_fake_db_throws_with_openDatabase,
+ test_openDatabase_directory,
+ test_read_gooddb,
+ test_read_baddb,
+ test_busy_telemetry,
+];
+
+function run_test() {
+ // Thunderbird doesn't have one or more of the probes used in this test.
+ // Ensure the data is collected anyway.
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+
+ for (var i = 0; i < tests.length; i++) {
+ tests[i]();
+ }
+
+ cleanup();
+}
diff --git a/storage/test/unit/test_storage_service_special.js b/storage/test/unit/test_storage_service_special.js
new file mode 100644
index 0000000000..50cde042af
--- /dev/null
+++ b/storage/test/unit/test_storage_service_special.js
@@ -0,0 +1,48 @@
+/* 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/. */
+
+// This file tests the openSpecialDatabase function of mozIStorageService.
+
+add_task(async function test_invalid_storage_key() {
+ try {
+ Services.storage.openSpecialDatabase("abcd");
+ do_throw("We should not get here!");
+ } catch (e) {
+ print(e);
+ print("e.result is " + e.result);
+ Assert.equal(Cr.NS_ERROR_INVALID_ARG, e.result);
+ }
+});
+
+add_task(async function test_memdb_close_clone_fails() {
+ for (const name of [null, "foo"]) {
+ const db = Services.storage.openSpecialDatabase("memory", name);
+ db.close();
+ expectError(Cr.NS_ERROR_NOT_INITIALIZED, () => db.clone());
+ }
+});
+
+add_task(async function test_memdb_no_file_on_disk() {
+ for (const name of [null, "foo"]) {
+ const db = Services.storage.openSpecialDatabase("memory", name);
+ db.close();
+ for (const dirKey of ["CurWorkD", "ProfD"]) {
+ const dir = Services.dirsvc.get(dirKey, Ci.nsIFile);
+ const file = dir.clone();
+ file.append(!name ? ":memory:" : "file:foo?mode=memory&cache=shared");
+ Assert.ok(!file.exists());
+ }
+ }
+});
+
+add_task(async function test_memdb_sharing() {
+ for (const name of [null, "foo"]) {
+ const db = Services.storage.openSpecialDatabase("memory", name);
+ db.executeSimpleSQL("CREATE TABLE test(name TEXT)");
+ const db2 = Services.storage.openSpecialDatabase("memory", name);
+ Assert.ok(!!name == db2.tableExists("test"));
+ db.close();
+ db2.close();
+ }
+});
diff --git a/storage/test/unit/test_storage_service_unshared.js b/storage/test/unit/test_storage_service_unshared.js
new file mode 100644
index 0000000000..da1865dfa6
--- /dev/null
+++ b/storage/test/unit/test_storage_service_unshared.js
@@ -0,0 +1,46 @@
+/* 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/. */
+
+// This file tests the openUnsharedDatabase function of mozIStorageService.
+
+function test_openUnsharedDatabase_null_file() {
+ try {
+ Services.storage.openUnsharedDatabase(null);
+ do_throw("We should not get here!");
+ } catch (e) {
+ print(e);
+ print("e.result is " + e.result);
+ Assert.equal(Cr.NS_ERROR_INVALID_ARG, e.result);
+ }
+}
+
+function test_openUnsharedDatabase_file_DNE() {
+ // the file should be created after calling
+ var db = getTestDB();
+ Assert.ok(!db.exists());
+ Services.storage.openUnsharedDatabase(db);
+ Assert.ok(db.exists());
+}
+
+function test_openUnsharedDatabase_file_exists() {
+ // it should already exist from our last test
+ var db = getTestDB();
+ Assert.ok(db.exists());
+ Services.storage.openUnsharedDatabase(db);
+ Assert.ok(db.exists());
+}
+
+var tests = [
+ test_openUnsharedDatabase_null_file,
+ test_openUnsharedDatabase_file_DNE,
+ test_openUnsharedDatabase_file_exists,
+];
+
+function run_test() {
+ for (var i = 0; i < tests.length; i++) {
+ tests[i]();
+ }
+
+ cleanup();
+}
diff --git a/storage/test/unit/test_storage_statement.js b/storage/test/unit/test_storage_statement.js
new file mode 100644
index 0000000000..3daf3e7812
--- /dev/null
+++ b/storage/test/unit/test_storage_statement.js
@@ -0,0 +1,183 @@
+/* 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/. */
+
+// This file tests the functions of mozIStorageStatement
+
+function setup() {
+ getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, name TEXT");
+}
+
+function test_parameterCount_none() {
+ var stmt = createStatement("SELECT * FROM test");
+ Assert.equal(0, stmt.parameterCount);
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_parameterCount_one() {
+ var stmt = createStatement("SELECT * FROM test WHERE id = ?1");
+ Assert.equal(1, stmt.parameterCount);
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_getParameterName() {
+ var stmt = createStatement("SELECT * FROM test WHERE id = :id");
+ Assert.equal(":id", stmt.getParameterName(0));
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_getParameterIndex_different() {
+ var stmt = createStatement(
+ "SELECT * FROM test WHERE id = :id OR name = :name"
+ );
+ Assert.equal(0, stmt.getParameterIndex("id"));
+ Assert.equal(1, stmt.getParameterIndex("name"));
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_getParameterIndex_same() {
+ var stmt = createStatement(
+ "SELECT * FROM test WHERE id = :test OR name = :test"
+ );
+ Assert.equal(0, stmt.getParameterIndex("test"));
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_columnCount() {
+ var stmt = createStatement("SELECT * FROM test WHERE id = ?1 OR name = ?2");
+ Assert.equal(2, stmt.columnCount);
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_getColumnName() {
+ var stmt = createStatement("SELECT name, id FROM test");
+ Assert.equal("id", stmt.getColumnName(1));
+ Assert.equal("name", stmt.getColumnName(0));
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_getColumnIndex_same_case() {
+ var stmt = createStatement("SELECT name, id FROM test");
+ Assert.equal(0, stmt.getColumnIndex("name"));
+ Assert.equal(1, stmt.getColumnIndex("id"));
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_getColumnIndex_different_case() {
+ var stmt = createStatement("SELECT name, id FROM test");
+ try {
+ Assert.equal(0, stmt.getColumnIndex("NaMe"));
+ do_throw("should not get here");
+ } catch (e) {
+ Assert.equal(Cr.NS_ERROR_INVALID_ARG, e.result);
+ }
+ try {
+ Assert.equal(1, stmt.getColumnIndex("Id"));
+ do_throw("should not get here");
+ } catch (e) {
+ Assert.equal(Cr.NS_ERROR_INVALID_ARG, e.result);
+ }
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_state_ready() {
+ var stmt = createStatement("SELECT name, id FROM test");
+ Assert.equal(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_READY, stmt.state);
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_state_executing() {
+ var stmt = createStatement("INSERT INTO test (name) VALUES ('foo')");
+ stmt.execute();
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("SELECT name, id FROM test");
+ stmt.executeStep();
+ Assert.equal(
+ Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_EXECUTING,
+ stmt.state
+ );
+ stmt.executeStep();
+ Assert.equal(
+ Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_EXECUTING,
+ stmt.state
+ );
+ stmt.reset();
+ Assert.equal(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_READY, stmt.state);
+ stmt.finalize();
+}
+
+function test_state_after_finalize() {
+ var stmt = createStatement("SELECT name, id FROM test");
+ stmt.executeStep();
+ stmt.finalize();
+ Assert.equal(
+ Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_INVALID,
+ stmt.state
+ );
+}
+
+function test_failed_execute() {
+ var stmt = createStatement("INSERT INTO test (name) VALUES ('foo')");
+ stmt.execute();
+ stmt.finalize();
+ var id = getOpenedDatabase().lastInsertRowID;
+ stmt = createStatement("INSERT INTO test(id, name) VALUES(:id, 'bar')");
+ stmt.params.id = id;
+ try {
+ // Should throw a constraint error
+ stmt.execute();
+ do_throw("Should have seen a constraint error");
+ } catch (e) {
+ Assert.equal(getOpenedDatabase().lastError, Ci.mozIStorageError.CONSTRAINT);
+ }
+ Assert.equal(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_READY, stmt.state);
+ // Should succeed without needing to reset the statement manually
+ stmt.finalize();
+}
+
+function test_bind_undefined() {
+ var stmt = createStatement("INSERT INTO test (name) VALUES ('foo')");
+
+ expectError(Cr.NS_ERROR_ILLEGAL_VALUE, () => stmt.bindParameters(undefined));
+
+ stmt.finalize();
+}
+
+var tests = [
+ test_parameterCount_none,
+ test_parameterCount_one,
+ test_getParameterName,
+ test_getParameterIndex_different,
+ test_getParameterIndex_same,
+ test_columnCount,
+ test_getColumnName,
+ test_getColumnIndex_same_case,
+ test_getColumnIndex_different_case,
+ test_state_ready,
+ test_state_executing,
+ test_state_after_finalize,
+ test_failed_execute,
+ test_bind_undefined,
+];
+
+function run_test() {
+ setup();
+
+ for (var i = 0; i < tests.length; i++) {
+ tests[i]();
+ }
+
+ cleanup();
+}
diff --git a/storage/test/unit/test_storage_value_array.js b/storage/test/unit/test_storage_value_array.js
new file mode 100644
index 0000000000..6a8f1fb6a2
--- /dev/null
+++ b/storage/test/unit/test_storage_value_array.js
@@ -0,0 +1,194 @@
+/* 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/. */
+
+// This file tests the functions of mozIStorageValueArray
+
+add_task(async function setup() {
+ getOpenedDatabase().createTable(
+ "test",
+ "id INTEGER PRIMARY KEY, name TEXT," +
+ "number REAL, nuller NULL, blobber BLOB"
+ );
+
+ var stmt = createStatement(
+ "INSERT INTO test (name, number, blobber) VALUES (?1, ?2, ?3)"
+ );
+ stmt.bindByIndex(0, "foo");
+ stmt.bindByIndex(1, 2.34);
+ stmt.bindBlobByIndex(2, [], 0);
+ stmt.execute();
+
+ stmt.bindByIndex(0, "");
+ stmt.bindByIndex(1, 1.23);
+ stmt.bindBlobByIndex(2, [1, 2], 2);
+ stmt.execute();
+
+ stmt.reset();
+ stmt.finalize();
+
+ registerCleanupFunction(cleanup);
+});
+
+add_task(async function test_getIsNull_for_null() {
+ var stmt = createStatement("SELECT nuller, blobber FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 1);
+ Assert.ok(stmt.executeStep());
+
+ Assert.ok(stmt.getIsNull(0)); // null field
+ Assert.ok(stmt.getIsNull(1)); // data is null if size is 0
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(async function test_getIsNull_for_non_null() {
+ var stmt = createStatement("SELECT name, blobber FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ Assert.ok(stmt.executeStep());
+
+ Assert.ok(!stmt.getIsNull(0));
+ Assert.ok(!stmt.getIsNull(1));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(async function test_value_type_null() {
+ var stmt = createStatement("SELECT nuller FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 1);
+ Assert.ok(stmt.executeStep());
+
+ Assert.equal(
+ Ci.mozIStorageValueArray.VALUE_TYPE_NULL,
+ stmt.getTypeOfIndex(0)
+ );
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(async function test_value_type_integer() {
+ var stmt = createStatement("SELECT id FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 1);
+ Assert.ok(stmt.executeStep());
+
+ Assert.equal(
+ Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER,
+ stmt.getTypeOfIndex(0)
+ );
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(async function test_value_type_float() {
+ var stmt = createStatement("SELECT number FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 1);
+ Assert.ok(stmt.executeStep());
+
+ Assert.equal(
+ Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT,
+ stmt.getTypeOfIndex(0)
+ );
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(async function test_value_type_text() {
+ var stmt = createStatement("SELECT name FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 1);
+ Assert.ok(stmt.executeStep());
+
+ Assert.equal(
+ Ci.mozIStorageValueArray.VALUE_TYPE_TEXT,
+ stmt.getTypeOfIndex(0)
+ );
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(async function test_value_type_blob() {
+ var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ Assert.ok(stmt.executeStep());
+
+ Assert.equal(
+ Ci.mozIStorageValueArray.VALUE_TYPE_BLOB,
+ stmt.getTypeOfIndex(0)
+ );
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(async function test_numEntries_one() {
+ var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ Assert.ok(stmt.executeStep());
+
+ Assert.equal(1, stmt.numEntries);
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(async function test_numEntries_all() {
+ var stmt = createStatement("SELECT * FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ Assert.ok(stmt.executeStep());
+
+ Assert.equal(5, stmt.numEntries);
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(async function test_getInt() {
+ var stmt = createStatement("SELECT id FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ Assert.ok(stmt.executeStep());
+
+ Assert.equal(2, stmt.getInt32(0));
+ Assert.equal(2, stmt.getInt64(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(async function test_getDouble() {
+ var stmt = createStatement("SELECT number FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ Assert.ok(stmt.executeStep());
+
+ Assert.equal(1.23, stmt.getDouble(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(async function test_getUTF8String() {
+ var stmt = createStatement("SELECT name FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 1);
+ Assert.ok(stmt.executeStep());
+
+ Assert.equal("foo", stmt.getUTF8String(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(async function test_getString() {
+ var stmt = createStatement("SELECT name FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ Assert.ok(stmt.executeStep());
+
+ Assert.equal("", stmt.getString(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(async function test_getBlob() {
+ var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ Assert.ok(stmt.executeStep());
+
+ var count = { value: 0 };
+ var arr = { value: null };
+ stmt.getBlob(0, count, arr);
+ Assert.equal(2, count.value);
+ Assert.equal(1, arr.value[0]);
+ Assert.equal(2, arr.value[1]);
+ stmt.reset();
+ stmt.finalize();
+});
diff --git a/storage/test/unit/test_unicode.js b/storage/test/unit/test_unicode.js
new file mode 100644
index 0000000000..0785a54a42
--- /dev/null
+++ b/storage/test/unit/test_unicode.js
@@ -0,0 +1,91 @@
+/* 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/. */
+
+// This file tests the unicode functions that we have added
+
+const LATIN1_AE = "\xc6"; // "Æ"
+const LATIN1_ae = "\xe6"; // "æ"
+
+add_task(async function setup() {
+ getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, name TEXT");
+
+ var stmt = createStatement("INSERT INTO test (name, id) VALUES (?1, ?2)");
+ stmt.bindByIndex(0, LATIN1_AE);
+ stmt.bindByIndex(1, 1);
+ stmt.execute();
+ stmt.bindByIndex(0, "A");
+ stmt.bindByIndex(1, 2);
+ stmt.execute();
+ stmt.bindByIndex(0, "b");
+ stmt.bindByIndex(1, 3);
+ stmt.execute();
+ stmt.bindByIndex(0, LATIN1_ae);
+ stmt.bindByIndex(1, 4);
+ stmt.execute();
+ stmt.finalize();
+
+ registerCleanupFunction(cleanup);
+});
+
+add_task(async function test_upper_ascii() {
+ var stmt = createStatement(
+ "SELECT name, id FROM test WHERE name = upper('a')"
+ );
+ Assert.ok(stmt.executeStep());
+ Assert.equal("A", stmt.getString(0));
+ Assert.equal(2, stmt.getInt32(1));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(async function test_upper_non_ascii() {
+ var stmt = createStatement(
+ "SELECT name, id FROM test WHERE name = upper(?1)"
+ );
+ stmt.bindByIndex(0, LATIN1_ae);
+ Assert.ok(stmt.executeStep());
+ Assert.equal(LATIN1_AE, stmt.getString(0));
+ Assert.equal(1, stmt.getInt32(1));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(async function test_lower_ascii() {
+ var stmt = createStatement(
+ "SELECT name, id FROM test WHERE name = lower('B')"
+ );
+ Assert.ok(stmt.executeStep());
+ Assert.equal("b", stmt.getString(0));
+ Assert.equal(3, stmt.getInt32(1));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(async function test_lower_non_ascii() {
+ var stmt = createStatement(
+ "SELECT name, id FROM test WHERE name = lower(?1)"
+ );
+ stmt.bindByIndex(0, LATIN1_AE);
+ Assert.ok(stmt.executeStep());
+ Assert.equal(LATIN1_ae, stmt.getString(0));
+ Assert.equal(4, stmt.getInt32(1));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(async function test_like_search_different() {
+ var stmt = createStatement("SELECT COUNT(*) FROM test WHERE name LIKE ?1");
+ stmt.bindByIndex(0, LATIN1_AE);
+ Assert.ok(stmt.executeStep());
+ Assert.equal(2, stmt.getInt32(0));
+ stmt.finalize();
+});
+
+add_task(async function test_like_search_same() {
+ var stmt = createStatement("SELECT COUNT(*) FROM test WHERE name LIKE ?1");
+ stmt.bindByIndex(0, LATIN1_ae);
+ Assert.ok(stmt.executeStep());
+ Assert.equal(2, stmt.getInt32(0));
+ stmt.finalize();
+});
diff --git a/storage/test/unit/test_vacuum.js b/storage/test/unit/test_vacuum.js
new file mode 100644
index 0000000000..a4cfdf714f
--- /dev/null
+++ b/storage/test/unit/test_vacuum.js
@@ -0,0 +1,372 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This file tests the Vacuum Manager and asyncVacuum().
+
+const { VacuumParticipant } = ChromeUtils.importESModule(
+ "resource://testing-common/VacuumParticipant.sys.mjs"
+);
+
+/**
+ * Sends a fake idle-daily notification to the VACUUM Manager.
+ */
+function synthesize_idle_daily() {
+ Cc["@mozilla.org/storage/vacuum;1"]
+ .getService(Ci.nsIObserver)
+ .observe(null, "idle-daily", null);
+}
+
+/**
+ * Returns a new nsIFile reference for a profile database.
+ * @param filename for the database, excluded the .sqlite extension.
+ */
+function new_db_file(name = "testVacuum") {
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append(name + ".sqlite");
+ return file;
+}
+
+function reset_vacuum_date(name = "testVacuum") {
+ let date = parseInt(Date.now() / 1000 - 31 * 86400);
+ // Set last VACUUM to a date in the past.
+ Services.prefs.setIntPref(`storage.vacuum.last.${name}.sqlite`, date);
+ return date;
+}
+
+function get_vacuum_date(name = "testVacuum") {
+ return Services.prefs.getIntPref(`storage.vacuum.last.${name}.sqlite`, 0);
+}
+
+add_setup(async function () {
+ // turn on Cu.isInAutomation
+ Services.prefs.setBoolPref(
+ "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer",
+ true
+ );
+});
+
+add_task(async function test_common_vacuum() {
+ let last_vacuum_date = reset_vacuum_date();
+ info("Test that a VACUUM correctly happens and all notifications are fired.");
+ let promiseTestVacuumBegin = TestUtils.topicObserved("test-begin-vacuum");
+ let promiseTestVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success");
+ let promiseVacuumBegin = TestUtils.topicObserved("vacuum-begin");
+ let promiseVacuumEnd = TestUtils.topicObserved("vacuum-end");
+
+ let participant = new VacuumParticipant(
+ Services.storage.openDatabase(new_db_file())
+ );
+ await participant.promiseRegistered();
+ synthesize_idle_daily();
+ // Wait for notifications.
+ await Promise.all([
+ promiseTestVacuumBegin,
+ promiseTestVacuumEnd,
+ promiseVacuumBegin,
+ promiseVacuumEnd,
+ ]);
+ Assert.greater(get_vacuum_date(), last_vacuum_date);
+ await participant.dispose();
+});
+
+add_task(async function test_skipped_if_recent_vacuum() {
+ info("Test that a VACUUM is skipped if it was run recently.");
+ Services.prefs.setIntPref(
+ "storage.vacuum.last.testVacuum.sqlite",
+ parseInt(Date.now() / 1000)
+ );
+ // Wait for VACUUM skipped notification.
+ let promiseSkipped = TestUtils.topicObserved("vacuum-skip");
+
+ let participant = new VacuumParticipant(
+ Services.storage.openDatabase(new_db_file())
+ );
+ await participant.promiseRegistered();
+ synthesize_idle_daily();
+
+ // Check that VACUUM has been skipped.
+ await promiseSkipped;
+
+ await participant.dispose();
+});
+
+add_task(async function test_page_size_change() {
+ info("Test that a VACUUM changes page_size");
+ reset_vacuum_date();
+
+ let conn = Services.storage.openDatabase(new_db_file());
+ info("Check initial page size.");
+ let stmt = conn.createStatement("PRAGMA page_size");
+ Assert.ok(stmt.executeStep());
+ Assert.equal(stmt.row.page_size, conn.defaultPageSize);
+ stmt.finalize();
+ await populateFreeList(conn);
+
+ let participant = new VacuumParticipant(conn, { expectedPageSize: 1024 });
+ await participant.promiseRegistered();
+ let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success");
+ synthesize_idle_daily();
+ await promiseVacuumEnd;
+
+ info("Check that page size was updated.");
+ stmt = conn.createStatement("PRAGMA page_size");
+ Assert.ok(stmt.executeStep());
+ Assert.equal(stmt.row.page_size, 1024);
+ stmt.finalize();
+
+ await participant.dispose();
+});
+
+add_task(async function test_skipped_optout_vacuum() {
+ info("Test that a VACUUM is skipped if the participant wants to opt-out.");
+ reset_vacuum_date();
+
+ let participant = new VacuumParticipant(
+ Services.storage.openDatabase(new_db_file()),
+ { grant: false }
+ );
+ await participant.promiseRegistered();
+ // Wait for VACUUM skipped notification.
+ let promiseSkipped = TestUtils.topicObserved("vacuum-skip");
+
+ synthesize_idle_daily();
+
+ // Check that VACUUM has been skipped.
+ await promiseSkipped;
+
+ await participant.dispose();
+});
+
+add_task(async function test_memory_database_crash() {
+ info("Test that we don't crash trying to vacuum a memory database");
+ reset_vacuum_date();
+
+ let participant = new VacuumParticipant(
+ Services.storage.openSpecialDatabase("memory")
+ );
+ await participant.promiseRegistered();
+ // Wait for VACUUM skipped notification.
+ let promiseSkipped = TestUtils.topicObserved("vacuum-skip");
+
+ synthesize_idle_daily();
+
+ // Check that VACUUM has been skipped.
+ await promiseSkipped;
+
+ await participant.dispose();
+});
+
+add_task(async function test_async_connection() {
+ info("Test we can vacuum an async connection");
+ reset_vacuum_date();
+
+ let conn = await openAsyncDatabase(new_db_file());
+ await populateFreeList(conn);
+ let participant = new VacuumParticipant(conn);
+ await participant.promiseRegistered();
+
+ let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success");
+ synthesize_idle_daily();
+ await promiseVacuumEnd;
+
+ await participant.dispose();
+});
+
+add_task(async function test_change_to_incremental_vacuum() {
+ info("Test we can change to incremental vacuum");
+ reset_vacuum_date();
+
+ let conn = Services.storage.openDatabase(new_db_file());
+ info("Check initial vacuum.");
+ let stmt = conn.createStatement("PRAGMA auto_vacuum");
+ Assert.ok(stmt.executeStep());
+ Assert.equal(stmt.row.auto_vacuum, 0);
+ stmt.finalize();
+ await populateFreeList(conn);
+
+ let participant = new VacuumParticipant(conn, { useIncrementalVacuum: true });
+ await participant.promiseRegistered();
+ let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success");
+ synthesize_idle_daily();
+ await promiseVacuumEnd;
+
+ info("Check that auto_vacuum was updated.");
+ stmt = conn.createStatement("PRAGMA auto_vacuum");
+ Assert.ok(stmt.executeStep());
+ Assert.equal(stmt.row.auto_vacuum, 2);
+ stmt.finalize();
+
+ await participant.dispose();
+});
+
+add_task(async function test_change_from_incremental_vacuum() {
+ info("Test we can change from incremental vacuum");
+ reset_vacuum_date();
+
+ let conn = Services.storage.openDatabase(new_db_file());
+ conn.executeSimpleSQL("PRAGMA auto_vacuum = 2");
+ info("Check initial vacuum.");
+ let stmt = conn.createStatement("PRAGMA auto_vacuum");
+ Assert.ok(stmt.executeStep());
+ Assert.equal(stmt.row.auto_vacuum, 2);
+ stmt.finalize();
+ await populateFreeList(conn);
+
+ let participant = new VacuumParticipant(conn, {
+ useIncrementalVacuum: false,
+ });
+ await participant.promiseRegistered();
+ let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success");
+ synthesize_idle_daily();
+ await promiseVacuumEnd;
+
+ info("Check that auto_vacuum was updated.");
+ stmt = conn.createStatement("PRAGMA auto_vacuum");
+ Assert.ok(stmt.executeStep());
+ Assert.equal(stmt.row.auto_vacuum, 0);
+ stmt.finalize();
+
+ await participant.dispose();
+});
+
+add_task(async function test_attached_vacuum() {
+ info("Test attached database is not a problem");
+ reset_vacuum_date();
+
+ let conn = Services.storage.openDatabase(new_db_file());
+ let conn2 = Services.storage.openDatabase(new_db_file("attached"));
+
+ info("Attach " + conn2.databaseFile.path);
+ conn.executeSimpleSQL(
+ `ATTACH DATABASE '${conn2.databaseFile.path}' AS attached`
+ );
+ await asyncClose(conn2);
+ let stmt = conn.createStatement("PRAGMA database_list");
+ let schemas = [];
+ while (stmt.executeStep()) {
+ schemas.push(stmt.row.name);
+ }
+ Assert.deepEqual(schemas, ["main", "attached"]);
+ stmt.finalize();
+
+ await populateFreeList(conn);
+ await populateFreeList(conn, "attached");
+
+ let participant = new VacuumParticipant(conn);
+ await participant.promiseRegistered();
+ let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success");
+ synthesize_idle_daily();
+ await promiseVacuumEnd;
+
+ await participant.dispose();
+});
+
+add_task(async function test_vacuum_fail() {
+ info("Test a failed vacuum");
+ reset_vacuum_date();
+
+ let conn = Services.storage.openDatabase(new_db_file());
+ // Cannot vacuum in a transaction.
+ conn.beginTransaction();
+ await populateFreeList(conn);
+
+ let participant = new VacuumParticipant(conn);
+ await participant.promiseRegistered();
+ let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-failure");
+ synthesize_idle_daily();
+ await promiseVacuumEnd;
+
+ conn.commitTransaction();
+ await participant.dispose();
+});
+
+add_task(async function test_async_vacuum() {
+ // Since previous tests already go through most cases, this only checks
+ // the basics of directly calling asyncVacuum().
+ info("Test synchronous connection");
+ let conn = Services.storage.openDatabase(new_db_file());
+ await populateFreeList(conn);
+ let rv = await new Promise(resolve => {
+ conn.asyncVacuum(status => {
+ resolve(status);
+ });
+ });
+ Assert.ok(Components.isSuccessCode(rv));
+ await asyncClose(conn);
+
+ info("Test asynchronous connection");
+ conn = await openAsyncDatabase(new_db_file());
+ await populateFreeList(conn);
+ rv = await new Promise(resolve => {
+ conn.asyncVacuum(status => {
+ resolve(status);
+ });
+ });
+ Assert.ok(Components.isSuccessCode(rv));
+ await asyncClose(conn);
+});
+
+// Chunked growth is disabled on Android, so this test is pointless there.
+add_task(
+ { skip_if: () => AppConstants.platform == "android" },
+ async function test_vacuum_growth() {
+ // Tests vacuum doesn't nullify chunked growth.
+ let conn = Services.storage.openDatabase(new_db_file("incremental"));
+ conn.executeSimpleSQL("PRAGMA auto_vacuum = INCREMENTAL");
+ conn.setGrowthIncrement(2 * conn.defaultPageSize, "");
+ await populateFreeList(conn);
+ let stmt = conn.createStatement("PRAGMA freelist_count");
+ let count = 0;
+ Assert.ok(stmt.executeStep());
+ count = stmt.row.freelist_count;
+ stmt.reset();
+ Assert.greater(count, 2, "There's more than 2 page in freelist");
+
+ let rv = await new Promise(resolve => {
+ conn.asyncVacuum(status => {
+ resolve(status);
+ }, true);
+ });
+ Assert.ok(Components.isSuccessCode(rv));
+
+ Assert.ok(stmt.executeStep());
+ Assert.equal(
+ stmt.row.freelist_count,
+ 2,
+ "chunked growth space was preserved"
+ );
+ stmt.reset();
+
+ // A full vacuuum should not be executed if there's less free pages than
+ // chunked growth.
+ rv = await new Promise(resolve => {
+ conn.asyncVacuum(status => {
+ resolve(status);
+ });
+ });
+ Assert.ok(Components.isSuccessCode(rv));
+
+ Assert.ok(stmt.executeStep());
+ Assert.equal(
+ stmt.row.freelist_count,
+ 2,
+ "chunked growth space was preserved"
+ );
+ stmt.finalize();
+
+ await asyncClose(conn);
+ }
+);
+
+async function populateFreeList(conn, schema = "main") {
+ await executeSimpleSQLAsync(conn, `CREATE TABLE ${schema}.test (id TEXT)`);
+ await executeSimpleSQLAsync(
+ conn,
+ `INSERT INTO ${schema}.test
+ VALUES ${Array.from({ length: 3000 }, () => Math.random()).map(
+ v => "('" + v + "')"
+ )}`
+ );
+ await executeSimpleSQLAsync(conn, `DROP TABLE ${schema}.test`);
+}
diff --git a/storage/test/unit/xpcshell.toml b/storage/test/unit/xpcshell.toml
new file mode 100644
index 0000000000..2560bf3281
--- /dev/null
+++ b/storage/test/unit/xpcshell.toml
@@ -0,0 +1,96 @@
+[DEFAULT]
+head = "head_storage.js"
+support-files = [
+ "baddataDB.sqlite",
+ "corruptDB.sqlite",
+ "fakeDB.sqlite",
+ "goodDB.sqlite",
+ "locale_collation.txt",
+ "VacuumParticipant.sys.mjs",
+]
+
+["test_bug-365166.js"]
+
+["test_bug-393952.js"]
+
+["test_bug-429521.js"]
+
+["test_bug-444233.js"]
+
+["test_cache_size.js"]
+
+["test_chunk_growth.js"]
+# Bug 676981: test fails consistently on Android
+fail-if = ["os == 'android'"]
+
+["test_connection_asyncClose.js"]
+
+["test_connection_executeAsync.js"]
+
+["test_connection_executeSimpleSQLAsync.js"]
+
+["test_connection_failsafe_close.js"]
+# The failsafe close mechanism asserts when performing SpinningSynchronousClose
+# on debug builds, so we can only test on non-debug builds.
+skip-if = ["debug"]
+
+["test_connection_interrupt.js"]
+
+["test_connection_online_backup.js"]
+
+["test_default_journal_size_limit.js"]
+
+["test_js_helpers.js"]
+
+["test_levenshtein.js"]
+
+["test_like.js"]
+
+["test_like_escape.js"]
+
+["test_locale_collation.js"]
+
+["test_minimizeMemory.js"]
+
+["test_page_size_is_32k.js"]
+
+["test_persist_journal.js"]
+
+["test_readonly-immutable-nolock_vfs.js"]
+
+["test_retry_on_busy.js"]
+
+["test_sqlite_secure_delete.js"]
+
+["test_statement_executeAsync.js"]
+
+["test_statement_wrapper_automatically.js"]
+
+["test_storage_connection.js"]
+# Bug 676981: test fails consistently on Android
+fail-if = ["os == 'android'"]
+
+["test_storage_ext.js"]
+
+["test_storage_ext_fts3.js"]
+skip-if = ["appname != 'thunderbird' && appname != 'seamonkey'"]
+
+["test_storage_ext_fts5.js"]
+
+["test_storage_function.js"]
+
+["test_storage_progresshandler.js"]
+
+["test_storage_service.js"]
+
+["test_storage_service_special.js"]
+
+["test_storage_service_unshared.js"]
+
+["test_storage_statement.js"]
+
+["test_storage_value_array.js"]
+
+["test_unicode.js"]
+
+["test_vacuum.js"]