summaryrefslogtreecommitdiffstats
path: root/storage/test
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /storage/test
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'storage/test')
-rw-r--r--storage/test/moz.build37
-rw-r--r--storage/test/storage_test_harness.h389
-rw-r--r--storage/test/storage_test_harness_tail.h28
-rw-r--r--storage/test/test_AsXXX_helpers.cpp127
-rw-r--r--storage/test/test_StatementCache.cpp163
-rw-r--r--storage/test/test_asyncStatementExecution_transaction.cpp509
-rw-r--r--storage/test/test_async_callbacks_with_spun_event_loops.cpp174
-rw-r--r--storage/test/test_binding_params.cpp215
-rw-r--r--storage/test/test_deadlock_detector.cpp602
-rw-r--r--storage/test/test_file_perms.cpp44
-rw-r--r--storage/test/test_mutex.cpp85
-rw-r--r--storage/test/test_service_init_background_thread.cpp58
-rw-r--r--storage/test/test_statement_scoper.cpp104
-rw-r--r--storage/test/test_transaction_helper.cpp175
-rw-r--r--storage/test/test_true_async.cpp186
-rw-r--r--storage/test/test_unlock_notify.cpp266
-rw-r--r--storage/test/unit/corruptDB.sqlitebin0 -> 32772 bytes
-rw-r--r--storage/test/unit/fakeDB.sqlite1
-rw-r--r--storage/test/unit/head_storage.js372
-rw-r--r--storage/test/unit/locale_collation.txt174
-rw-r--r--storage/test/unit/test_bug-365166.js26
-rw-r--r--storage/test/unit/test_bug-393952.js38
-rw-r--r--storage/test/unit/test_bug-429521.js46
-rw-r--r--storage/test/unit/test_bug-444233.js51
-rw-r--r--storage/test/unit/test_cache_size.js72
-rw-r--r--storage/test/unit/test_chunk_growth.js52
-rw-r--r--storage/test/unit/test_connection_asyncClose.js125
-rw-r--r--storage/test/unit/test_connection_executeAsync.js171
-rw-r--r--storage/test/unit/test_connection_executeSimpleSQLAsync.js79
-rw-r--r--storage/test/unit/test_js_helpers.js125
-rw-r--r--storage/test/unit/test_levenshtein.js74
-rw-r--r--storage/test/unit/test_like.js202
-rw-r--r--storage/test/unit/test_like_escape.js60
-rw-r--r--storage/test/unit/test_locale_collation.js304
-rw-r--r--storage/test/unit/test_page_size_is_32k.js35
-rw-r--r--storage/test/unit/test_sqlite_secure_delete.js80
-rw-r--r--storage/test/unit/test_statement_executeAsync.js998
-rw-r--r--storage/test/unit/test_statement_wrapper_automatically.js167
-rw-r--r--storage/test/unit/test_storage_aggregates.js116
-rw-r--r--storage/test/unit/test_storage_connection.js763
-rw-r--r--storage/test/unit/test_storage_fulltextindex.js86
-rw-r--r--storage/test/unit/test_storage_function.js95
-rw-r--r--storage/test/unit/test_storage_progresshandler.js111
-rw-r--r--storage/test/unit/test_storage_service.js142
-rw-r--r--storage/test/unit/test_storage_service_unshared.js35
-rw-r--r--storage/test/unit/test_storage_statement.js184
-rw-r--r--storage/test/unit/test_storage_value_array.js182
-rw-r--r--storage/test/unit/test_telemetry_vfs.js30
-rw-r--r--storage/test/unit/test_unicode.js83
-rw-r--r--storage/test/unit/test_vacuum.js335
-rw-r--r--storage/test/unit/vacuumParticipant.js125
-rw-r--r--storage/test/unit/vacuumParticipant.manifest3
-rw-r--r--storage/test/unit/xpcshell.ini46
53 files changed, 8750 insertions, 0 deletions
diff --git a/storage/test/moz.build b/storage/test/moz.build
new file mode 100644
index 000000000..cf8b49c50
--- /dev/null
+++ b/storage/test/moz.build
@@ -0,0 +1,37 @@
+# -*- 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.ini']
+
+GeckoCppUnitTests([
+ 'test_AsXXX_helpers',
+ 'test_async_callbacks_with_spun_event_loops',
+ 'test_asyncStatementExecution_transaction',
+ 'test_binding_params',
+ 'test_file_perms',
+ 'test_mutex',
+ 'test_service_init_background_thread',
+ 'test_statement_scoper',
+ 'test_StatementCache',
+ 'test_transaction_helper',
+ 'test_true_async',
+ 'test_unlock_notify',
+])
+
+if CONFIG['MOZ_DEBUG'] and CONFIG['OS_ARCH'] not in ('WINNT', 'Darwin'):
+ # FIXME bug 523392: test_deadlock_detector doesn't like Windows
+ # FIXME bug 523378: also fails on OS X
+ GeckoCppUnitTests([
+ 'test_deadlock_detector',
+ ])
+
+LOCAL_INCLUDES += [
+ '..',
+]
+
+USE_LIBS += [
+ 'sqlite',
+]
diff --git a/storage/test/storage_test_harness.h b/storage/test/storage_test_harness.h
new file mode 100644
index 000000000..043652b1c
--- /dev/null
+++ b/storage/test/storage_test_harness.h
@@ -0,0 +1,389 @@
+/* -*- 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 "TestHarness.h"
+
+#include "nsMemory.h"
+#include "prthread.h"
+#include "nsThreadUtils.h"
+#include "nsDirectoryServiceDefs.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"
+
+static int gTotalTests = 0;
+static int gPassedTests = 0;
+
+#define do_check_true(aCondition) \
+ PR_BEGIN_MACRO \
+ gTotalTests++; \
+ if (aCondition) { \
+ gPassedTests++; \
+ } else { \
+ fail("%s | Expected true, got false at line %d", __FILE__, __LINE__); \
+ } \
+ PR_END_MACRO
+
+#define do_check_false(aCondition) \
+ PR_BEGIN_MACRO \
+ gTotalTests++; \
+ if (!aCondition) { \
+ gPassedTests++; \
+ } else { \
+ fail("%s | Expected false, got true at line %d", __FILE__, __LINE__); \
+ } \
+ PR_END_MACRO
+
+#define do_check_success(aResult) \
+ do_check_true(NS_SUCCEEDED(aResult))
+
+#ifdef LINUX
+// XXX Linux opt builds on tinderbox are orange due to linking with stdlib.
+// This is sad and annoying, but it's a workaround that works.
+#define do_check_eq(aExpected, aActual) \
+ do_check_true(aExpected == aActual)
+#else
+#include <sstream>
+// Print nsresult as uint32_t
+std::ostream& operator<<(std::ostream& aStream, const nsresult aInput)
+{
+ return aStream << static_cast<uint32_t>(aInput);
+}
+#define do_check_eq(aExpected, aActual) \
+ PR_BEGIN_MACRO \
+ gTotalTests++; \
+ if (aExpected == aActual) { \
+ gPassedTests++; \
+ } else { \
+ std::ostringstream temp; \
+ temp << __FILE__ << " | Expected '" << aExpected << "', got '"; \
+ temp << aActual <<"' at line " << __LINE__; \
+ fail(temp.str().c_str()); \
+ } \
+ PR_END_MACRO
+#endif
+
+#define do_check_ok(aInvoc) do_check_true((aInvoc) == SQLITE_OK)
+
+already_AddRefed<mozIStorageService>
+getService()
+{
+ nsCOMPtr<mozIStorageService> ss =
+ do_GetService("@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("memory", getter_AddRefs(conn));
+ do_check_success(rv);
+ return conn.forget();
+}
+
+already_AddRefed<mozIStorageConnection>
+getDatabase()
+{
+ nsCOMPtr<nsIFile> dbFile;
+ (void)NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(dbFile));
+ NS_ASSERTION(dbFile, "The directory doesn't exists?!");
+
+ nsresult rv = dbFile->Append(NS_LITERAL_STRING("storage_test_db.sqlite"));
+ do_check_success(rv);
+
+ nsCOMPtr<mozIStorageService> ss = getService();
+ nsCOMPtr<mozIStorageConnection> conn;
+ rv = ss->OpenDatabase(dbFile, getter_AddRefs(conn));
+ do_check_success(rv);
+ return conn.forget();
+}
+
+
+class AsyncStatementSpinner : public mozIStorageStatementCallback
+ , public mozIStorageCompletionCallback
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGESTATEMENTCALLBACK
+ NS_DECL_MOZISTORAGECOMPLETIONCALLBACK
+
+ AsyncStatementSpinner();
+
+ void SpinUntilCompleted();
+
+ uint16_t completionReason;
+
+protected:
+ virtual ~AsyncStatementSpinner() {}
+ volatile bool mCompleted;
+};
+
+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;
+
+////////////////////////////////////////////////////////////////////////////////
+//// 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.
+ */
+PRThread *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)
+{
+ PRThread *curThread = ::PR_GetCurrentThread();
+ if (curThread == watched_thread)
+ mutex_used_on_watched_thread = true;
+ else
+ last_non_watched_thread = curThread;
+ 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);
+}
+
+void hook_sqlite_mutex()
+{
+ // 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));
+}
+
+/**
+ * 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;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+//// 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)
+ : 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;
+ 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)
+{
+ // 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(
+ NS_LITERAL_CSTRING("SELECT 1"),
+ getter_AddRefs(stmt));
+ blocking_async_execute(stmt);
+ stmt->Finalize();
+
+ nsCOMPtr<nsIThreadManager> threadMan =
+ do_GetService("@mozilla.org/thread-manager;1");
+ nsCOMPtr<nsIThread> asyncThread;
+ threadMan->GetThreadFromPRThread(last_non_watched_thread,
+ getter_AddRefs(asyncThread));
+
+ // 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);
+ PRThread *allegedPRThread;
+ (void)allegedAsyncThread->GetPRThread(&allegedPRThread);
+ do_check_eq(allegedPRThread, last_non_watched_thread);
+ return asyncThread.forget();
+}
diff --git a/storage/test/storage_test_harness_tail.h b/storage/test/storage_test_harness_tail.h
new file mode 100644
index 000000000..4ad25c719
--- /dev/null
+++ b/storage/test/storage_test_harness_tail.h
@@ -0,0 +1,28 @@
+/* -*- 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 TEST_NAME
+#error "Must #define TEST_NAME before including storage_test_harness_tail.h"
+#endif
+
+#ifndef TEST_FILE
+#error "Must #define TEST_FILE before include storage_test_harness_tail.h"
+#endif
+
+int
+main(int aArgc,
+ char **aArgv)
+{
+ ScopedXPCOM xpcom(TEST_NAME);
+
+ for (size_t i = 0; i < mozilla::ArrayLength(gTests); i++)
+ gTests[i]();
+
+ if (gPassedTests == gTotalTests)
+ passed(TEST_FILE);
+
+ (void)printf("%i of %i tests passed\n", gPassedTests, gTotalTests);
+}
diff --git a/storage/test/test_AsXXX_helpers.cpp b/storage/test/test_AsXXX_helpers.cpp
new file mode 100644
index 000000000..4edf7b93a
--- /dev/null
+++ b/storage/test/test_AsXXX_helpers.cpp
@@ -0,0 +1,127 @@
+/*
+ *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"
+
+/**
+ * This file tests AsXXX (AsInt32, AsInt64, ...) helpers.
+ */
+
+////////////////////////////////////////////////////////////////////////////////
+//// Event Loop Spinning
+
+class Spinner : public AsyncStatementSpinner
+{
+public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_ASYNCSTATEMENTSPINNER
+ Spinner() {}
+protected:
+ virtual ~Spinner() {}
+};
+
+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;
+}
+
+void
+test_NULLFallback()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ nsCOMPtr<mozIStorageStatement> stmt;
+ (void)db->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT NULL"
+ ), 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);
+}
+
+void
+test_asyncNULLFallback()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ nsCOMPtr<mozIStorageAsyncStatement> stmt;
+ (void)db->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "SELECT NULL"
+ ), 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();
+
+}
+
+void (*gTests[])(void) = {
+ test_NULLFallback
+, test_asyncNULLFallback
+};
+
+const char *file = __FILE__;
+#define TEST_NAME "AsXXX helpers"
+#define TEST_FILE file
+#include "storage_test_harness_tail.h"
diff --git a/storage/test/test_StatementCache.cpp b/storage/test/test_StatementCache.cpp
new file mode 100644
index 000000000..731a858de
--- /dev/null
+++ b/storage/test/test_StatementCache.cpp
@@ -0,0 +1,163 @@
+/* -*- 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
+
+template<typename StringType>
+void
+test_GetCachedStatement()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+ SyncCache cache(db);
+
+ StringType 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());
+}
+
+template <typename StringType>
+void
+test_FinalizeStatements()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+ SyncCache cache(db);
+
+ StringType 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());
+}
+
+template<typename StringType>
+void
+test_GetCachedAsyncStatement()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+ AsyncCache cache(db);
+
+ StringType 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());
+}
+
+template <typename StringType>
+void
+test_FinalizeAsyncStatements()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+ AsyncCache cache(db);
+
+ StringType 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));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Test Harness Stuff
+
+void (*gTests[])(void) = {
+ test_GetCachedStatement<const char []>,
+ test_GetCachedStatement<StringWrapper>,
+ test_FinalizeStatements<const char []>,
+ test_FinalizeStatements<StringWrapper>,
+ test_GetCachedAsyncStatement<const char []>,
+ test_GetCachedAsyncStatement<StringWrapper>,
+ test_FinalizeAsyncStatements<const char []>,
+ test_FinalizeAsyncStatements<StringWrapper>,
+};
+
+const char *file = __FILE__;
+#define TEST_NAME "StatementCache"
+#define TEST_FILE file
+#include "storage_test_harness_tail.h"
diff --git a/storage/test/test_asyncStatementExecution_transaction.cpp b/storage/test/test_asyncStatementExecution_transaction.cpp
new file mode 100644
index 000000000..7e94bd80e
--- /dev/null
+++ b/storage/test/test_asyncStatementExecution_transaction.cpp
@@ -0,0 +1,509 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+#include "storage_test_harness.h"
+
+#include "nsIEventTarget.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,
+ mozIStorageBaseStatement **aStmts,
+ uint32_t aStmtsLen,
+ 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, aStmtsLen, 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 < aStmtsLen; ++i) {
+ aStmts[i]->Finalize();
+ }
+ blocking_async_close(aDB);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Tests
+
+/**
+ * Test that executing multiple readonly AsyncStatements doesn't create a
+ * transaction.
+ */
+void
+test_MultipleAsyncReadStatements()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageAsyncStatement> stmt1;
+ db->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "SELECT * FROM sqlite_master"
+ ), getter_AddRefs(stmt1));
+
+ nsCOMPtr<mozIStorageAsyncStatement> stmt2;
+ db->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "SELECT * FROM sqlite_master"
+ ), getter_AddRefs(stmt2));
+
+ mozIStorageBaseStatement *stmts[] = {
+ stmt1,
+ stmt2,
+ };
+
+ check_transaction(db, stmts, ArrayLength(stmts), false);
+}
+
+/**
+ * Test that executing multiple readonly Statements doesn't create a
+ * transaction.
+ */
+void
+test_MultipleReadStatements()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageStatement> stmt1;
+ db->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT * FROM sqlite_master"
+ ), getter_AddRefs(stmt1));
+
+ nsCOMPtr<mozIStorageStatement> stmt2;
+ db->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT * FROM sqlite_master"
+ ), getter_AddRefs(stmt2));
+
+ mozIStorageBaseStatement *stmts[] = {
+ stmt1,
+ stmt2,
+ };
+
+ check_transaction(db, stmts, ArrayLength(stmts), false);
+}
+
+/**
+ * Test that executing multiple AsyncStatements causing writes creates a
+ * transaction.
+ */
+void
+test_MultipleAsyncReadWriteStatements()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageAsyncStatement> stmt1;
+ db->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "SELECT * FROM sqlite_master"
+ ), getter_AddRefs(stmt1));
+
+ nsCOMPtr<mozIStorageAsyncStatement> stmt2;
+ db->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "CREATE TABLE test (id INTEGER PRIMARY KEY)"
+ ), getter_AddRefs(stmt2));
+
+ mozIStorageBaseStatement *stmts[] = {
+ stmt1,
+ stmt2,
+ };
+
+ check_transaction(db, stmts, ArrayLength(stmts), true);
+}
+
+/**
+ * Test that executing multiple Statements causing writes creates a transaction.
+ */
+void
+test_MultipleReadWriteStatements()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageStatement> stmt1;
+ db->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT * FROM sqlite_master"
+ ), getter_AddRefs(stmt1));
+
+ nsCOMPtr<mozIStorageStatement> stmt2;
+ db->CreateStatement(NS_LITERAL_CSTRING(
+ "CREATE TABLE test (id INTEGER PRIMARY KEY)"
+ ), getter_AddRefs(stmt2));
+
+ mozIStorageBaseStatement *stmts[] = {
+ stmt1,
+ stmt2,
+ };
+
+ check_transaction(db, stmts, ArrayLength(stmts), true);
+}
+
+/**
+ * Test that executing multiple AsyncStatements causing writes creates a
+ * single transaction.
+ */
+void
+test_MultipleAsyncWriteStatements()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageAsyncStatement> stmt1;
+ db->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "CREATE TABLE test1 (id INTEGER PRIMARY KEY)"
+ ), getter_AddRefs(stmt1));
+
+ nsCOMPtr<mozIStorageAsyncStatement> stmt2;
+ db->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "CREATE TABLE test2 (id INTEGER PRIMARY KEY)"
+ ), getter_AddRefs(stmt2));
+
+ mozIStorageBaseStatement *stmts[] = {
+ stmt1,
+ stmt2,
+ };
+
+ check_transaction(db, stmts, ArrayLength(stmts), true);
+}
+
+/**
+ * Test that executing multiple Statements causing writes creates a
+ * single transaction.
+ */
+void
+test_MultipleWriteStatements()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageStatement> stmt1;
+ db->CreateStatement(NS_LITERAL_CSTRING(
+ "CREATE TABLE test1 (id INTEGER PRIMARY KEY)"
+ ), getter_AddRefs(stmt1));
+
+ nsCOMPtr<mozIStorageStatement> stmt2;
+ db->CreateStatement(NS_LITERAL_CSTRING(
+ "CREATE TABLE test2 (id INTEGER PRIMARY KEY)"
+ ), getter_AddRefs(stmt2));
+
+ mozIStorageBaseStatement *stmts[] = {
+ stmt1,
+ stmt2,
+ };
+
+ check_transaction(db, stmts, ArrayLength(stmts), true);
+}
+
+/**
+ * Test that executing a single read-only AsyncStatement doesn't create a
+ * transaction.
+ */
+void
+test_SingleAsyncReadStatement()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageAsyncStatement> stmt;
+ db->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "SELECT * FROM sqlite_master"
+ ), getter_AddRefs(stmt));
+
+ mozIStorageBaseStatement *stmts[] = {
+ stmt,
+ };
+
+ check_transaction(db, stmts, ArrayLength(stmts), false);
+}
+
+/**
+ * Test that executing a single read-only Statement doesn't create a
+ * transaction.
+ */
+void
+test_SingleReadStatement()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageStatement> stmt;
+ db->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT * FROM sqlite_master"
+ ), getter_AddRefs(stmt));
+
+ mozIStorageBaseStatement *stmts[] = {
+ stmt,
+ };
+
+ check_transaction(db, stmts, ArrayLength(stmts), false);
+}
+
+/**
+ * Test that executing a single AsyncStatement causing writes creates a
+ * transaction.
+ */
+void
+test_SingleAsyncWriteStatement()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageAsyncStatement> stmt;
+ db->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "CREATE TABLE test (id INTEGER PRIMARY KEY)"
+ ), getter_AddRefs(stmt));
+
+ mozIStorageBaseStatement *stmts[] = {
+ stmt,
+ };
+
+ check_transaction(db, stmts, ArrayLength(stmts), true);
+}
+
+/**
+ * Test that executing a single Statement causing writes creates a transaction.
+ */
+void
+test_SingleWriteStatement()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageStatement> stmt;
+ db->CreateStatement(NS_LITERAL_CSTRING(
+ "CREATE TABLE test (id INTEGER PRIMARY KEY)"
+ ), getter_AddRefs(stmt));
+
+ mozIStorageBaseStatement *stmts[] = {
+ stmt,
+ };
+
+ check_transaction(db, stmts, ArrayLength(stmts), true);
+}
+
+/**
+ * Test that executing a single read-only AsyncStatement with multiple params
+ * doesn't create a transaction.
+ */
+void
+test_MultipleParamsAsyncReadStatement()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageAsyncStatement> stmt;
+ db->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "SELECT :param FROM sqlite_master"
+ ), 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(NS_LITERAL_CSTRING("param"), 1);
+ paramsArray->AddParams(params);
+ }
+ stmt->BindParameters(paramsArray);
+ paramsArray = nullptr;
+
+ mozIStorageBaseStatement *stmts[] = {
+ stmt,
+ };
+
+ check_transaction(db, stmts, ArrayLength(stmts), false);
+}
+
+/**
+ * Test that executing a single read-only Statement with multiple params
+ * doesn't create a transaction.
+ */
+void
+test_MultipleParamsReadStatement()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageStatement> stmt;
+ db->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT :param FROM sqlite_master"
+ ), 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(NS_LITERAL_CSTRING("param"), 1);
+ paramsArray->AddParams(params);
+ }
+ stmt->BindParameters(paramsArray);
+ paramsArray = nullptr;
+
+ mozIStorageBaseStatement *stmts[] = {
+ stmt,
+ };
+
+ check_transaction(db, stmts, ArrayLength(stmts), false);
+}
+
+/**
+ * Test that executing a single write AsyncStatement with multiple params
+ * creates a transaction.
+ */
+void
+test_MultipleParamsAsyncWriteStatement()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create a table for writes
+ nsCOMPtr<mozIStorageStatement> tableStmt;
+ db->CreateStatement(NS_LITERAL_CSTRING(
+ "CREATE TABLE test (id INTEGER PRIMARY KEY)"
+ ), getter_AddRefs(tableStmt));
+ tableStmt->Execute();
+ tableStmt->Finalize();
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageAsyncStatement> stmt;
+ db->CreateAsyncStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM test WHERE id = :param"
+ ), 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(NS_LITERAL_CSTRING("param"), 1);
+ paramsArray->AddParams(params);
+ }
+ stmt->BindParameters(paramsArray);
+ paramsArray = nullptr;
+
+ mozIStorageBaseStatement *stmts[] = {
+ stmt,
+ };
+
+ check_transaction(db, stmts, ArrayLength(stmts), true);
+}
+
+/**
+ * Test that executing a single write Statement with multiple params
+ * creates a transaction.
+ */
+void
+test_MultipleParamsWriteStatement()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // -- create a table for writes
+ nsCOMPtr<mozIStorageStatement> tableStmt;
+ db->CreateStatement(NS_LITERAL_CSTRING(
+ "CREATE TABLE test (id INTEGER PRIMARY KEY)"
+ ), getter_AddRefs(tableStmt));
+ tableStmt->Execute();
+ tableStmt->Finalize();
+
+ // -- create statements and execute them
+ nsCOMPtr<mozIStorageStatement> stmt;
+ db->CreateStatement(NS_LITERAL_CSTRING(
+ "DELETE FROM test WHERE id = :param"
+ ), 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(NS_LITERAL_CSTRING("param"), 1);
+ paramsArray->AddParams(params);
+ }
+ stmt->BindParameters(paramsArray);
+ paramsArray = nullptr;
+
+ mozIStorageBaseStatement *stmts[] = {
+ stmt,
+ };
+
+ check_transaction(db, stmts, ArrayLength(stmts), true);
+}
+
+void (*gTests[])(void) = {
+ test_MultipleAsyncReadStatements,
+ test_MultipleReadStatements,
+ test_MultipleAsyncReadWriteStatements,
+ test_MultipleReadWriteStatements,
+ test_MultipleAsyncWriteStatements,
+ test_MultipleWriteStatements,
+ test_SingleAsyncReadStatement,
+ test_SingleReadStatement,
+ test_SingleAsyncWriteStatement,
+ test_SingleWriteStatement,
+ test_MultipleParamsAsyncReadStatement,
+ test_MultipleParamsReadStatement,
+ test_MultipleParamsAsyncWriteStatement,
+ test_MultipleParamsWriteStatement,
+};
+
+const char *file = __FILE__;
+#define TEST_NAME "async statement execution transaction"
+#define TEST_FILE file
+#include "storage_test_harness_tail.h"
diff --git a/storage/test/test_async_callbacks_with_spun_event_loops.cpp b/storage/test/test_async_callbacks_with_spun_event_loops.cpp
new file mode 100644
index 000000000..75687c7f3
--- /dev/null
+++ b/storage/test/test_async_callbacks_with_spun_event_loops.cpp
@@ -0,0 +1,174 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+#include "storage_test_harness.h"
+#include "prthread.h"
+#include "nsIEventTarget.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) {
+ NS_RUNTIMEABORT("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) {
+ NS_RUNTIMEABORT("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
+
+void
+test_SpinEventsLoopInHandleResult()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // Create a test table and populate it.
+ nsCOMPtr<mozIStorageStatement> stmt;
+ db->CreateStatement(NS_LITERAL_CSTRING(
+ "CREATE TABLE test (id INTEGER PRIMARY KEY)"
+ ), getter_AddRefs(stmt));
+ stmt->Execute();
+ stmt->Finalize();
+
+ db->CreateStatement(NS_LITERAL_CSTRING(
+ "INSERT INTO test (id) VALUES (?)"
+ ), getter_AddRefs(stmt));
+ for (int32_t i = 0; i < 30; ++i) {
+ stmt->BindInt32ByIndex(0, i);
+ stmt->Execute();
+ stmt->Reset();
+ }
+ stmt->Finalize();
+
+ db->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT * FROM test"
+ ), 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);
+}
+
+void
+test_SpinEventsLoopInHandleError()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // Create a test table and populate it.
+ nsCOMPtr<mozIStorageStatement> stmt;
+ db->CreateStatement(NS_LITERAL_CSTRING(
+ "CREATE TABLE test (id INTEGER PRIMARY KEY)"
+ ), getter_AddRefs(stmt));
+ stmt->Execute();
+ stmt->Finalize();
+
+ db->CreateStatement(NS_LITERAL_CSTRING(
+ "INSERT INTO test (id) VALUES (1)"
+ ), getter_AddRefs(stmt));
+ stmt->Execute();
+ stmt->Finalize();
+
+ // This will cause a constraint error.
+ db->CreateStatement(NS_LITERAL_CSTRING(
+ "INSERT INTO test (id) VALUES (1)"
+ ), 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);
+}
+
+void (*gTests[])(void) = {
+ test_SpinEventsLoopInHandleResult,
+ test_SpinEventsLoopInHandleError,
+};
+
+const char *file = __FILE__;
+#define TEST_NAME "test async callbacks with spun event loops"
+#define TEST_FILE file
+#include "storage_test_harness_tail.h"
diff --git a/storage/test/test_binding_params.cpp b/storage/test/test_binding_params.cpp
new file mode 100644
index 000000000..e5af6009d
--- /dev/null
+++ b/storage/test/test_binding_params.cpp
@@ -0,0 +1,215 @@
+/* -*- 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"
+
+using namespace mozilla;
+
+/**
+ * This file tests binding and reading out string parameters through the
+ * mozIStorageStatement API.
+ */
+
+void
+test_ASCIIString()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // Create table with a single string column.
+ (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE TABLE test (str STRING)"
+ ));
+
+ // Create statements to INSERT and SELECT the string.
+ nsCOMPtr<mozIStorageStatement> insert, select;
+ (void)db->CreateStatement(NS_LITERAL_CSTRING(
+ "INSERT INTO test (str) VALUES (?1)"
+ ), getter_AddRefs(insert));
+ (void)db->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT str FROM test"
+ ), 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(NS_LITERAL_CSTRING("DELETE FROM test"));
+}
+
+void
+test_CString()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // Create table with a single string column.
+ (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE TABLE test (str STRING)"
+ ));
+
+ // Create statements to INSERT and SELECT the string.
+ nsCOMPtr<mozIStorageStatement> insert, select;
+ (void)db->CreateStatement(NS_LITERAL_CSTRING(
+ "INSERT INTO test (str) VALUES (?1)"
+ ), getter_AddRefs(insert));
+ (void)db->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT str FROM test"
+ ), 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(NS_LITERAL_CSTRING("DELETE FROM test"));
+}
+
+void
+test_UTFStrings()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // Create table with a single string column.
+ (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE TABLE test (str STRING)"
+ ));
+
+ // Create statements to INSERT and SELECT the string.
+ nsCOMPtr<mozIStorageStatement> insert, select;
+ (void)db->CreateStatement(NS_LITERAL_CSTRING(
+ "INSERT INTO test (str) VALUES (?1)"
+ ), getter_AddRefs(insert));
+ (void)db->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT str FROM test"
+ ), getter_AddRefs(select));
+
+ // Roundtrip a UTF8 string through the table, using UTF8 input and output.
+ static const char sCharArray[] =
+ "I'm a \xc3\xbb\xc3\xbc\xc3\xa2\xc3\xa4\xc3\xa7 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(NS_LITERAL_CSTRING("DELETE FROM test"));
+
+ // 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(NS_LITERAL_CSTRING("DELETE FROM test"));
+}
+
+void (*gTests[])(void) = {
+ test_ASCIIString,
+ test_CString,
+ test_UTFStrings,
+};
+
+const char *file = __FILE__;
+#define TEST_NAME "binding string params"
+#define TEST_FILE file
+#include "storage_test_harness_tail.h"
diff --git a/storage/test/test_deadlock_detector.cpp b/storage/test/test_deadlock_detector.cpp
new file mode 100644
index 000000000..1cbd79467
--- /dev/null
+++ b/storage/test/test_deadlock_detector.cpp
@@ -0,0 +1,602 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: sw=4 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 a copy of xpcom/tests/TestDeadlockDetector.cpp, but all
+ * mutexes were turned into SQLiteMutexes.
+ */
+
+#include "prenv.h"
+#include "prerror.h"
+#include "prio.h"
+#include "prproces.h"
+
+#include "nsMemory.h"
+
+#include "mozilla/CondVar.h"
+#include "mozilla/ReentrantMonitor.h"
+#include "SQLiteMutex.h"
+
+#include "TestHarness.h"
+
+using namespace mozilla;
+
+/**
+ * Helper class to allocate a sqlite3_mutex for our SQLiteMutex. Also makes
+ * keeping the test files in sync easier.
+ */
+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;
+};
+
+static PRThread*
+spawn(void (*run)(void*), void* arg)
+{
+ return PR_CreateThread(PR_SYSTEM_THREAD,
+ run,
+ arg,
+ PR_PRIORITY_NORMAL,
+ PR_GLOBAL_THREAD,
+ PR_JOINABLE_THREAD,
+ 0);
+}
+
+#define PASS() \
+ do { \
+ passed(__FUNCTION__); \
+ return NS_OK; \
+ } while (0)
+
+#define FAIL(why) \
+ do { \
+ fail("%s | %s - %s", __FILE__, __FUNCTION__, why); \
+ return NS_ERROR_FAILURE; \
+ } while (0)
+
+//-----------------------------------------------------------------------------
+
+static const char* sPathToThisBinary;
+static const char* sAssertBehaviorEnv = "XPCOM_DEBUG_BREAK=abort";
+
+class Subprocess
+{
+public:
+ // not available until process finishes
+ int32_t mExitCode;
+ nsCString mStdout;
+ nsCString mStderr;
+
+ explicit Subprocess(const char* aTestName) {
+ // set up stdio redirection
+ PRFileDesc* readStdin; PRFileDesc* writeStdin;
+ PRFileDesc* readStdout; PRFileDesc* writeStdout;
+ PRFileDesc* readStderr; PRFileDesc* writeStderr;
+ PRProcessAttr* pattr = PR_NewProcessAttr();
+
+ NS_ASSERTION(pattr, "couldn't allocate process attrs");
+
+ NS_ASSERTION(PR_SUCCESS == PR_CreatePipe(&readStdin, &writeStdin),
+ "couldn't create child stdin pipe");
+ NS_ASSERTION(PR_SUCCESS == PR_SetFDInheritable(readStdin, true),
+ "couldn't set child stdin inheritable");
+ PR_ProcessAttrSetStdioRedirect(pattr, PR_StandardInput, readStdin);
+
+ NS_ASSERTION(PR_SUCCESS == PR_CreatePipe(&readStdout, &writeStdout),
+ "couldn't create child stdout pipe");
+ NS_ASSERTION(PR_SUCCESS == PR_SetFDInheritable(writeStdout, true),
+ "couldn't set child stdout inheritable");
+ PR_ProcessAttrSetStdioRedirect(pattr, PR_StandardOutput, writeStdout);
+
+ NS_ASSERTION(PR_SUCCESS == PR_CreatePipe(&readStderr, &writeStderr),
+ "couldn't create child stderr pipe");
+ NS_ASSERTION(PR_SUCCESS == PR_SetFDInheritable(writeStderr, true),
+ "couldn't set child stderr inheritable");
+ PR_ProcessAttrSetStdioRedirect(pattr, PR_StandardError, writeStderr);
+
+ // set up argv with test name to run
+ char* const newArgv[3] = {
+ strdup(sPathToThisBinary),
+ strdup(aTestName),
+ 0
+ };
+
+ // make sure the child will abort if an assertion fails
+ NS_ASSERTION(PR_SUCCESS == PR_SetEnv(sAssertBehaviorEnv),
+ "couldn't set XPCOM_DEBUG_BREAK env var");
+
+ PRProcess* proc;
+ NS_ASSERTION(proc = PR_CreateProcess(sPathToThisBinary,
+ newArgv,
+ 0, // inherit environment
+ pattr),
+ "couldn't create process");
+ PR_Close(readStdin);
+ PR_Close(writeStdout);
+ PR_Close(writeStderr);
+
+ mProc = proc;
+ mStdinfd = writeStdin;
+ mStdoutfd = readStdout;
+ mStderrfd = readStderr;
+
+ free(newArgv[0]);
+ free(newArgv[1]);
+ PR_DestroyProcessAttr(pattr);
+ }
+
+ void RunToCompletion(uint32_t aWaitMs)
+ {
+ PR_Close(mStdinfd);
+
+ PRPollDesc pollfds[2];
+ int32_t nfds;
+ bool stdoutOpen = true, stderrOpen = true;
+ char buf[4096];
+
+ PRIntervalTime now = PR_IntervalNow();
+ PRIntervalTime deadline = now + PR_MillisecondsToInterval(aWaitMs);
+
+ while ((stdoutOpen || stderrOpen) && now < deadline) {
+ nfds = 0;
+ if (stdoutOpen) {
+ pollfds[nfds].fd = mStdoutfd;
+ pollfds[nfds].in_flags = PR_POLL_READ;
+ pollfds[nfds].out_flags = 0;
+ ++nfds;
+ }
+ if (stderrOpen) {
+ pollfds[nfds].fd = mStderrfd;
+ pollfds[nfds].in_flags = PR_POLL_READ;
+ pollfds[nfds].out_flags = 0;
+ ++nfds;
+ }
+
+ int32_t rv = PR_Poll(pollfds, nfds, deadline - now);
+ NS_ASSERTION(0 <= rv, PR_ErrorToName(PR_GetError()));
+
+ if (0 == rv) { // timeout
+ fputs("(timed out!)\n", stderr);
+ Finish(false); // abnormal
+ return;
+ }
+
+ for (int32_t i = 0; i < nfds; ++i) {
+ if (!pollfds[i].out_flags)
+ continue;
+
+ bool isStdout = mStdoutfd == pollfds[i].fd;
+ int32_t len = 0;
+
+ if (PR_POLL_READ & pollfds[i].out_flags) {
+ len = PR_Read(pollfds[i].fd, buf, sizeof(buf) - 1);
+ NS_ASSERTION(0 <= len, PR_ErrorToName(PR_GetError()));
+ }
+ else if (!(PR_POLL_HUP & pollfds[i].out_flags)) {
+ NS_ERROR(PR_ErrorToName(PR_GetError()));
+ }
+
+ if (0 < len) {
+ buf[len] = '\0';
+ if (isStdout)
+ mStdout += buf;
+ else
+ mStderr += buf;
+ }
+ else if (isStdout) {
+ stdoutOpen = false;
+ }
+ else {
+ stderrOpen = false;
+ }
+ }
+
+ now = PR_IntervalNow();
+ }
+
+ if (stdoutOpen)
+ fputs("(stdout still open!)\n", stderr);
+ if (stderrOpen)
+ fputs("(stderr still open!)\n", stderr);
+ if (now > deadline)
+ fputs("(timed out!)\n", stderr);
+
+ Finish(!stdoutOpen && !stderrOpen && now <= deadline);
+ }
+
+private:
+ void Finish(bool normalExit) {
+ if (!normalExit) {
+ PR_KillProcess(mProc);
+ mExitCode = -1;
+ int32_t dummy;
+ PR_WaitProcess(mProc, &dummy);
+ }
+ else {
+ PR_WaitProcess(mProc, &mExitCode); // this had better not block ...
+ }
+
+ PR_Close(mStdoutfd);
+ PR_Close(mStderrfd);
+ }
+
+ PRProcess* mProc;
+ PRFileDesc* mStdinfd; // writeable
+ PRFileDesc* mStdoutfd; // readable
+ PRFileDesc* mStderrfd; // readable
+};
+
+//-----------------------------------------------------------------------------
+// Harness for checking detector errors
+bool
+CheckForDeadlock(const char* test, const char* const* findTokens)
+{
+ Subprocess proc(test);
+ proc.RunToCompletion(5000);
+
+ if (0 == proc.mExitCode)
+ return false;
+
+ int32_t idx = 0;
+ for (const char* const* tp = findTokens; *tp; ++tp) {
+ const char* const token = *tp;
+#ifdef MOZILLA_INTERNAL_API
+ idx = proc.mStderr.Find(token, false, idx);
+#else
+ nsCString tokenCString(token);
+ idx = proc.mStderr.Find(tokenCString, idx);
+#endif
+ if (-1 == idx) {
+ printf("(missed token '%s' in output)\n", token);
+ puts("----------------------------------\n");
+ puts(proc.mStderr.get());
+ puts("----------------------------------\n");
+ return false;
+ }
+ idx += strlen(token);
+ }
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+// Single-threaded sanity tests
+
+// Stupidest possible deadlock.
+int
+Sanity_Child()
+{
+ TestMutex m1("dd.sanity.m1");
+ m1.Lock();
+ m1.Lock();
+ return 0; // not reached
+}
+
+nsresult
+Sanity()
+{
+ const char* const tokens[] = {
+ "###!!! ERROR: Potential deadlock detected",
+ "=== Cyclical dependency starts at\n--- Mutex : dd.sanity.m1",
+ "=== Cycle completed at\n--- Mutex : dd.sanity.m1",
+ "###!!! Deadlock may happen NOW!", // better catch these easy cases...
+ "###!!! ASSERTION: Potential deadlock detected",
+ 0
+ };
+ if (CheckForDeadlock("Sanity", tokens)) {
+ PASS();
+ } else {
+ FAIL("deadlock not detected");
+ }
+}
+
+// Slightly less stupid deadlock.
+int
+Sanity2_Child()
+{
+ TestMutex m1("dd.sanity2.m1");
+ TestMutex m2("dd.sanity2.m2");
+ m1.Lock();
+ m2.Lock();
+ m1.Lock();
+ return 0; // not reached
+}
+
+nsresult
+Sanity2()
+{
+ const char* const tokens[] = {
+ "###!!! ERROR: Potential deadlock detected",
+ "=== Cyclical dependency starts at\n--- Mutex : dd.sanity2.m1",
+ "--- Next dependency:\n--- Mutex : dd.sanity2.m2",
+ "=== Cycle completed at\n--- Mutex : dd.sanity2.m1",
+ "###!!! Deadlock may happen NOW!", // better catch these easy cases...
+ "###!!! ASSERTION: Potential deadlock detected",
+ 0
+ };
+ if (CheckForDeadlock("Sanity2", tokens)) {
+ PASS();
+ } else {
+ FAIL("deadlock not detected");
+ }
+}
+
+
+int
+Sanity3_Child()
+{
+ TestMutex m1("dd.sanity3.m1");
+ TestMutex m2("dd.sanity3.m2");
+ TestMutex m3("dd.sanity3.m3");
+ TestMutex m4("dd.sanity3.m4");
+
+ m1.Lock();
+ m2.Lock();
+ m3.Lock();
+ m4.Lock();
+ m4.Unlock();
+ m3.Unlock();
+ m2.Unlock();
+ m1.Unlock();
+
+ m4.Lock();
+ m1.Lock();
+ return 0;
+}
+
+nsresult
+Sanity3()
+{
+ const char* const tokens[] = {
+ "###!!! ERROR: Potential deadlock detected",
+ "=== Cyclical dependency starts at\n--- Mutex : dd.sanity3.m1",
+ "--- Next dependency:\n--- Mutex : dd.sanity3.m2",
+ "--- Next dependency:\n--- Mutex : dd.sanity3.m3",
+ "--- Next dependency:\n--- Mutex : dd.sanity3.m4",
+ "=== Cycle completed at\n--- Mutex : dd.sanity3.m1",
+ "###!!! ASSERTION: Potential deadlock detected",
+ 0
+ };
+ if (CheckForDeadlock("Sanity3", tokens)) {
+ PASS();
+ } else {
+ FAIL("deadlock not detected");
+ }
+}
+
+
+int
+Sanity4_Child()
+{
+ mozilla::ReentrantMonitor m1("dd.sanity4.m1");
+ TestMutex m2("dd.sanity4.m2");
+ m1.Enter();
+ m2.Lock();
+ m1.Enter();
+ return 0;
+}
+
+nsresult
+Sanity4()
+{
+ const char* const tokens[] = {
+ "Re-entering ReentrantMonitor after acquiring other resources",
+ "###!!! ERROR: Potential deadlock detected",
+ "=== Cyclical dependency starts at\n--- ReentrantMonitor : dd.sanity4.m1",
+ "--- Next dependency:\n--- Mutex : dd.sanity4.m2",
+ "=== Cycle completed at\n--- ReentrantMonitor : dd.sanity4.m1",
+ "###!!! ASSERTION: Potential deadlock detected",
+ 0
+ };
+ if (CheckForDeadlock("Sanity4", tokens)) {
+ PASS();
+ } else {
+ FAIL("deadlock not detected");
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Multithreaded tests
+
+TestMutex* ttM1;
+TestMutex* ttM2;
+
+static void
+TwoThreads_thread(void* arg)
+{
+ int32_t m1First = NS_PTR_TO_INT32(arg);
+ if (m1First) {
+ ttM1->Lock();
+ ttM2->Lock();
+ ttM2->Unlock();
+ ttM1->Unlock();
+ }
+ else {
+ ttM2->Lock();
+ ttM1->Lock();
+ ttM1->Unlock();
+ ttM2->Unlock();
+ }
+}
+
+int
+TwoThreads_Child()
+{
+ ttM1 = new TestMutex("dd.twothreads.m1");
+ ttM2 = new TestMutex("dd.twothreads.m2");
+ if (!ttM1 || !ttM2)
+ NS_RUNTIMEABORT("couldn't allocate mutexes");
+
+ PRThread* t1 = spawn(TwoThreads_thread, (void*) 0);
+ PR_JoinThread(t1);
+
+ PRThread* t2 = spawn(TwoThreads_thread, (void*) 1);
+ PR_JoinThread(t2);
+
+ return 0;
+}
+
+nsresult
+TwoThreads()
+{
+ const char* const tokens[] = {
+ "###!!! ERROR: Potential deadlock detected",
+ "=== Cyclical dependency starts at\n--- Mutex : dd.twothreads.m2",
+ "--- Next dependency:\n--- Mutex : dd.twothreads.m1",
+ "=== Cycle completed at\n--- Mutex : dd.twothreads.m2",
+ "###!!! ASSERTION: Potential deadlock detected",
+ 0
+ };
+
+ if (CheckForDeadlock("TwoThreads", tokens)) {
+ PASS();
+ } else {
+ FAIL("deadlock not detected");
+ }
+}
+
+
+TestMutex* cndMs[4];
+const uint32_t K = 100000;
+
+static void
+ContentionNoDeadlock_thread(void* arg)
+{
+ int32_t starti = NS_PTR_TO_INT32(arg);
+
+ for (uint32_t k = 0; k < K; ++k) {
+ for (int32_t i = starti; i < (int32_t) ArrayLength(cndMs); ++i)
+ cndMs[i]->Lock();
+ // comment out the next two lines for deadlocking fun!
+ for (int32_t i = ArrayLength(cndMs) - 1; i >= starti; --i)
+ cndMs[i]->Unlock();
+
+ starti = (starti + 1) % 3;
+ }
+}
+
+int
+ContentionNoDeadlock_Child()
+{
+ PRThread* threads[3];
+
+ for (uint32_t i = 0; i < ArrayLength(cndMs); ++i)
+ cndMs[i] = new TestMutex("dd.cnd.ms");
+
+ for (int32_t i = 0; i < (int32_t) ArrayLength(threads); ++i)
+ threads[i] = spawn(ContentionNoDeadlock_thread, NS_INT32_TO_PTR(i));
+
+ for (uint32_t i = 0; i < ArrayLength(threads); ++i)
+ PR_JoinThread(threads[i]);
+
+ for (uint32_t i = 0; i < ArrayLength(cndMs); ++i)
+ delete cndMs[i];
+
+ return 0;
+}
+
+nsresult
+ContentionNoDeadlock()
+{
+ const char * func = __func__;
+ Subprocess proc(func);
+ proc.RunToCompletion(60000);
+ if (0 != proc.mExitCode) {
+ printf("(expected 0 == return code, got %d)\n", proc.mExitCode);
+ puts("(output)\n----------------------------------\n");
+ puts(proc.mStdout.get());
+ puts("----------------------------------\n");
+ puts("(error output)\n----------------------------------\n");
+ puts(proc.mStderr.get());
+ puts("----------------------------------\n");
+
+ FAIL("deadlock");
+ }
+ PASS();
+}
+
+
+
+//-----------------------------------------------------------------------------
+
+int
+main(int argc, char** argv)
+{
+ if (1 < argc) {
+ // XXX can we run w/o scoped XPCOM?
+ const char* test = argv[1];
+ ScopedXPCOM xpcom(test);
+ if (xpcom.failed())
+ return 1;
+
+ // running in a spawned process. call the specificed child function.
+ if (!strcmp("Sanity", test))
+ return Sanity_Child();
+ if (!strcmp("Sanity2", test))
+ return Sanity2_Child();
+ if (!strcmp("Sanity3", test))
+ return Sanity3_Child();
+ if (!strcmp("Sanity4", test))
+ return Sanity4_Child();
+
+ if (!strcmp("TwoThreads", test))
+ return TwoThreads_Child();
+ if (!strcmp("ContentionNoDeadlock", test))
+ return ContentionNoDeadlock_Child();
+
+ fail("%s | %s - unknown child test", __FILE__, __FUNCTION__);
+ return 1;
+ }
+
+ ScopedXPCOM xpcom("Storage deadlock detector correctness (" __FILE__ ")");
+ if (xpcom.failed())
+ return 1;
+
+ // in the first invocation of this process. we will be the "driver".
+ int rv = 0;
+
+ sPathToThisBinary = argv[0];
+
+ if (NS_FAILED(Sanity()))
+ rv = 1;
+ if (NS_FAILED(Sanity2()))
+ rv = 1;
+ if (NS_FAILED(Sanity3()))
+ rv = 1;
+ if (NS_FAILED(Sanity4()))
+ rv = 1;
+
+ if (NS_FAILED(TwoThreads()))
+ rv = 1;
+ if (NS_FAILED(ContentionNoDeadlock()))
+ rv = 1;
+
+ return rv;
+}
diff --git a/storage/test/test_file_perms.cpp b/storage/test/test_file_perms.cpp
new file mode 100644
index 000000000..1235fe09b
--- /dev/null
+++ b/storage/test/test_file_perms.cpp
@@ -0,0 +1,44 @@
+/* -*- 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
+ */
+
+void
+test_file_perms()
+{
+ 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
+ // db/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
+}
+
+void (*gTests[])(void) = {
+ test_file_perms,
+};
+
+const char *file = __FILE__;
+#define TEST_NAME "file perms"
+#define TEST_FILE file
+#include "storage_test_harness_tail.h"
diff --git a/storage/test/test_mutex.cpp b/storage/test/test_mutex.cpp
new file mode 100644
index 000000000..8fd0eb3f3
--- /dev/null
+++ b/storage/test/test_mutex.cpp
@@ -0,0 +1,85 @@
+/* -*- 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.
+ */
+
+void
+test_AutoLock()
+{
+ int lockTypes[] = {
+ SQLITE_MUTEX_FAST,
+ SQLITE_MUTEX_RECURSIVE,
+ };
+ for (size_t i = 0; i < ArrayLength(lockTypes); i++) {
+ // 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(lockTypes[i]);
+ 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);
+ }
+}
+
+void
+test_AutoUnlock()
+{
+ int lockTypes[] = {
+ SQLITE_MUTEX_FAST,
+ SQLITE_MUTEX_RECURSIVE,
+ };
+ for (size_t i = 0; i < ArrayLength(lockTypes); i++) {
+ // 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(lockTypes[i]);
+ 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);
+ }
+}
+
+void (*gTests[])(void) = {
+ test_AutoLock,
+ test_AutoUnlock,
+};
+
+const char *file = __FILE__;
+#define TEST_NAME "SQLiteMutex"
+#define TEST_FILE file
+#include "storage_test_harness_tail.h"
diff --git a/storage/test/test_service_init_background_thread.cpp b/storage/test/test_service_init_background_thread.cpp
new file mode 100644
index 000000000..56e61a19b
--- /dev/null
+++ b/storage/test/test_service_init_background_thread.cpp
@@ -0,0 +1,58 @@
+/* -*- 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 "nsThreadUtils.h"
+
+/**
+ * This file tests that the storage service can be initialized off of the main
+ * thread without issue.
+ */
+
+////////////////////////////////////////////////////////////////////////////////
+//// Helpers
+
+class ServiceInitializer : public mozilla::Runnable
+{
+public:
+ NS_IMETHOD Run() override
+ {
+ // Use an explicit do_GetService instead of getService so that the check in
+ // getService doesn't blow up.
+ nsCOMPtr<mozIStorageService> service = do_GetService("@mozilla.org/storage/service;1");
+ do_check_false(service);
+ return NS_OK;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// Test Functions
+
+void
+test_service_initialization_on_background_thread()
+{
+ nsCOMPtr<nsIRunnable> event = new ServiceInitializer();
+ do_check_true(event);
+
+ nsCOMPtr<nsIThread> thread;
+ do_check_success(NS_NewThread(getter_AddRefs(thread)));
+
+ do_check_success(thread->Dispatch(event, NS_DISPATCH_NORMAL));
+
+ // Shutting down the thread will spin the event loop until all work in its
+ // event queue is completed. This will act as our thread synchronization.
+ do_check_success(thread->Shutdown());
+}
+
+void (*gTests[])(void) = {
+ test_service_initialization_on_background_thread,
+};
+
+const char *file = __FILE__;
+#define TEST_NAME "Background Thread Initialization"
+#define TEST_FILE file
+#include "storage_test_harness_tail.h"
diff --git a/storage/test/test_statement_scoper.cpp b/storage/test/test_statement_scoper.cpp
new file mode 100644
index 000000000..784d8d772
--- /dev/null
+++ b/storage/test/test_statement_scoper.cpp
@@ -0,0 +1,104 @@
+/* -*- 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.
+ */
+
+void
+test_automatic_reset()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // Need to create a table to populate sqlite_master with an entry.
+ (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE TABLE test (id INTEGER PRIMARY KEY)"
+ ));
+
+ nsCOMPtr<mozIStorageStatement> stmt;
+ (void)db->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT * FROM sqlite_master"
+ ), 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);
+}
+
+void
+test_Abandon()
+{
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+
+ // Need to create a table to populate sqlite_master with an entry.
+ (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE TABLE test (id INTEGER PRIMARY KEY)"
+ ));
+
+ nsCOMPtr<mozIStorageStatement> stmt;
+ (void)db->CreateStatement(NS_LITERAL_CSTRING(
+ "SELECT * FROM sqlite_master"
+ ), 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);
+}
+
+void (*gTests[])(void) = {
+ test_automatic_reset,
+ test_Abandon,
+};
+
+const char *file = __FILE__;
+#define TEST_NAME "statement scoper"
+#define TEST_FILE file
+#include "storage_test_harness_tail.h"
diff --git a/storage/test/test_transaction_helper.cpp b/storage/test/test_transaction_helper.cpp
new file mode 100644
index 000000000..4277c8f94
--- /dev/null
+++ b/storage/test/test_transaction_helper.cpp
@@ -0,0 +1,175 @@
+/* -*- 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 test our Transaction helper in mozStorageHelper.h.
+ */
+
+void
+test_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_true(has_transaction(db));
+ (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE TABLE test (id INTEGER PRIMARY KEY)"
+ ));
+ (void)transaction.Commit();
+ }
+ do_check_false(has_transaction(db));
+
+ bool exists = false;
+ (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists);
+ do_check_true(exists);
+}
+
+void
+test_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_true(has_transaction(db));
+ (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE TABLE test (id INTEGER PRIMARY KEY)"
+ ));
+ (void)transaction.Rollback();
+ }
+ do_check_false(has_transaction(db));
+
+ bool exists = true;
+ (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists);
+ do_check_false(exists);
+}
+
+void
+test_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_true(has_transaction(db));
+ (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE TABLE test (id INTEGER PRIMARY KEY)"
+ ));
+ }
+ do_check_false(has_transaction(db));
+
+ bool exists = false;
+ (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists);
+ do_check_true(exists);
+}
+
+void
+test_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_true(has_transaction(db));
+ (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE TABLE test (id INTEGER PRIMARY KEY)"
+ ));
+ }
+ do_check_false(has_transaction(db));
+
+ bool exists = true;
+ (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists);
+ do_check_false(exists);
+}
+
+void
+test_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_true(NS_SUCCEEDED(transaction.Commit()));
+ do_check_true(NS_SUCCEEDED(transaction.Rollback()));
+}
+
+void
+test_async_Commit()
+{
+ // note this will be active for any following test.
+ hook_sqlite_mutex();
+
+ 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_true(has_transaction(db));
+ (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE TABLE test (id INTEGER PRIMARY KEY)"
+ ));
+ (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(NS_LITERAL_CSTRING(
+ "SELECT NULL"
+ ), getter_AddRefs(stmt));
+ blocking_async_execute(stmt);
+ stmt->Finalize();
+ do_check_false(has_transaction(db));
+ bool exists = false;
+ (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists);
+ do_check_true(exists);
+
+ blocking_async_close(db);
+}
+
+void (*gTests[])(void) = {
+ test_Commit,
+ test_Rollback,
+ test_AutoCommit,
+ test_AutoRollback,
+ test_null_database_connection,
+ test_async_Commit,
+};
+
+const char *file = __FILE__;
+#define TEST_NAME "transaction helper"
+#define TEST_FILE file
+#include "storage_test_harness_tail.h"
diff --git a/storage/test/test_true_async.cpp b/storage/test/test_true_async.cpp
new file mode 100644
index 000000000..fea00392d
--- /dev/null
+++ b/storage/test/test_true_async.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"
+
+////////////////////////////////////////////////////////////////////////////////
+//// Tests
+
+void
+test_TrueAsyncStatement()
+{
+ // (only the first test needs to call this)
+ hook_sqlite_mutex();
+
+ 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(
+ NS_LITERAL_CSTRING("CREATE TABLE test (id INTEGER PRIMARY KEY)"),
+ 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(
+ NS_LITERAL_CSTRING("INSERT INTO test (id) VALUES (?)"),
+ 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(
+ NS_LITERAL_CSTRING("INSERT INTO test (id) VALUES (:id)"),
+ getter_AddRefs(stmt)
+ );
+ nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
+ stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
+ nsCOMPtr<mozIStorageBindingParams> params;
+ paramsArray->NewBindingParams(getter_AddRefs(params));
+ params->BindInt32ByName(NS_LITERAL_CSTRING("id"), 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(NS_LITERAL_CSTRING("SELECT * FROM test"),
+ 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.
+ */
+void
+test_AsyncCancellation()
+{
+ 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(
+ NS_LITERAL_CSTRING("CREATE TABLE asyncTable (id INTEGER PRIMARY KEY)"),
+ 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(
+ NS_LITERAL_CSTRING("CREATE TABLE syncTable (id INTEGER PRIMARY KEY)"),
+ 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(NS_LITERAL_CSTRING("asyncTable"), &exists);
+ do_check_true(rv == NS_OK);
+ do_check_false(exists);
+ rv = db->TableExists(NS_LITERAL_CSTRING("syncTable"), &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).
+ */
+void test_AsyncDestructorFinalizesOnAsyncThread()
+{
+ // test_TrueAsyncStatement called hook_sqlite_mutex() for us
+
+ nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase());
+ watch_for_mutex_use_on_this_thread();
+
+ // -- create an async statement
+ nsCOMPtr<mozIStorageAsyncStatement> stmt;
+ db->CreateAsyncStatement(
+ NS_LITERAL_CSTRING("CREATE TABLE test (id INTEGER PRIMARY KEY)"),
+ 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);
+}
+
+void (*gTests[])(void) = {
+ // this test must be first because it hooks the mutex mechanics
+ test_TrueAsyncStatement,
+ test_AsyncCancellation,
+ test_AsyncDestructorFinalizesOnAsyncThread
+};
+
+const char *file = __FILE__;
+#define TEST_NAME "true async statement"
+#define TEST_FILE file
+#include "storage_test_harness_tail.h"
diff --git a/storage/test/test_unlock_notify.cpp b/storage/test/test_unlock_notify.cpp
new file mode 100644
index 000000000..0a7ca67f9
--- /dev/null
+++ b/storage/test/test_unlock_notify.cpp
@@ -0,0 +1,266 @@
+/* -*- 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)
+ : monitor("DatabaseLocker::monitor")
+ , mSQL(aSQL)
+ , mState(STARTING)
+ {
+ }
+
+ 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());
+
+ 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;
+
+protected:
+ nsCOMPtr<nsIThread> mThread;
+ const char *const mSQL;
+ State mState;
+};
+
+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(NS_LITERAL_CSTRING(
+ "CREATE TABLE test (id INTEGER PRIMARY KEY, data STRING)"
+ ));
+ do_check_success(rv);
+ rv = db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "INSERT INTO test (data) VALUES ('foo')"
+ ));
+ do_check_success(rv);
+ rv = db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "INSERT INTO test (data) VALUES ('bar')"
+ ));
+ do_check_success(rv);
+ rv = db->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "CREATE UNIQUE INDEX unique_data ON test (data)"
+ ));
+ 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(NS_LITERAL_CSTRING(
+ "INSERT INTO test (data) VALUES ('test1')"
+ ), getter_AddRefs(stmt));
+ do_check_success(rv);
+
+ RefPtr<DatabaseLocker> locker(new DatabaseLocker("SELECT * FROM test"));
+ 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(NS_LITERAL_CSTRING(
+ "SELECT * FROM test"
+ ), 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(NS_LITERAL_CSTRING(
+ "SELECT * FROM test"
+ ), 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();
+}
+
+void (*gTests[])(void) = {
+ setup,
+ test_step_locked_does_not_block_main_thread,
+ test_drop_index_does_not_loop,
+ test_drop_table_does_not_loop,
+};
+
+const char *file = __FILE__;
+#define TEST_NAME "sqlite3_unlock_notify"
+#define TEST_FILE file
+#include "storage_test_harness_tail.h"
diff --git a/storage/test/unit/corruptDB.sqlite b/storage/test/unit/corruptDB.sqlite
new file mode 100644
index 000000000..b234246ca
--- /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 000000000..5f7498bfc
--- /dev/null
+++ b/storage/test/unit/fakeDB.sqlite
@@ -0,0 +1 @@
+BACON
diff --git a/storage/test/unit/head_storage.js b/storage/test/unit/head_storage.js
new file mode 100644
index 000000000..40374afad
--- /dev/null
+++ b/storage/test/unit/head_storage.js
@@ -0,0 +1,372 @@
+/* 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 Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+
+
+do_get_profile();
+var dirSvc = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties);
+
+var gDBConn = null;
+
+function getTestDB()
+{
+ var db = dirSvc.get("ProfD", Ci.nsIFile);
+ db.append("test_storage.sqlite");
+ 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 curThread = Components.classes["@mozilla.org/thread-manager;1"]
+ .getService().currentThread;
+ while (!closed)
+ curThread.processNextEvent(true);
+
+ // 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();
+}
+
+function getService()
+{
+ return Cc["@mozilla.org/storage/service;1"].getService(Ci.mozIStorageService);
+}
+
+/**
+ * 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()
+{
+ if (!gDBConn) {
+ gDBConn = getService().openDatabase(getTestDB());
+ }
+ 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 = getService().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 getService().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 {
+ do_check_true(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) {
+ do_check_eq(stmt.VALUE_TYPE_NULL, valType);
+ do_check_true(stmt.getIsNull(iCol));
+ }
+ else if (typeof expectedVal == "number") {
+ if (Math.floor(expectedVal) == expectedVal) {
+ do_check_eq(stmt.VALUE_TYPE_INTEGER, valType);
+ do_check_eq(expectedVal, stmt.getInt32(iCol));
+ }
+ else {
+ do_check_eq(stmt.VALUE_TYPE_FLOAT, valType);
+ do_check_eq(expectedVal, stmt.getDouble(iCol));
+ }
+ }
+ else if (typeof expectedVal == "string") {
+ do_check_eq(stmt.VALUE_TYPE_TEXT, valType);
+ do_check_eq(expectedVal, stmt.getUTF8String(iCol));
+ }
+ else { // blob
+ do_check_eq(stmt.VALUE_TYPE_BLOB, valType);
+ let count = { value: 0 }, blob = { value: null };
+ stmt.getBlob(iCol, count, blob);
+ do_check_eq(count.value, expectedVal.length);
+ for (let i = 0; i < count.value; i++) {
+ do_check_eq(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 {
+ do_check_true(countStmt.executeStep());
+ currentRows = countStmt.row.count;
+ }
+ finally {
+ countStmt.finalize();
+ }
+ return currentRows;
+}
+
+// Promise-Returning Functions
+
+function asyncClone(db, readOnly) {
+ let deferred = Promise.defer();
+ db.asyncClone(readOnly, function (status, db2) {
+ if (Components.isSuccessCode(status)) {
+ deferred.resolve(db2);
+ } else {
+ deferred.reject(status);
+ }
+ });
+ return deferred.promise;
+}
+
+function asyncClose(db) {
+ let deferred = Promise.defer();
+ db.asyncClose(function (status) {
+ if (Components.isSuccessCode(status)) {
+ deferred.resolve();
+ } else {
+ deferred.reject(status);
+ }
+ });
+ return deferred.promise;
+}
+
+function openAsyncDatabase(file, options) {
+ let deferred = Promise.defer();
+ let properties;
+ if (options) {
+ properties = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+ for (let k in options) {
+ properties.setProperty(k, options[k]);
+ }
+ }
+ getService().openAsyncDatabase(file, properties, function (status, db) {
+ if (Components.isSuccessCode(status)) {
+ deferred.resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection));
+ } else {
+ deferred.reject(status);
+ }
+ });
+ return deferred.promise;
+}
+
+function executeAsync(statement, onResult) {
+ let deferred = Promise.defer();
+ statement.executeAsync({
+ handleError: function (error) {
+ deferred.reject(error);
+ },
+ handleResult: function (result) {
+ if (onResult) {
+ onResult(result);
+ }
+ },
+ handleCompletion: function (result) {
+ deferred.resolve(result);
+ }
+ });
+ return deferred.promise;
+}
+
+function executeMultipleStatementsAsync(db, statements, onResult) {
+ let deferred = Promise.defer();
+ db.executeAsync(statements, statements.length, {
+ handleError: function (error) {
+ deferred.reject(error);
+ },
+ handleResult: function (result) {
+ if (onResult) {
+ onResult(result);
+ }
+ },
+ handleCompletion: function (result) {
+ deferred.resolve(result);
+ }
+ });
+ return deferred.promise;
+}
+
+function executeSimpleSQLAsync(db, query, onResult) {
+ let deferred = Promise.defer();
+ db.executeSimpleSQLAsync(query, {
+ handleError(error) {
+ deferred.reject(error);
+ },
+ handleResult(result) {
+ if (onResult) {
+ onResult(result);
+ } else {
+ do_throw("No results were expected");
+ }
+ },
+ handleCompletion(result) {
+ deferred.resolve(result);
+ }
+ });
+ return deferred.promise;
+}
+
+cleanup();
diff --git a/storage/test/unit/locale_collation.txt b/storage/test/unit/locale_collation.txt
new file mode 100644
index 000000000..86f50579b
--- /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 000000000..ebcdc6900
--- /dev/null
+++ b/storage/test/unit/test_bug-365166.js
@@ -0,0 +1,26 @@
+// 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 storageService = Components.classes["@mozilla.org/storage/service;1"].
+ getService(Components.interfaces.mozIStorageService);
+ var conn = storageService.openDatabase(file);
+ var statement = conn.createStatement(sql);
+ try {
+ // This shouldn't crash:
+ do_check_eq(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 000000000..f3c61f62b
--- /dev/null
+++ b/storage/test/unit/test_bug-393952.js
@@ -0,0 +1,38 @@
+/* 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 000000000..a9eafc1c2
--- /dev/null
+++ b/storage/test/unit/test_bug-429521.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/. */
+
+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.
+ var tzId = 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 000000000..eb315f934
--- /dev/null
+++ b/storage/test/unit/test_bug-444233.js
@@ -0,0 +1,51 @@
+/* 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");
+ do_check_true(stmt.executeStep());
+ do_check_eq(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 000000000..e0a5e8723
--- /dev/null
+++ b/storage/test/unit/test_cache_size.js
@@ -0,0 +1,72 @@
+/* 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");
+ do_check_true(stmt.executeStep());
+ do_check_eq(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");
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.cache_size, expectedCacheSize);
+ stmt.finalize();
+}
+
+function new_file(name)
+{
+ let file = dirSvc.get("ProfD", Ci.nsIFile);
+ file.append(name + ".sqlite");
+ do_check_false(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(getService().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 000000000..2cf91fe16
--- /dev/null
+++ b/storage/test/unit/test_chunk_growth.js
@@ -0,0 +1,52 @@
+// 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 = 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.
+ do_check_true(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");
+ do_check_true(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 000000000..7704dcc81
--- /dev/null
+++ b/storage/test/unit/test_connection_asyncClose.js
@@ -0,0 +1,125 @@
+/* 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(function* test_asyncClose_does_not_complete_before_statements() {
+ let db = getService().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)
+ yield asyncClose(db);
+ equal((yield 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.
+ */
+add_task(function* test_double_asyncClose_throws() {
+ let db = yield openAsyncDatabase(getTestDB());
+
+ // (Don't yield control flow yet, save the promise for after we make the
+ // second call.)
+ // Branch coverage: (asyncThread && mDBConn)
+ let realClosePromise = yield asyncClose(db);
+ try {
+ // Branch coverage: (!asyncThread && !mDBConn)
+ db.asyncClose();
+ ok(false, "should have thrown");
+ } catch (e) {
+ equal(e.result, Cr.NS_ERROR_NOT_INITIALIZED);
+ }
+
+ yield 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(function* test_asyncClose_on_sync_db() {
+ let db = getService().openDatabase(getTestDB());
+
+ // Branch coverage: (!asyncThread && mDBConn)
+ yield 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(function* test_asyncClose_failed_open() {
+ // This will fail and the promise will be rejected.
+ let openPromise = openAsyncDatabase(getFakeDB());
+ yield 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(function* test_asyncClose_does_not_throw_without_callback() {
+ let db = yield 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 000000000..e56d98e55
--- /dev/null
+++ b/storage/test/unit/test_connection_executeAsync.js
@@ -0,0 +1,171 @@
+/* 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(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 = yield 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 {
+ do_check_true(stmt.executeStep());
+ do_check_eq(TEXT, stmt.getString(0));
+ do_check_eq(REAL, stmt.getDouble(1));
+ do_check_true(stmt.getIsNull(2));
+ let count = { value: 0 };
+ let blob = { value: null };
+ stmt.getBlob(3, count, blob);
+ do_check_eq(BLOB.length, count.value);
+ for (let i = 0; i < BLOB.length; i++)
+ do_check_eq(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 {
+ do_check_true(stmt.executeStep());
+ do_check_eq(2, stmt.getInt32(0));
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ stmts[0].finalize();
+ stmts[1].finalize();
+});
+
+add_task(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, BLOB.length);
+ 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 {
+ do_check_true(countStmt.executeStep());
+ currentRows = countStmt.row.count;
+ }
+ finally {
+ countStmt.reset();
+ }
+
+ // Execute asynchronously.
+ let execResult = yield 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 {
+ do_check_true(countStmt.executeStep());
+ do_check_eq(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.
+ yield 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 000000000..142cc8e14
--- /dev/null
+++ b/storage/test/unit/test_connection_executeSimpleSQLAsync.js
@@ -0,0 +1,79 @@
+/* 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(function* test_create_and_add() {
+ let adb = yield openAsyncDatabase(getTestDB());
+
+ let completion = yield executeSimpleSQLAsync(adb,
+ "CREATE TABLE test (id INTEGER, string TEXT, number REAL)");
+
+ do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion);
+
+ completion = yield executeSimpleSQLAsync(adb,
+ "INSERT INTO test (id, string, number) " +
+ "VALUES (" + INTEGER + ", \"" + TEXT + "\", " + REAL + ")");
+
+ do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion);
+
+ let result = null;
+
+ completion = yield executeSimpleSQLAsync(adb,
+ "SELECT string, number FROM test WHERE id = 1",
+ function (aResultSet) {
+ result = aResultSet.getNextRow();
+ do_check_eq(2, result.numEntries);
+ do_check_eq(TEXT, result.getString(0));
+ do_check_eq(REAL, result.getDouble(1));
+ }
+ );
+
+ do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion);
+ do_check_neq(result, null);
+ result = null;
+
+ yield executeSimpleSQLAsync(adb, "SELECT COUNT(0) FROM test",
+ function (aResultSet) {
+ result = aResultSet.getNextRow();
+ do_check_eq(1, result.getInt32(0));
+ });
+
+ do_check_neq(result, null);
+
+ yield asyncClose(adb);
+});
+
+
+add_task(function* test_asyncClose_does_not_complete_before_statement() {
+ let adb = yield openAsyncDatabase(getTestDB());
+ let executed = false;
+
+ let reason = yield executeSimpleSQLAsync(adb, "SELECT * FROM test",
+ function (aResultSet) {
+ let result = aResultSet.getNextRow();
+
+ do_check_neq(result, null);
+ do_check_eq(3, result.numEntries);
+ do_check_eq(INTEGER, result.getInt32(0));
+ do_check_eq(TEXT, result.getString(1));
+ do_check_eq(REAL, result.getDouble(2));
+ executed = true;
+ }
+ );
+
+ do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, reason);
+
+ // Ensure that the statement executed to completion.
+ do_check_true(executed);
+
+ yield asyncClose(adb);
+});
diff --git a/storage/test/unit/test_js_helpers.js b/storage/test/unit/test_js_helpers.js
new file mode 100644
index 000000000..dde9fac20
--- /dev/null
+++ b/storage/test/unit/test_js_helpers.js
@@ -0,0 +1,125 @@
+/* -*- 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 = ["a", "b", "c"];
+ let index = 0;
+ for (let name in stmt.params) {
+ if (name == "QueryInterface")
+ continue;
+ do_check_eq(name, expected[index++]);
+ }
+}
+
+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;
+ do_check_eq(stmt.params.test, 2);
+ stmt.finalize();
+}
+
+function test_row_prototype()
+{
+ let stmt = createStatement(
+ "SELECT * FROM sqlite_master"
+ );
+
+ do_check_true(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;
+ do_check_eq(stmt.row.test, 2);
+
+ // Clean up after ourselves.
+ delete Object.getPrototypeOf(stmt.row).test;
+ stmt.finalize();
+}
+
+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_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 " +
+ ")"
+ );
+
+ // 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 000000000..ced141abd
--- /dev/null
+++ b/storage/test/unit/test_levenshtein.js
@@ -0,0 +1,74 @@
+/* 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 = getService().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");
+ do_check_true(stmt.executeStep());
+ let enc = stmt.getString(0);
+ stmt.finalize();
+
+ // The value returned will actually be UTF-16le or UTF-16be.
+ do_check_true(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 {
+ do_check_true(stmt.executeStep());
+ do_check_eq(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 000000000..b42f2eb27
--- /dev/null
+++ b/storage/test/unit/test_like.js
@@ -0,0 +1,202 @@
+/* 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;");
+ do_check_true(stmt.executeStep());
+ do_check_eq(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"];
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_false(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"];
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_false(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"];
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_false(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"];
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_false(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"];
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_false(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"];
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_false(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"];
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_false(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"];
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_false(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 000000000..414f6237c
--- /dev/null
+++ b/storage/test/unit/test_like_escape.js
@@ -0,0 +1,60 @@
+/* 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()
+{
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?1 ESCAPE '/'");
+ var paramForLike = stmt.escapeStringForLIKE("oo/bar_baz%20chees", '/');
+ // verify that we escaped / _ and %
+ do_check_eq(paramForLike, "oo//bar/_baz/%20chees");
+ // prepend and append with % for "contains"
+ stmt.bindByIndex(0, "%" + paramForLike + "%");
+ stmt.executeStep();
+ do_check_eq("foo/bar_baz%20cheese", stmt.getString(0));
+ stmt.finalize();
+}
+
+function test_escape_for_like_non_ascii()
+{
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?1 ESCAPE '/'");
+ var paramForLike = stmt.escapeStringForLIKE("oo%20" + LATIN1_AE + "/_ba", '/');
+ // verify that we escaped / _ and %
+ do_check_eq(paramForLike, "oo/%20" + LATIN1_AE + "///_ba");
+ // prepend and append with % for "contains"
+ stmt.bindByIndex(0, "%" + paramForLike + "%");
+ stmt.executeStep();
+ do_check_eq("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 000000000..12ba2b943
--- /dev/null
+++ b/storage/test/unit/test_locale_collation.js
@@ -0,0 +1,304 @@
+/* -*- 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 collation created from the application's locale. Used by localeCompare().
+var gLocaleCollation;
+
+// 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 = getService().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");
+ do_check_true(stmt.executeStep());
+ let enc = stmt.getString(0);
+ stmt.finalize();
+
+ // The value returned will actually be UTF-16le or UTF-16be.
+ do_check_true(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);
+
+ do_check_eq(aActual.length, aExpected.length);
+ for (let i = 0; i < aActual.length; i++)
+ do_check_eq(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)
+{
+ var strength;
+
+ switch (aCollation) {
+ case "locale":
+ strength = Ci.nsICollation.kCollationCaseInSensitive;
+ break;
+ case "locale_case_sensitive":
+ strength = Ci.nsICollation.kCollationAccentInsenstive;
+ break;
+ case "locale_accent_sensitive":
+ strength = Ci.nsICollation.kCollationCaseInsensitiveAscii;
+ break;
+ case "locale_case_accent_sensitive":
+ strength = Ci.nsICollation.kCollationCaseSensitive;
+ break;
+ default:
+ do_throw("Error in test: unknown collation '" + aCollation + "'");
+ break;
+ }
+ return function (aStr1, aStr2) {
+ return gLocaleCollation.compareString(strength, 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(Components.interfaces.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);
+
+ let localeSvc = Cc["@mozilla.org/intl/nslocaleservice;1"].
+ getService(Ci.nsILocaleService);
+ let collFact = Cc["@mozilla.org/intl/collation-factory;1"].
+ createInstance(Ci.nsICollationFactory);
+ gLocaleCollation = collFact.CreateCollation(localeSvc.getApplicationLocale());
+}
+
+// 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();
+ });
+}
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 000000000..a2548d1e6
--- /dev/null
+++ b/storage/test/unit/test_page_size_is_32k.js
@@ -0,0 +1,35 @@
+/* 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();
+ do_check_eq(stmt.getInt32(0), kExpectedPageSize);
+ stmt.finalize();
+ stmt = db.createStatement("PRAGMA cache_size");
+ stmt.executeStep();
+ do_check_eq(stmt.getInt32(0), kExpectedCacheSize);
+ stmt.finalize();
+}
+
+function new_file(name)
+{
+ var file = dirSvc.get("ProfD", Ci.nsIFile);
+ file.append(name + ".sqlite");
+ do_check_false(file.exists());
+ return file;
+}
+
+function run_test()
+{
+ check_size(getDatabase(new_file("shared32k")));
+ check_size(getService().openUnsharedDatabase(new_file("unshared32k")));
+}
+
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 000000000..1eff34f70
--- /dev/null
+++ b/storage/test/unit/test_sqlite_secure_delete.js
@@ -0,0 +1,80 @@
+/* -*- 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 = getService().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);
+ do_check_neq(-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);
+ do_check_eq(-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 000000000..edcecb999
--- /dev/null
+++ b/storage/test/unit/test_statement_executeAsync.js
@@ -0,0 +1,998 @@
+/* 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.
+ */
+
+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 != false)
+ 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 == true && errorCodeSeen == false)
+ 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();
+
+ let curThread = Components.classes["@mozilla.org/thread-manager;1"]
+ .getService().currentThread;
+ while (!completed && !_quit)
+ curThread.processNextEvent(true);
+
+ 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()
+{
+ // gibberish
+ let stmt = makeTestStatement("I AM A ROBOT. DO AS I SAY.");
+ execAsync(stmt, {error: Ci.mozIStorageError.ERROR});
+ stmt.finalize();
+
+ // legal SQL syntax, but with semantics issues.
+ stmt = makeTestStatement("SELECT destination FROM funkytown");
+ execAsync(stmt, {error: Ci.mozIStorageError.ERROR});
+ stmt.finalize();
+
+ run_next_test();
+}
+test_illegal_sql_async_deferred.asyncOnly = true;
+
+function test_create_table()
+{
+ // Ensure our table doesn't exist
+ do_check_false(getOpenedDatabase().tableExists("test"));
+
+ var stmt = makeTestStatement(
+ "CREATE TABLE test (" +
+ "id INTEGER, " +
+ "string TEXT, " +
+ "number REAL, " +
+ "nuller NULL, " +
+ "blober BLOB" +
+ ")"
+ );
+ execAsync(stmt);
+ stmt.finalize();
+
+ // Check that the table has been created
+ do_check_true(getOpenedDatabase().tableExists("test"));
+
+ // 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();
+ run_next_test();
+}
+
+function test_add_data()
+{
+ 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();
+
+ // Check that the result is in the table
+ verifyQuery("SELECT string, number, nuller, blober FROM test WHERE id = ?",
+ INTEGER,
+ [TEXT, REAL, null, BLOB]);
+ run_next_test();
+}
+
+function test_get_data()
+{
+ var stmt = makeTestStatement(
+ "SELECT string, number, nuller, blober, id FROM test WHERE id = ?"
+ );
+ stmt.bindByIndex(0, INTEGER);
+ execAsync(stmt, {}, [
+ function (tuple) {
+ do_check_neq(null, tuple);
+
+ // Check that it's what we expect
+ do_check_false(tuple.getIsNull(0));
+ do_check_eq(tuple.getResultByName("string"), tuple.getResultByIndex(0));
+ do_check_eq(TEXT, tuple.getResultByName("string"));
+ do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_TEXT,
+ tuple.getTypeOfIndex(0));
+
+ do_check_false(tuple.getIsNull(1));
+ do_check_eq(tuple.getResultByName("number"), tuple.getResultByIndex(1));
+ do_check_eq(REAL, tuple.getResultByName("number"));
+ do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT,
+ tuple.getTypeOfIndex(1));
+
+ do_check_true(tuple.getIsNull(2));
+ do_check_eq(tuple.getResultByName("nuller"), tuple.getResultByIndex(2));
+ do_check_eq(null, tuple.getResultByName("nuller"));
+ do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_NULL,
+ tuple.getTypeOfIndex(2));
+
+ do_check_false(tuple.getIsNull(3));
+ var blobByName = tuple.getResultByName("blober");
+ do_check_eq(BLOB.length, blobByName.length);
+ var blobByIndex = tuple.getResultByIndex(3);
+ do_check_eq(BLOB.length, blobByIndex.length);
+ for (let i = 0; i < BLOB.length; i++) {
+ do_check_eq(BLOB[i], blobByName[i]);
+ do_check_eq(BLOB[i], blobByIndex[i]);
+ }
+ var count = { value: 0 };
+ var blob = { value: null };
+ tuple.getBlob(3, count, blob);
+ do_check_eq(BLOB.length, count.value);
+ for (let i = 0; i < BLOB.length; i++)
+ do_check_eq(BLOB[i], blob.value[i]);
+ do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_BLOB,
+ tuple.getTypeOfIndex(3));
+
+ do_check_false(tuple.getIsNull(4));
+ do_check_eq(tuple.getResultByName("id"), tuple.getResultByIndex(4));
+ do_check_eq(INTEGER, tuple.getResultByName("id"));
+ do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER,
+ tuple.getTypeOfIndex(4));
+ }]);
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_tuple_out_of_bounds()
+{
+ var stmt = makeTestStatement(
+ "SELECT string FROM test"
+ );
+ execAsync(stmt, {}, [
+ function (tuple) {
+ do_check_neq(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) {
+ do_check_eq(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) {
+ do_check_eq(Cr.NS_ERROR_ILLEGAL_VALUE, e.result);
+ }
+ }]);
+ stmt.finalize();
+ 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()
+{
+ var stmt = makeTestStatement(
+ "SELECT 1"
+ );
+ execAsync(stmt, null, 1);
+ execAsync(stmt, null, 1);
+ stmt.finalize();
+ 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, BLOB.length);
+ 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);
+ do_check_eq(array.length, i + 1);
+ }
+ stmt.bindParameters(array);
+
+ let rowCount = getTableRowCount("test");
+ execAsync(stmt);
+ do_check_eq(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, BLOB.length);
+ array.addParams(bp);
+ do_check_eq(array.length, i + 1);
+ }
+ stmt.bindParameters(array);
+
+ let rowCount = getTableRowCount("test");
+ execAsync(stmt);
+ do_check_eq(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, BLOB.length));
+
+ 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();
+ let array2 = 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.
+ do_check_true(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.
+ do_execute_soon(_run_next_test);
+ }
+}
+
+function run_test()
+{
+ 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 000000000..58b27dd2d
--- /dev/null
+++ b/storage/test/unit/test_statement_wrapper_automatically.js
@@ -0,0 +1,167 @@
+/* -*- 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;
+ do_check_eq(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");
+ do_check_true(stmt.step());
+ do_check_eq(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");
+ do_check_true(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");
+ do_check_true(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_aggregates.js b/storage/test/unit/test_storage_aggregates.js
new file mode 100644
index 000000000..400aba836
--- /dev/null
+++ b/storage/test/unit/test_storage_aggregates.js
@@ -0,0 +1,116 @@
+/* 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 aggregate 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 testSquareAndSumFunction = {
+ calls: 0,
+ _sas: 0,
+
+ reset() {
+ this.calls = 0;
+ this._sas = 0;
+ },
+
+ onStep(val) {
+ ++this.calls;
+ this._sas += val.getInt32(0) * val.getInt32(0);
+ },
+
+ onFinal() {
+ var retval = this._sas;
+ this._sas = 0; // Prepare for next group
+ return retval;
+ }
+};
+
+function test_aggregate_registration()
+{
+ var msc = getOpenedDatabase();
+ msc.createAggregateFunction("test_sas_aggr", 1, testSquareAndSumFunction);
+}
+
+function test_aggregate_no_double_registration()
+{
+ var msc = getOpenedDatabase();
+ try {
+ msc.createAggregateFunction("test_sas_aggr", 2, testSquareAndSumFunction);
+ do_throw("We shouldn't get here!");
+ } catch (e) {
+ do_check_eq(Cr.NS_ERROR_FAILURE, e.result);
+ }
+}
+
+function test_aggregate_removal()
+{
+ var msc = getOpenedDatabase();
+ msc.removeFunction("test_sas_aggr");
+ // Should be Ok now
+ msc.createAggregateFunction("test_sas_aggr", 1, testSquareAndSumFunction);
+}
+
+function test_aggregate_no_aliases()
+{
+ var msc = getOpenedDatabase();
+ try {
+ msc.createAggregateFunction("test_sas_aggr2", 1, testSquareAndSumFunction);
+ do_throw("We shouldn't get here!");
+ } catch (e) {
+ do_check_eq(Cr.NS_ERROR_FAILURE, e.result);
+ }
+}
+
+function test_aggregate_call()
+{
+ var stmt = createStatement("SELECT test_sas_aggr(id) FROM function_tests");
+ while (stmt.executeStep()) {
+ // Do nothing.
+ }
+ do_check_eq(testNums.length, testSquareAndSumFunction.calls);
+ testSquareAndSumFunction.reset();
+ stmt.finalize();
+}
+
+function test_aggregate_result()
+{
+ var sas = 0;
+ for (var i = 0; i < testNums.length; ++i) {
+ sas += testNums[i] * testNums[i];
+ }
+ var stmt = createStatement("SELECT test_sas_aggr(id) FROM function_tests");
+ stmt.executeStep();
+ do_check_eq(sas, stmt.getInt32(0));
+ testSquareAndSumFunction.reset();
+ stmt.finalize();
+}
+
+var tests = [test_aggregate_registration, test_aggregate_no_double_registration,
+ test_aggregate_removal, test_aggregate_no_aliases, test_aggregate_call,
+ test_aggregate_result];
+
+function run_test()
+{
+ setup();
+
+ for (var i = 0; i < tests.length; i++) {
+ tests[i]();
+ }
+
+ cleanup();
+}
diff --git a/storage/test/unit/test_storage_connection.js b/storage/test/unit/test_storage_connection.js
new file mode 100644
index 000000000..ce98d0891
--- /dev/null
+++ b/storage/test/unit/test_storage_connection.js
@@ -0,0 +1,763 @@
+/* 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
+
+// Test Functions
+
+add_task(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();
+ do_check_true(msc.connectionReady);
+});
+
+add_task(function* test_connectionReady_closed() {
+ // This also tests mozIStorageConnection::Close()
+
+ var msc = getOpenedDatabase();
+ msc.close();
+ do_check_false(msc.connectionReady);
+ gDBConn = null; // this is so later tests don't start to fail.
+});
+
+add_task(function* test_databaseFile() {
+ var msc = getOpenedDatabase();
+ do_check_true(getTestDB().equals(msc.databaseFile));
+});
+
+add_task(function* test_tableExists_not_created() {
+ var msc = getOpenedDatabase();
+ do_check_false(msc.tableExists("foo"));
+});
+
+add_task(function* test_indexExists_not_created() {
+ var msc = getOpenedDatabase();
+ do_check_false(msc.indexExists("foo"));
+});
+
+add_task(function* test_temp_tableExists_and_indexExists() {
+ var msc = getOpenedDatabase();
+ msc.executeSimpleSQL("CREATE TEMP TABLE test_temp(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)");
+ do_check_true(msc.tableExists("test_temp"));
+
+ msc.executeSimpleSQL("CREATE INDEX test_temp_ind ON test_temp (name)");
+ do_check_true(msc.indexExists("test_temp_ind"));
+
+ msc.executeSimpleSQL("DROP INDEX test_temp_ind");
+ msc.executeSimpleSQL("DROP TABLE test_temp");
+});
+
+add_task(function* test_createTable_not_created() {
+ var msc = getOpenedDatabase();
+ msc.createTable("test", "id INTEGER PRIMARY KEY, name TEXT");
+ do_check_true(msc.tableExists("test"));
+});
+
+add_task(function* test_indexExists_created() {
+ var msc = getOpenedDatabase();
+ msc.executeSimpleSQL("CREATE INDEX name_ind ON test (name)");
+ do_check_true(msc.indexExists("name_ind"));
+});
+
+add_task(function* test_createTable_already_created() {
+ var msc = getOpenedDatabase();
+ do_check_true(msc.tableExists("test"));
+ Assert.throws(() => msc.createTable("test", "id INTEGER PRIMARY KEY, name TEXT"),
+ /NS_ERROR_FAILURE/);
+});
+
+add_task(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");
+
+ do_check_false(msc.tableExists("sample.test"));
+ msc.createTable("sample.test", "id INTEGER PRIMARY KEY, name TEXT");
+ do_check_true(msc.tableExists("sample.test"));
+ Assert.throws(() => msc.createTable("sample.test", "id INTEGER PRIMARY KEY, name TEXT"),
+ /NS_ERROR_FAILURE/);
+
+ do_check_false(msc.indexExists("sample.test_ind"));
+ msc.executeSimpleSQL("CREATE INDEX sample.test_ind ON test (name)");
+ do_check_true(msc.indexExists("sample.test_ind"));
+
+ msc.executeSimpleSQL("DETACH DATABASE sample");
+ msc2.close();
+ try {
+ file.remove(false);
+ } catch (e) {
+ // Do nothing.
+ }
+});
+
+add_task(function* test_lastInsertRowID() {
+ var msc = getOpenedDatabase();
+ msc.executeSimpleSQL("INSERT INTO test (name) VALUES ('foo')");
+ do_check_eq(1, msc.lastInsertRowID);
+});
+
+add_task(function* test_transactionInProgress_no() {
+ var msc = getOpenedDatabase();
+ do_check_false(msc.transactionInProgress);
+});
+
+add_task(function* test_transactionInProgress_yes() {
+ var msc = getOpenedDatabase();
+ msc.beginTransaction();
+ do_check_true(msc.transactionInProgress);
+ msc.commitTransaction();
+ do_check_false(msc.transactionInProgress);
+
+ msc.beginTransaction();
+ do_check_true(msc.transactionInProgress);
+ msc.rollbackTransaction();
+ do_check_false(msc.transactionInProgress);
+});
+
+add_task(function* test_commitTransaction_no_transaction() {
+ var msc = getOpenedDatabase();
+ do_check_false(msc.transactionInProgress);
+ Assert.throws(() => msc.commitTransaction(), /NS_ERROR_UNEXPECTED/);
+});
+
+add_task(function* test_rollbackTransaction_no_transaction() {
+ var msc = getOpenedDatabase();
+ do_check_false(msc.transactionInProgress);
+ Assert.throws(() => msc.rollbackTransaction(), /NS_ERROR_UNEXPECTED/);
+});
+
+add_task(function* test_get_schemaVersion_not_set() {
+ do_check_eq(0, getOpenedDatabase().schemaVersion);
+});
+
+add_task(function* test_set_schemaVersion() {
+ var msc = getOpenedDatabase();
+ const version = 1;
+ msc.schemaVersion = version;
+ do_check_eq(version, msc.schemaVersion);
+});
+
+add_task(function* test_set_schemaVersion_same() {
+ var msc = getOpenedDatabase();
+ const version = 1;
+ msc.schemaVersion = version; // should still work ok
+ do_check_eq(version, msc.schemaVersion);
+});
+
+add_task(function* test_set_schemaVersion_negative() {
+ var msc = getOpenedDatabase();
+ const version = -1;
+ msc.schemaVersion = version;
+ do_check_eq(version, msc.schemaVersion);
+});
+
+add_task(function* test_createTable() {
+ var temp = getTestDB().parent;
+ temp.append("test_db_table");
+ try {
+ var con = getService().openDatabase(temp);
+ con.createTable("a", "");
+ } catch (e) {
+ if (temp.exists()) {
+ try {
+ temp.remove(false);
+ } catch (e2) {
+ // Do nothing.
+ }
+ }
+ do_check_true(e.result == Cr.NS_ERROR_NOT_INITIALIZED ||
+ e.result == Cr.NS_ERROR_FAILURE);
+ } finally {
+ if (con) {
+ con.close();
+ }
+ }
+});
+
+add_task(function* test_defaultSynchronousAtNormal() {
+ var msc = getOpenedDatabase();
+ var stmt = createStatement("PRAGMA synchronous;");
+ try {
+ stmt.executeStep();
+ do_check_eq(1, stmt.getInt32(0));
+ }
+ finally {
+ stmt.reset();
+ stmt.finalize();
+ }
+});
+
+// must be ran before executeAsync tests
+add_task(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.
+ let thread = Cc["@mozilla.org/thread-manager;1"].
+ getService(Ci.nsIThreadManager).
+ currentThread;
+ thread.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL);
+
+ // Sanity check, then close the database. Afterwards, we should not have ran!
+ do_check_false(event.ran);
+ getOpenedDatabase().close();
+ do_check_false(event.ran);
+
+ // Reset gDBConn so that later tests will get a new connection object.
+ gDBConn = null;
+});
+
+add_task(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();
+
+ yield asyncClose(getOpenedDatabase());
+ // Reset gDBConn so that later tests will get a new connection object.
+ gDBConn = null;
+});
+
+add_task(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(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");
+ yield 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;
+});
+
+add_task(function* test_close_fails_with_async_statement_ran() {
+ let deferred = Promise.defer();
+ let stmt = createStatement("SELECT * FROM test");
+ stmt.executeAsync();
+ stmt.finalize();
+
+ let db = getOpenedDatabase();
+ Assert.throws(() => db.close(), /NS_ERROR_UNEXPECTED/);
+
+ // Clean up after ourselves.
+ db.asyncClose(function () {
+ // Reset gDBConn so that later tests will get a new connection object.
+ gDBConn = null;
+ deferred.resolve();
+ });
+
+ yield deferred.promise;
+});
+
+add_task(function* test_clone_optional_param() {
+ let db1 = getService().openUnsharedDatabase(getTestDB());
+ let db2 = db1.clone();
+ do_check_true(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");
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+
+ // Additionally check that it is a connection on the same database.
+ do_check_true(db1.databaseFile.equals(db2.databaseFile));
+
+ db1.close();
+ db2.close();
+});
+
+function* standardAsyncTest(promisedDB, name, shouldInit = false) {
+ do_print("Performing standard async test " + name);
+
+ let adb = yield promisedDB;
+ do_check_true(adb instanceof Ci.mozIStorageAsyncConnection);
+ do_check_false(adb instanceof Ci.mozIStorageConnection);
+
+ if (shouldInit) {
+ let stmt = adb.createAsyncStatement("CREATE TABLE test(name TEXT)");
+ yield 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 = yield executeAsync(stmt);
+ do_print("Request complete");
+ stmt.finalize();
+ do_check_true(Components.isSuccessCode(result));
+ do_print("Extracting data");
+ stmt = adb.createAsyncStatement("SELECT * FROM test");
+ let found = false;
+ yield executeAsync(stmt, function (results) {
+ do_print("Data has been extracted");
+ for (let row = results.getNextRow(); row != null; row = results.getNextRow()) {
+ if (row.getResultByName("name") == name) {
+ found = true;
+ break;
+ }
+ }
+ });
+ do_check_true(found);
+ stmt.finalize();
+ yield asyncClose(adb);
+
+ do_print("Standard async test " + name + " complete");
+}
+
+add_task(function* test_open_async() {
+ yield standardAsyncTest(openAsyncDatabase(getTestDB(), null), "default");
+ yield standardAsyncTest(openAsyncDatabase(getTestDB()), "no optional arg");
+ yield standardAsyncTest(openAsyncDatabase(getTestDB(),
+ {shared: false, growthIncrement: 54}), "non-default options");
+ yield standardAsyncTest(openAsyncDatabase("memory"),
+ "in-memory database", true);
+ yield standardAsyncTest(openAsyncDatabase("memory",
+ {shared: false}),
+ "in-memory database and options", true);
+
+ do_print("Testing async opening with bogus options 0");
+ let raised = false;
+ let adb = null;
+
+ try {
+ adb = yield openAsyncDatabase("memory", {shared: false, growthIncrement: 54});
+ } catch (ex) {
+ raised = true;
+ } finally {
+ if (adb) {
+ yield asyncClose(adb);
+ }
+ }
+ do_check_true(raised);
+
+ do_print("Testing async opening with bogus options 1");
+ raised = false;
+ adb = null;
+ try {
+ adb = yield openAsyncDatabase(getTestDB(), {shared: "forty-two"});
+ } catch (ex) {
+ raised = true;
+ } finally {
+ if (adb) {
+ yield asyncClose(adb);
+ }
+ }
+ do_check_true(raised);
+
+ do_print("Testing async opening with bogus options 2");
+ raised = false;
+ adb = null;
+ try {
+ adb = yield openAsyncDatabase(getTestDB(), {growthIncrement: "forty-two"});
+ } catch (ex) {
+ raised = true;
+ } finally {
+ if (adb) {
+ yield asyncClose(adb);
+ }
+ }
+ do_check_true(raised);
+});
+
+
+add_task(function* test_async_open_with_shared_cache() {
+ do_print("Testing that opening with a shared cache doesn't break stuff");
+ let adb = yield openAsyncDatabase(getTestDB(), {shared: true});
+
+ let stmt = adb.createAsyncStatement("INSERT INTO test (name) VALUES (:name)");
+ stmt.params.name = "clockworker";
+ let result = yield executeAsync(stmt);
+ do_print("Request complete");
+ stmt.finalize();
+ do_check_true(Components.isSuccessCode(result));
+ do_print("Extracting data");
+ stmt = adb.createAsyncStatement("SELECT * FROM test");
+ let found = false;
+ yield executeAsync(stmt, function (results) {
+ do_print("Data has been extracted");
+ for (let row = results.getNextRow(); row != null; row = results.getNextRow()) {
+ if (row.getResultByName("name") == "clockworker") {
+ found = true;
+ break;
+ }
+ }
+ });
+ do_check_true(found);
+ stmt.finalize();
+ yield asyncClose(adb);
+});
+
+add_task(function* test_clone_trivial_async() {
+ do_print("Open connection");
+ let db = getService().openDatabase(getTestDB());
+ do_check_true(db instanceof Ci.mozIStorageAsyncConnection);
+ do_print("AsyncClone connection");
+ let clone = yield asyncClone(db, true);
+ do_check_true(clone instanceof Ci.mozIStorageAsyncConnection);
+ do_print("Close connection");
+ yield asyncClose(db);
+ do_print("Close clone");
+ yield asyncClose(clone);
+});
+
+add_task(function* test_clone_no_optional_param_async() {
+ "use strict";
+ do_print("Testing async cloning");
+ let adb1 = yield openAsyncDatabase(getTestDB(), null);
+ do_check_true(adb1 instanceof Ci.mozIStorageAsyncConnection);
+
+ do_print("Cloning database");
+
+ let adb2 = yield asyncClone(adb1);
+ do_print("Testing that the cloned db is a mozIStorageAsyncConnection " +
+ "and not a mozIStorageConnection");
+ do_check_true(adb2 instanceof Ci.mozIStorageAsyncConnection);
+ do_check_false(adb2 instanceof Ci.mozIStorageConnection);
+
+ do_print("Inserting data into source db");
+ let stmt = adb1.
+ createAsyncStatement("INSERT INTO test (name) VALUES (:name)");
+
+ stmt.params.name = "yoric";
+ let result = yield executeAsync(stmt);
+ do_print("Request complete");
+ stmt.finalize();
+ do_check_true(Components.isSuccessCode(result));
+ do_print("Extracting data from clone db");
+ stmt = adb2.createAsyncStatement("SELECT * FROM test");
+ let found = false;
+ yield executeAsync(stmt, function (results) {
+ do_print("Data has been extracted");
+ for (let row = results.getNextRow(); row != null; row = results.getNextRow()) {
+ if (row.getResultByName("name") == "yoric") {
+ found = true;
+ break;
+ }
+ }
+ });
+ do_check_true(found);
+ stmt.finalize();
+ do_print("Closing databases");
+ yield asyncClose(adb2);
+ do_print("First db closed");
+
+ yield asyncClose(adb1);
+ do_print("Second db closed");
+});
+
+add_task(function* test_clone_readonly() {
+ let db1 = getService().openUnsharedDatabase(getTestDB());
+ let db2 = db1.clone(true);
+ do_check_true(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");
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+
+ db1.close();
+ db2.close();
+});
+
+add_task(function* test_clone_shared_readonly() {
+ let db1 = getService().openDatabase(getTestDB());
+ let db2 = db1.clone(true);
+ do_check_true(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");
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+
+ db1.close();
+ db2.close();
+});
+
+add_task(function* test_close_clone_fails() {
+ let calls = [
+ "openDatabase",
+ "openUnsharedDatabase",
+ ];
+ calls.forEach(function (methodName) {
+ let db = getService()[methodName](getTestDB());
+ db.close();
+ expectError(Cr.NS_ERROR_NOT_INITIALIZED, () => db.clone());
+ });
+});
+
+add_task(function* test_memory_clone_fails() {
+ let db = getService().openSpecialDatabase("memory");
+ db.close();
+ expectError(Cr.NS_ERROR_NOT_INITIALIZED, () => db.clone());
+});
+
+add_task(function* test_clone_copies_functions() {
+ const FUNC_NAME = "test_func";
+ let calls = [
+ "openDatabase",
+ "openUnsharedDatabase",
+ ];
+ let functionMethods = [
+ "createFunction",
+ "createAggregateFunction",
+ ];
+ calls.forEach(function (methodName) {
+ [true, false].forEach(function (readOnly) {
+ functionMethods.forEach(function (functionMethod) {
+ let db1 = getService()[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(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",
+ "createAggregateFunction",
+ ];
+ calls.forEach(function (methodName) {
+ [true, false].forEach(function (readOnly) {
+ functionMethods.forEach(function (functionMethod) {
+ let db1 = getService()[methodName](getTestDB());
+ // Create a function for db1.
+ let func = new test_func();
+ db1[functionMethod](FUNC_NAME, 1, func);
+ do_check_false(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();
+ do_check_true(func.called);
+ stmt.finalize();
+ db1.close();
+ db2.close();
+ });
+ });
+ });
+});
+
+add_task(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 = getService().openUnsharedDatabase(getTestDB());
+
+ // Sanity check initial values are different from enforced ones.
+ PRAGMAS.forEach(function (pragma) {
+ let stmt = db1.createStatement("PRAGMA " + pragma.name);
+ do_check_true(stmt.executeStep());
+ do_check_neq(pragma.value, stmt.getInt32(0));
+ stmt.finalize();
+ });
+ // Execute pragmas.
+ PRAGMAS.forEach(function (pragma) {
+ db1.executeSimpleSQL("PRAGMA " + pragma.name + " = " + pragma.value);
+ });
+
+ let db2 = db1.clone();
+ do_check_true(db2.connectionReady);
+
+ // Check cloned connection inherited pragma values.
+ PRAGMAS.forEach(function (pragma) {
+ let stmt = db2.createStatement("PRAGMA " + pragma.name);
+ do_check_true(stmt.executeStep());
+ let validate = pragma.copied ? do_check_eq : do_check_neq;
+ validate(pragma.value, stmt.getInt32(0));
+ stmt.finalize();
+ });
+
+ db1.close();
+ db2.close();
+});
+
+add_task(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 = getService().openUnsharedDatabase(getTestDB());
+
+ // Sanity check initial values are different from enforced ones.
+ PRAGMAS.forEach(function (pragma) {
+ let stmt = db1.createStatement("PRAGMA " + pragma.name);
+ do_check_true(stmt.executeStep());
+ do_check_neq(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);
+ do_check_true(db2.connectionReady);
+
+ // Check cloned connection inherited pragma values.
+ PRAGMAS.forEach(function (pragma) {
+ let stmt = db2.createStatement("PRAGMA " + pragma.name);
+ do_check_true(stmt.executeStep());
+ let validate = pragma.copied ? do_check_eq : do_check_neq;
+ validate(pragma.value, stmt.getInt32(0));
+ stmt.finalize();
+ });
+
+ db1.close();
+ db2.close();
+});
+
+add_task(function* test_clone_attach_database() {
+ let db1 = getService().openUnsharedDatabase(getTestDB());
+
+ let c = 0;
+ function attachDB(conn, name) {
+ let file = dirSvc.get("ProfD", Ci.nsIFile);
+ file.append("test_storage_" + (++c) + ".sqlite");
+ let db = getService().openUnsharedDatabase(file);
+ conn.executeSimpleSQL(`ATTACH DATABASE '${db.databaseFile.path}' AS ${name}`);
+ db.close();
+ }
+ attachDB(db1, "attached_1");
+ attachDB(db1, "attached_2");
+
+ // These should not throw.
+ db1.createStatement("SELECT * FROM attached_1.sqlite_master");
+ db1.createStatement("SELECT * FROM attached_2.sqlite_master");
+
+ // R/W clone.
+ let db2 = db1.clone();
+ do_check_true(db2.connectionReady);
+
+ // These should not throw.
+ db2.createStatement("SELECT * FROM attached_1.sqlite_master");
+ db2.createStatement("SELECT * FROM attached_2.sqlite_master");
+
+ // R/O clone.
+ let db3 = db1.clone(true);
+ do_check_true(db3.connectionReady);
+
+ // These should not throw.
+ db3.createStatement("SELECT * FROM attached_1.sqlite_master");
+ db3.createStatement("SELECT * FROM attached_2.sqlite_master");
+
+ db1.close();
+ db2.close();
+ db3.close();
+});
+
+add_task(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.
+ do_check_true(target != null);
+
+ yield asyncClose(db);
+ gDBConn = null;
+});
diff --git a/storage/test/unit/test_storage_fulltextindex.js b/storage/test/unit/test_storage_fulltextindex.js
new file mode 100644
index 000000000..1f7614067
--- /dev/null
+++ b/storage/test/unit/test_storage_fulltextindex.js
@@ -0,0 +1,86 @@
+/* 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)");
+
+ do_check_true(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();
+
+ do_check_eq(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'");
+
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.getInt32(0), 3);
+ do_check_eq(stmt.getString(1), "broccoli pie");
+ do_check_eq(stmt.getString(2), "broccoli cheese onions flour");
+
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.getInt32(0), 4);
+ do_check_eq(stmt.getString(1), "pumpkin pie");
+ do_check_eq(stmt.getString(2), "pumpkin sugar flour butter");
+
+ do_check_false(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]();
+ }
+ }
+ // 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.
+ finally {
+ cleanup();
+ }
+}
diff --git a/storage/test/unit/test_storage_function.js b/storage/test/unit/test_storage_function.js
new file mode 100644
index 000000000..6532709ec
--- /dev/null
+++ b/storage/test/unit/test_storage_function.js
@@ -0,0 +1,95 @@
+/* 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) {
+ do_check_eq(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.
+ }
+ do_check_eq(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();
+ do_check_eq(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 000000000..c06a57e83
--- /dev/null
+++ b/storage/test/unit/test_storage_progresshandler.js
@@ -0,0 +1,111 @@
+/* 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);
+ do_check_true(oldH instanceof Ci.mozIStorageProgressHandler);
+}
+
+function test_handler_removal()
+{
+ var msc = getOpenedDatabase();
+ msc.removeProgressHandler();
+ var oldH = msc.removeProgressHandler();
+ do_check_eq(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.
+ }
+ do_check_true(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) {
+ do_check_eq(Cr.NS_ERROR_ABORT, e.result);
+ do_check_eq(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
+ do_check_eq(Cr.NS_ERROR_ABORT, e.result);
+ do_check_eq(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 000000000..9cf46620e
--- /dev/null
+++ b/storage/test/unit/test_storage_service.js
@@ -0,0 +1,142 @@
+/* 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
+// openUnsharedDatabase, which is tested by test_storage_service_unshared.js.
+
+const BACKUP_FILE_NAME = "test_storage.sqlite.backup";
+
+function test_openSpecialDatabase_invalid_arg()
+{
+ try {
+ getService().openSpecialDatabase("abcd");
+ do_throw("We should not get here!");
+ } catch (e) {
+ print(e);
+ print("e.result is " + e.result);
+ do_check_eq(Cr.NS_ERROR_INVALID_ARG, e.result);
+ }
+}
+
+function test_openDatabase_null_file()
+{
+ try {
+ getService().openDatabase(null);
+ do_throw("We should not get here!");
+ } catch (e) {
+ print(e);
+ print("e.result is " + e.result);
+ do_check_eq(Cr.NS_ERROR_INVALID_ARG, e.result);
+ }
+}
+
+function test_openUnsharedDatabase_null_file()
+{
+ try {
+ getService().openUnsharedDatabase(null);
+ do_throw("We should not get here!");
+ } catch (e) {
+ print(e);
+ print("e.result is " + e.result);
+ do_check_eq(Cr.NS_ERROR_INVALID_ARG, e.result);
+ }
+}
+
+function test_openDatabase_file_DNE()
+{
+ // the file should be created after calling
+ var db = getTestDB();
+ do_check_false(db.exists());
+ getService().openDatabase(db);
+ do_check_true(db.exists());
+}
+
+function test_openDatabase_file_exists()
+{
+ // it should already exist from our last test
+ var db = getTestDB();
+ do_check_true(db.exists());
+ getService().openDatabase(db);
+ do_check_true(db.exists());
+}
+
+function test_corrupt_db_throws_with_openDatabase()
+{
+ try {
+ getDatabase(getCorruptDB());
+ do_throw("should not be here");
+ }
+ catch (e) {
+ do_check_eq(Cr.NS_ERROR_FILE_CORRUPTED, e.result);
+ }
+}
+
+function test_fake_db_throws_with_openDatabase()
+{
+ try {
+ getDatabase(getFakeDB());
+ do_throw("should not be here");
+ }
+ catch (e) {
+ do_check_eq(Cr.NS_ERROR_FILE_CORRUPTED, e.result);
+ }
+}
+
+function test_backup_not_new_filename()
+{
+ const fname = getTestDB().leafName;
+
+ var backup = getService().backupDatabaseFile(getTestDB(), fname);
+ do_check_neq(fname, backup.leafName);
+
+ backup.remove(false);
+}
+
+function test_backup_new_filename()
+{
+ var backup = getService().backupDatabaseFile(getTestDB(), BACKUP_FILE_NAME);
+ do_check_eq(BACKUP_FILE_NAME, backup.leafName);
+
+ backup.remove(false);
+}
+
+function test_backup_new_folder()
+{
+ var parentDir = getTestDB().parent;
+ parentDir.append("test_storage_temp");
+ if (parentDir.exists())
+ parentDir.remove(true);
+ parentDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ do_check_true(parentDir.exists());
+
+ var backup = getService().backupDatabaseFile(getTestDB(), BACKUP_FILE_NAME,
+ parentDir);
+ do_check_eq(BACKUP_FILE_NAME, backup.leafName);
+ do_check_true(parentDir.equals(backup.parent));
+
+ parentDir.remove(true);
+}
+
+var tests = [
+ test_openSpecialDatabase_invalid_arg,
+ test_openDatabase_null_file,
+ test_openUnsharedDatabase_null_file,
+ test_openDatabase_file_DNE,
+ test_openDatabase_file_exists,
+ test_corrupt_db_throws_with_openDatabase,
+ test_fake_db_throws_with_openDatabase,
+ test_backup_not_new_filename,
+ test_backup_new_filename,
+ test_backup_new_folder,
+];
+
+function run_test()
+{
+ for (var i = 0; i < tests.length; i++) {
+ tests[i]();
+ }
+
+ cleanup();
+}
+
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 000000000..70efb2a43
--- /dev/null
+++ b/storage/test/unit/test_storage_service_unshared.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 the openUnsharedDatabase function of mozIStorageService.
+
+function test_openUnsharedDatabase_file_DNE()
+{
+ // the file should be created after calling
+ var db = getTestDB();
+ do_check_false(db.exists());
+ getService().openUnsharedDatabase(db);
+ do_check_true(db.exists());
+}
+
+function test_openUnsharedDatabase_file_exists()
+{
+ // it should already exist from our last test
+ var db = getTestDB();
+ do_check_true(db.exists());
+ getService().openUnsharedDatabase(db);
+ do_check_true(db.exists());
+}
+
+var tests = [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 000000000..026e271ac
--- /dev/null
+++ b/storage/test/unit/test_storage_statement.js
@@ -0,0 +1,184 @@
+/* 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");
+ do_check_eq(0, stmt.parameterCount);
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_parameterCount_one()
+{
+ var stmt = createStatement("SELECT * FROM test WHERE id = ?1");
+ do_check_eq(1, stmt.parameterCount);
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_getParameterName()
+{
+ var stmt = createStatement("SELECT * FROM test WHERE id = :id");
+ do_check_eq(":id", stmt.getParameterName(0));
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_getParameterIndex_different()
+{
+ var stmt = createStatement("SELECT * FROM test WHERE id = :id OR name = :name");
+ do_check_eq(0, stmt.getParameterIndex("id"));
+ do_check_eq(1, stmt.getParameterIndex("name"));
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_getParameterIndex_same()
+{
+ var stmt = createStatement("SELECT * FROM test WHERE id = :test OR name = :test");
+ do_check_eq(0, stmt.getParameterIndex("test"));
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_columnCount()
+{
+ var stmt = createStatement("SELECT * FROM test WHERE id = ?1 OR name = ?2");
+ do_check_eq(2, stmt.columnCount);
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_getColumnName()
+{
+ var stmt = createStatement("SELECT name, id FROM test");
+ do_check_eq("id", stmt.getColumnName(1));
+ do_check_eq("name", stmt.getColumnName(0));
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_getColumnIndex_same_case()
+{
+ var stmt = createStatement("SELECT name, id FROM test");
+ do_check_eq(0, stmt.getColumnIndex("name"));
+ do_check_eq(1, stmt.getColumnIndex("id"));
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_getColumnIndex_different_case()
+{
+ var stmt = createStatement("SELECT name, id FROM test");
+ try {
+ do_check_eq(0, stmt.getColumnIndex("NaMe"));
+ do_throw("should not get here");
+ } catch (e) {
+ do_check_eq(Cr.NS_ERROR_INVALID_ARG, e.result);
+ }
+ try {
+ do_check_eq(1, stmt.getColumnIndex("Id"));
+ do_throw("should not get here");
+ } catch (e) {
+ do_check_eq(Cr.NS_ERROR_INVALID_ARG, e.result);
+ }
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_state_ready()
+{
+ var stmt = createStatement("SELECT name, id FROM test");
+ do_check_eq(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();
+ do_check_eq(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_EXECUTING,
+ stmt.state);
+ stmt.executeStep();
+ do_check_eq(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_EXECUTING,
+ stmt.state);
+ stmt.reset();
+ do_check_eq(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();
+ do_check_eq(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) {
+ do_check_eq(getOpenedDatabase().lastError, Ci.mozIStorageError.CONSTRAINT);
+ }
+ do_check_eq(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 000000000..27bd23992
--- /dev/null
+++ b/storage/test/unit/test_storage_value_array.js
@@ -0,0 +1,182 @@
+/* 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(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();
+
+ do_register_cleanup(cleanup);
+});
+
+add_task(function* test_getIsNull_for_null() {
+ var stmt = createStatement("SELECT nuller, blobber FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 1);
+ do_check_true(stmt.executeStep());
+
+ do_check_true(stmt.getIsNull(0)); // null field
+ do_check_true(stmt.getIsNull(1)); // data is null if size is 0
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_getIsNull_for_non_null() {
+ var stmt = createStatement("SELECT name, blobber FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ do_check_true(stmt.executeStep());
+
+ do_check_false(stmt.getIsNull(0));
+ do_check_false(stmt.getIsNull(1));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_value_type_null() {
+ var stmt = createStatement("SELECT nuller FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 1);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_NULL,
+ stmt.getTypeOfIndex(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_value_type_integer() {
+ var stmt = createStatement("SELECT id FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 1);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER,
+ stmt.getTypeOfIndex(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_value_type_float() {
+ var stmt = createStatement("SELECT number FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 1);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT,
+ stmt.getTypeOfIndex(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_value_type_text() {
+ var stmt = createStatement("SELECT name FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 1);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_TEXT,
+ stmt.getTypeOfIndex(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_value_type_blob() {
+ var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_BLOB,
+ stmt.getTypeOfIndex(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_numEntries_one() {
+ var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq(1, stmt.numEntries);
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_numEntries_all() {
+ var stmt = createStatement("SELECT * FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq(5, stmt.numEntries);
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_getInt() {
+ var stmt = createStatement("SELECT id FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq(2, stmt.getInt32(0));
+ do_check_eq(2, stmt.getInt64(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_getDouble() {
+ var stmt = createStatement("SELECT number FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq(1.23, stmt.getDouble(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_getUTF8String() {
+ var stmt = createStatement("SELECT name FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 1);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq("foo", stmt.getUTF8String(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_getString() {
+ var stmt = createStatement("SELECT name FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq("", stmt.getString(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_getBlob() {
+ var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ do_check_true(stmt.executeStep());
+
+ var count = { value: 0 };
+ var arr = { value: null };
+ stmt.getBlob(0, count, arr);
+ do_check_eq(2, count.value);
+ do_check_eq(1, arr.value[0]);
+ do_check_eq(2, arr.value[1]);
+ stmt.reset();
+ stmt.finalize();
+});
+
+
diff --git a/storage/test/unit/test_telemetry_vfs.js b/storage/test/unit/test_telemetry_vfs.js
new file mode 100644
index 000000000..0822fe3e7
--- /dev/null
+++ b/storage/test/unit/test_telemetry_vfs.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Make sure that there are telemetry entries created by sqlite io
+
+function run_sql(d, sql) {
+ var stmt = d.createStatement(sql);
+ stmt.execute();
+ stmt.finalize();
+}
+
+function new_file(name)
+{
+ var file = dirSvc.get("ProfD", Ci.nsIFile);
+ file.append(name);
+ return file;
+}
+function run_test()
+{
+ const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
+ let read_hgram = Telemetry.getHistogramById("MOZ_SQLITE_OTHER_READ_B");
+ let old_sum = read_hgram.snapshot().sum;
+ const file = new_file("telemetry.sqlite");
+ var d = getDatabase(file);
+ run_sql(d, "CREATE TABLE bloat(data varchar)");
+ run_sql(d, "DROP TABLE bloat");
+ do_check_true(read_hgram.snapshot().sum > old_sum);
+}
+
diff --git a/storage/test/unit/test_unicode.js b/storage/test/unit/test_unicode.js
new file mode 100644
index 000000000..7753bbfdb
--- /dev/null
+++ b/storage/test/unit/test_unicode.js
@@ -0,0 +1,83 @@
+/* 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(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();
+
+ do_register_cleanup(cleanup);
+});
+
+add_task(function* test_upper_ascii() {
+ var stmt = createStatement("SELECT name, id FROM test WHERE name = upper('a')");
+ do_check_true(stmt.executeStep());
+ do_check_eq("A", stmt.getString(0));
+ do_check_eq(2, stmt.getInt32(1));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_upper_non_ascii() {
+ var stmt = createStatement("SELECT name, id FROM test WHERE name = upper(?1)");
+ stmt.bindByIndex(0, LATIN1_ae);
+ do_check_true(stmt.executeStep());
+ do_check_eq(LATIN1_AE, stmt.getString(0));
+ do_check_eq(1, stmt.getInt32(1));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_lower_ascii() {
+ var stmt = createStatement("SELECT name, id FROM test WHERE name = lower('B')");
+ do_check_true(stmt.executeStep());
+ do_check_eq("b", stmt.getString(0));
+ do_check_eq(3, stmt.getInt32(1));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_lower_non_ascii() {
+ var stmt = createStatement("SELECT name, id FROM test WHERE name = lower(?1)");
+ stmt.bindByIndex(0, LATIN1_AE);
+ do_check_true(stmt.executeStep());
+ do_check_eq(LATIN1_ae, stmt.getString(0));
+ do_check_eq(4, stmt.getInt32(1));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_like_search_different() {
+ var stmt = createStatement("SELECT COUNT(*) FROM test WHERE name LIKE ?1");
+ stmt.bindByIndex(0, LATIN1_AE);
+ do_check_true(stmt.executeStep());
+ do_check_eq(2, stmt.getInt32(0));
+ stmt.finalize();
+});
+
+add_task(function* test_like_search_same() {
+ var stmt = createStatement("SELECT COUNT(*) FROM test WHERE name LIKE ?1");
+ stmt.bindByIndex(0, LATIN1_ae);
+ do_check_true(stmt.executeStep());
+ do_check_eq(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 000000000..e284f78c7
--- /dev/null
+++ b/storage/test/unit/test_vacuum.js
@@ -0,0 +1,335 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This file tests the Vacuum Manager.
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+/**
+ * Loads a test component that will register as a vacuum-participant.
+ * If other participants are found they will be unregistered, to avoid conflicts
+ * with the test itself.
+ */
+function load_test_vacuum_component()
+{
+ const CATEGORY_NAME = "vacuum-participant";
+
+ do_load_manifest("vacuumParticipant.manifest");
+
+ // This is a lazy check, there could be more participants than just this test
+ // we just mind that the test exists though.
+ const EXPECTED_ENTRIES = ["vacuumParticipant"];
+ let catMan = Cc["@mozilla.org/categorymanager;1"].
+ getService(Ci.nsICategoryManager);
+ let found = false;
+ let entries = catMan.enumerateCategory(CATEGORY_NAME);
+ while (entries.hasMoreElements()) {
+ let entry = entries.getNext().QueryInterface(Ci.nsISupportsCString).data;
+ print("Check if the found category entry (" + entry + ") is expected.");
+ if (EXPECTED_ENTRIES.indexOf(entry) != -1) {
+ print("Check that only one test entry exists.");
+ do_check_false(found);
+ found = true;
+ }
+ else {
+ // Temporary unregister other participants for this test.
+ catMan.deleteCategoryEntry("vacuum-participant", entry, false);
+ }
+ }
+ print("Check the test entry exists.");
+ do_check_true(found);
+}
+
+/**
+ * Sends a fake idle-daily notification to the VACUUM Manager.
+ */
+function synthesize_idle_daily()
+{
+ let vm = Cc["@mozilla.org/storage/vacuum;1"].getService(Ci.nsIObserver);
+ vm.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)
+{
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append(name + ".sqlite");
+ return file;
+}
+
+function run_test()
+{
+ do_test_pending();
+
+ // Change initial page size. Do it immediately since it would require an
+ // additional vacuum op to do it later. As a bonus this makes the page size
+ // change test really fast since it only has to check results.
+ let conn = getDatabase(new_db_file("testVacuum"));
+ conn.executeSimpleSQL("PRAGMA page_size = 1024");
+ print("Check current page size.");
+ let stmt = conn.createStatement("PRAGMA page_size");
+ try {
+ while (stmt.executeStep()) {
+ do_check_eq(stmt.row.page_size, 1024);
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ load_test_vacuum_component();
+
+ run_next_test();
+}
+
+const TESTS = [
+
+ function test_common_vacuum()
+ {
+ print("\n*** Test that a VACUUM correctly happens and all notifications are fired.");
+ // Wait for VACUUM begin.
+ let beginVacuumReceived = false;
+ Services.obs.addObserver(function onVacuum(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(onVacuum, aTopic);
+ beginVacuumReceived = true;
+ }, "test-begin-vacuum", false);
+
+ // Wait for heavy IO notifications.
+ let heavyIOTaskBeginReceived = false;
+ let heavyIOTaskEndReceived = false;
+ Services.obs.addObserver(function onVacuum(aSubject, aTopic, aData) {
+ if (heavyIOTaskBeginReceived && heavyIOTaskEndReceived) {
+ Services.obs.removeObserver(onVacuum, aTopic);
+ }
+
+ if (aData == "vacuum-begin") {
+ heavyIOTaskBeginReceived = true;
+ }
+ else if (aData == "vacuum-end") {
+ heavyIOTaskEndReceived = true;
+ }
+ }, "heavy-io-task", false);
+
+ // Wait for VACUUM end.
+ Services.obs.addObserver(function onVacuum(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(onVacuum, aTopic);
+ print("Check we received onBeginVacuum");
+ do_check_true(beginVacuumReceived);
+ print("Check we received heavy-io-task notifications");
+ do_check_true(heavyIOTaskBeginReceived);
+ do_check_true(heavyIOTaskEndReceived);
+ print("Received onEndVacuum");
+ run_next_test();
+ }, "test-end-vacuum", false);
+
+ synthesize_idle_daily();
+ },
+
+ function test_skipped_if_recent_vacuum()
+ {
+ print("\n*** 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 begin.
+ let vacuumObserver = {
+ gotNotification: false,
+ observe: function VO_observe(aSubject, aTopic, aData) {
+ this.gotNotification = true;
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+ };
+ Services.obs.addObserver(vacuumObserver, "test-begin-vacuum", false);
+
+ // Check after a couple seconds that no VACUUM has been run.
+ do_timeout(2000, function () {
+ print("Check VACUUM did not run.");
+ do_check_false(vacuumObserver.gotNotification);
+ Services.obs.removeObserver(vacuumObserver, "test-begin-vacuum");
+ run_next_test();
+ });
+
+ synthesize_idle_daily();
+ },
+
+ function test_page_size_change()
+ {
+ print("\n*** Test that a VACUUM changes page_size");
+
+ // We did setup the database with a small page size, the previous vacuum
+ // should have updated it.
+ print("Check that page size was updated.");
+ let conn = getDatabase(new_db_file("testVacuum"));
+ let stmt = conn.createStatement("PRAGMA page_size");
+ try {
+ while (stmt.executeStep()) {
+ do_check_eq(stmt.row.page_size, conn.defaultPageSize);
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ run_next_test();
+ },
+
+ function test_skipped_optout_vacuum()
+ {
+ print("\n*** Test that a VACUUM is skipped if the participant wants to opt-out.");
+ Services.obs.notifyObservers(null, "test-options", "opt-out");
+
+ // Wait for VACUUM begin.
+ let vacuumObserver = {
+ gotNotification: false,
+ observe: function VO_observe(aSubject, aTopic, aData) {
+ this.gotNotification = true;
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+ };
+ Services.obs.addObserver(vacuumObserver, "test-begin-vacuum", false);
+
+ // Check after a couple seconds that no VACUUM has been run.
+ do_timeout(2000, function () {
+ print("Check VACUUM did not run.");
+ do_check_false(vacuumObserver.gotNotification);
+ Services.obs.removeObserver(vacuumObserver, "test-begin-vacuum");
+ run_next_test();
+ });
+
+ synthesize_idle_daily();
+ },
+
+ /* Changing page size on WAL is not supported till Bug 634374 is properly fixed.
+ function test_page_size_change_with_wal()
+ {
+ print("\n*** Test that a VACUUM changes page_size with WAL mode");
+ Services.obs.notifyObservers(null, "test-options", "wal");
+
+ // Set a small page size.
+ let conn = getDatabase(new_db_file("testVacuum2"));
+ conn.executeSimpleSQL("PRAGMA page_size = 1024");
+ let stmt = conn.createStatement("PRAGMA page_size");
+ try {
+ while (stmt.executeStep()) {
+ do_check_eq(stmt.row.page_size, 1024);
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ // Use WAL journal mode.
+ conn.executeSimpleSQL("PRAGMA journal_mode = WAL");
+ stmt = conn.createStatement("PRAGMA journal_mode");
+ try {
+ while (stmt.executeStep()) {
+ do_check_eq(stmt.row.journal_mode, "wal");
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ // Wait for VACUUM end.
+ let vacuumObserver = {
+ observe: function VO_observe(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(this, aTopic);
+ print("Check page size has been updated.");
+ let stmt = conn.createStatement("PRAGMA page_size");
+ try {
+ while (stmt.executeStep()) {
+ do_check_eq(stmt.row.page_size, Ci.mozIStorageConnection.DEFAULT_PAGE_SIZE);
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ print("Check journal mode has been restored.");
+ stmt = conn.createStatement("PRAGMA journal_mode");
+ try {
+ while (stmt.executeStep()) {
+ do_check_eq(stmt.row.journal_mode, "wal");
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ run_next_test();
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+ }
+ Services.obs.addObserver(vacuumObserver, "test-end-vacuum", false);
+
+ synthesize_idle_daily();
+ },
+ */
+
+ function test_memory_database_crash()
+ {
+ print("\n*** Test that we don't crash trying to vacuum a memory database");
+ Services.obs.notifyObservers(null, "test-options", "memory");
+
+ // Wait for VACUUM begin.
+ let vacuumObserver = {
+ gotNotification: false,
+ observe: function VO_observe(aSubject, aTopic, aData) {
+ this.gotNotification = true;
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+ };
+ Services.obs.addObserver(vacuumObserver, "test-begin-vacuum", false);
+
+ // Check after a couple seconds that no VACUUM has been run.
+ do_timeout(2000, function () {
+ print("Check VACUUM did not run.");
+ do_check_false(vacuumObserver.gotNotification);
+ Services.obs.removeObserver(vacuumObserver, "test-begin-vacuum");
+ run_next_test();
+ });
+
+ synthesize_idle_daily();
+ },
+
+ /* Changing page size on WAL is not supported till Bug 634374 is properly fixed.
+ function test_wal_restore_fail()
+ {
+ print("\n*** Test that a failing WAL restoration notifies failure");
+ Services.obs.notifyObservers(null, "test-options", "wal-fail");
+
+ // Wait for VACUUM end.
+ let vacuumObserver = {
+ observe: function VO_observe(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(vacuumObserver, "test-end-vacuum");
+ print("Check WAL restoration failed.");
+ do_check_false(aData);
+ run_next_test();
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+ }
+ Services.obs.addObserver(vacuumObserver, "test-end-vacuum", false);
+
+ synthesize_idle_daily();
+ },
+ */
+];
+
+function run_next_test()
+{
+ if (TESTS.length == 0) {
+ Services.obs.notifyObservers(null, "test-options", "dispose");
+ do_test_finished();
+ }
+ else {
+ // Set last VACUUM to a date in the past.
+ Services.prefs.setIntPref("storage.vacuum.last.testVacuum.sqlite",
+ parseInt(Date.now() / 1000 - 31 * 86400));
+ do_execute_soon(TESTS.shift());
+ }
+}
diff --git a/storage/test/unit/vacuumParticipant.js b/storage/test/unit/vacuumParticipant.js
new file mode 100644
index 000000000..01b980178
--- /dev/null
+++ b/storage/test/unit/vacuumParticipant.js
@@ -0,0 +1,125 @@
+/* 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 Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+/**
+ * Returns a new nsIFile reference for a profile database.
+ * @param filename for the database, excluded the .sqlite extension.
+ */
+function new_db_file(name)
+{
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append(name + ".sqlite");
+ return file;
+}
+
+/**
+ * Opens and returns a connection to the provided database file.
+ * @param nsIFile interface to the database file.
+ */
+function getDatabase(aFile)
+{
+ return Cc["@mozilla.org/storage/service;1"].getService(Ci.mozIStorageService)
+ .openDatabase(aFile);
+}
+
+function vacuumParticipant()
+{
+ this._dbConn = getDatabase(new_db_file("testVacuum"));
+ Services.obs.addObserver(this, "test-options", false);
+}
+
+vacuumParticipant.prototype =
+{
+ classDescription: "vacuumParticipant",
+ classID: Components.ID("{52aa0b22-b82f-4e38-992a-c3675a3355d2}"),
+ contractID: "@unit.test.com/test-vacuum-participant;1",
+
+ get expectedDatabasePageSize() {
+ return this._dbConn.defaultPageSize;
+ },
+ get databaseConnection() {
+ return this._dbConn;
+ },
+
+ _grant: true,
+ onBeginVacuum: function TVP_onBeginVacuum()
+ {
+ if (!this._grant) {
+ this._grant = true;
+ return false;
+ }
+ Services.obs.notifyObservers(null, "test-begin-vacuum", null);
+ return true;
+ },
+ onEndVacuum: function TVP_EndVacuum(aSucceeded)
+ {
+ if (this._stmt) {
+ this._stmt.finalize();
+ }
+ Services.obs.notifyObservers(null, "test-end-vacuum", aSucceeded);
+ },
+
+ observe: function TVP_observe(aSubject, aTopic, aData)
+ {
+ if (aData == "opt-out") {
+ this._grant = false;
+ }
+ else if (aData == "wal") {
+ try {
+ this._dbConn.close();
+ } catch (e) {
+ // Do nothing.
+ }
+ this._dbConn = getDatabase(new_db_file("testVacuum2"));
+ }
+ else if (aData == "wal-fail") {
+ try {
+ this._dbConn.close();
+ } catch (e) {
+ // Do nothing.
+ }
+ this._dbConn = getDatabase(new_db_file("testVacuum3"));
+ // Use WAL journal mode.
+ this._dbConn.executeSimpleSQL("PRAGMA journal_mode = WAL");
+ // Create a not finalized statement.
+ this._stmt = this._dbConn.createStatement("SELECT :test");
+ this._stmt.params.test = 1;
+ this._stmt.executeStep();
+ }
+ else if (aData == "memory") {
+ try {
+ this._dbConn.asyncClose();
+ } catch (e) {
+ // Do nothing.
+ }
+ this._dbConn = Cc["@mozilla.org/storage/service;1"].
+ getService(Ci.mozIStorageService).
+ openSpecialDatabase("memory");
+ }
+ else if (aData == "dispose") {
+ Services.obs.removeObserver(this, "test-options");
+ try {
+ this._dbConn.asyncClose();
+ } catch (e) {
+ // Do nothing.
+ }
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.mozIStorageVacuumParticipant,
+ Ci.nsIObserver,
+ ])
+};
+
+var gComponentsArray = [vacuumParticipant];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(gComponentsArray);
diff --git a/storage/test/unit/vacuumParticipant.manifest b/storage/test/unit/vacuumParticipant.manifest
new file mode 100644
index 000000000..cf359a80f
--- /dev/null
+++ b/storage/test/unit/vacuumParticipant.manifest
@@ -0,0 +1,3 @@
+component {52aa0b22-b82f-4e38-992a-c3675a3355d2} vacuumParticipant.js
+contract @unit.test.com/test-vacuum-participant;1 {52aa0b22-b82f-4e38-992a-c3675a3355d2}
+category vacuum-participant vacuumParticipant @unit.test.com/test-vacuum-participant;1
diff --git a/storage/test/unit/xpcshell.ini b/storage/test/unit/xpcshell.ini
new file mode 100644
index 000000000..e93c7d5b9
--- /dev/null
+++ b/storage/test/unit/xpcshell.ini
@@ -0,0 +1,46 @@
+[DEFAULT]
+head = head_storage.js
+tail =
+support-files =
+ corruptDB.sqlite
+ fakeDB.sqlite
+ locale_collation.txt
+ vacuumParticipant.js
+ vacuumParticipant.manifest
+
+[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_js_helpers.js]
+[test_levenshtein.js]
+[test_like.js]
+[test_like_escape.js]
+[test_locale_collation.js]
+[test_page_size_is_32k.js]
+[test_sqlite_secure_delete.js]
+[test_statement_executeAsync.js]
+[test_statement_wrapper_automatically.js]
+[test_storage_aggregates.js]
+[test_storage_connection.js]
+# Bug 676981: test fails consistently on Android
+fail-if = os == "android"
+[test_storage_fulltextindex.js]
+[test_storage_function.js]
+[test_storage_progresshandler.js]
+[test_storage_service.js]
+[test_storage_service_unshared.js]
+[test_storage_statement.js]
+[test_storage_value_array.js]
+[test_unicode.js]
+[test_vacuum.js]
+[test_telemetry_vfs.js]
+# Bug 676981: test fails consistently on Android
+# fail-if = os == "android"