diff options
Diffstat (limited to '')
62 files changed, 9109 insertions, 0 deletions
diff --git a/storage/test/gtest/moz.build b/storage/test/gtest/moz.build new file mode 100644 index 0000000000..62f91b6d95 --- /dev/null +++ b/storage/test/gtest/moz.build @@ -0,0 +1,39 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +UNIFIED_SOURCES += [ + "storage_test_harness.cpp", + "test_AsXXX_helpers.cpp", + "test_async_callbacks_with_spun_event_loops.cpp", + "test_asyncStatementExecution_transaction.cpp", + "test_binding_params.cpp", + "test_file_perms.cpp", + "test_interruptSynchronousConnection.cpp", + "test_mutex.cpp", + "test_spinningSynchronousClose.cpp", + "test_statement_scoper.cpp", + "test_StatementCache.cpp", + "test_transaction_helper.cpp", + "test_true_async.cpp", + "test_unlock_notify.cpp", +] + +if ( + CONFIG["MOZ_DEBUG"] + and CONFIG["OS_ARCH"] not in ("WINNT") + and CONFIG["OS_TARGET"] != "Android" +): + # FIXME bug 523392: test_deadlock_detector doesn't like Windows + # Bug 1054249: Doesn't work on Android + UNIFIED_SOURCES += [ + "test_deadlock_detector.cpp", + ] + +LOCAL_INCLUDES += [ + "../..", +] + +FINAL_LIBRARY = "xul-gtest" diff --git a/storage/test/gtest/storage_test_harness.cpp b/storage/test/gtest/storage_test_harness.cpp new file mode 100644 index 0000000000..3f5a9ff82b --- /dev/null +++ b/storage/test/gtest/storage_test_harness.cpp @@ -0,0 +1,220 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef storage_test_harness_ +#define storage_test_harness_ + +#include "storage_test_harness.h" + +already_AddRefed<mozIStorageService> getService() { + nsCOMPtr<mozIStorageService> ss = + do_CreateInstance("@mozilla.org/storage/service;1"); + do_check_true(ss); + return ss.forget(); +} + +already_AddRefed<mozIStorageConnection> getMemoryDatabase() { + nsCOMPtr<mozIStorageService> ss = getService(); + nsCOMPtr<mozIStorageConnection> conn; + nsresult rv = ss->OpenSpecialDatabase( + kMozStorageMemoryStorageKey, VoidCString(), + mozIStorageService::CONNECTION_DEFAULT, getter_AddRefs(conn)); + do_check_success(rv); + return conn.forget(); +} + +already_AddRefed<mozIStorageConnection> getDatabase(nsIFile* aDBFile, + uint32_t aConnectionFlags) { + nsCOMPtr<nsIFile> dbFile; + nsresult rv; + if (!aDBFile) { + MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Can't get tmp dir off mainthread."); + (void)NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(dbFile)); + NS_ASSERTION(dbFile, "The directory doesn't exists?!"); + + rv = dbFile->Append(u"storage_test_db.sqlite"_ns); + do_check_success(rv); + } else { + dbFile = aDBFile; + } + + nsCOMPtr<mozIStorageService> ss = getService(); + nsCOMPtr<mozIStorageConnection> conn; + rv = ss->OpenDatabase(dbFile, aConnectionFlags, getter_AddRefs(conn)); + do_check_success(rv); + return conn.forget(); +} + +NS_IMPL_ISUPPORTS(AsyncStatementSpinner, mozIStorageStatementCallback, + mozIStorageCompletionCallback) + +AsyncStatementSpinner::AsyncStatementSpinner() + : completionReason(0), mCompleted(false) {} + +NS_IMETHODIMP +AsyncStatementSpinner::HandleResult(mozIStorageResultSet* aResultSet) { + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatementSpinner::HandleError(mozIStorageError* aError) { + int32_t result; + nsresult rv = aError->GetResult(&result); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString message; + rv = aError->GetMessage(message); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString warnMsg; + warnMsg.AppendLiteral( + "An error occurred while executing an async statement: "); + warnMsg.AppendInt(result); + warnMsg.Append(' '); + warnMsg.Append(message); + NS_WARNING(warnMsg.get()); + + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatementSpinner::HandleCompletion(uint16_t aReason) { + completionReason = aReason; + mCompleted = true; + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatementSpinner::Complete(nsresult, nsISupports*) { + mCompleted = true; + return NS_OK; +} + +void AsyncStatementSpinner::SpinUntilCompleted() { + nsCOMPtr<nsIThread> thread(::do_GetCurrentThread()); + nsresult rv = NS_OK; + bool processed = true; + while (!mCompleted && NS_SUCCEEDED(rv)) { + rv = thread->ProcessNextEvent(true, &processed); + } +} + +#define NS_DECL_ASYNCSTATEMENTSPINNER \ + NS_IMETHOD HandleResult(mozIStorageResultSet* aResultSet) override; + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * Execute an async statement, blocking the main thread until we get the + * callback completion notification. + */ +void blocking_async_execute(mozIStorageBaseStatement* stmt) { + RefPtr<AsyncStatementSpinner> spinner(new AsyncStatementSpinner()); + + nsCOMPtr<mozIStoragePendingStatement> pendy; + (void)stmt->ExecuteAsync(spinner, getter_AddRefs(pendy)); + spinner->SpinUntilCompleted(); +} + +/** + * Invoke AsyncClose on the given connection, blocking the main thread until we + * get the completion notification. + */ +void blocking_async_close(mozIStorageConnection* db) { + RefPtr<AsyncStatementSpinner> spinner(new AsyncStatementSpinner()); + + db->AsyncClose(spinner); + spinner->SpinUntilCompleted(); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Mutex Watching + +/** + * Verify that mozIStorageAsyncStatement's life-cycle never triggers a mutex on + * the caller (generally main) thread. We do this by decorating the sqlite + * mutex logic with our own code that checks what thread it is being invoked on + * and sets a flag if it is invoked on the main thread. We are able to easily + * decorate the SQLite mutex logic because SQLite allows us to retrieve the + * current function pointers being used and then provide a new set. + */ + +sqlite3_mutex_methods orig_mutex_methods; +sqlite3_mutex_methods wrapped_mutex_methods; + +bool mutex_used_on_watched_thread = false; +PRThread* watched_thread = nullptr; +/** + * Ugly hack to let us figure out what a connection's async thread is. If we + * were MOZILLA_INTERNAL_API and linked as such we could just include + * mozStorageConnection.h and just ask Connection directly. But that turns out + * poorly. + * + * When the thread a mutex is invoked on isn't watched_thread we save it to this + * variable. + */ +nsIThread* last_non_watched_thread = nullptr; + +/** + * Set a flag if the mutex is used on the thread we are watching, but always + * call the real mutex function. + */ +extern "C" void wrapped_MutexEnter(sqlite3_mutex* mutex) { + if (PR_GetCurrentThread() == watched_thread) + mutex_used_on_watched_thread = true; + else + last_non_watched_thread = NS_GetCurrentThread(); + orig_mutex_methods.xMutexEnter(mutex); +} + +extern "C" int wrapped_MutexTry(sqlite3_mutex* mutex) { + if (::PR_GetCurrentThread() == watched_thread) + mutex_used_on_watched_thread = true; + return orig_mutex_methods.xMutexTry(mutex); +} + +/** + * Call to clear the watch state and to set the watching against this thread. + * + * Check |mutex_used_on_watched_thread| to see if the mutex has fired since + * this method was last called. Since we're talking about the current thread, + * there are no race issues to be concerned about + */ +void watch_for_mutex_use_on_this_thread() { + watched_thread = ::PR_GetCurrentThread(); + mutex_used_on_watched_thread = false; +} + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * A horrible hack to figure out what the connection's async thread is. By + * creating a statement and async dispatching we can tell from the mutex who + * is the async thread, PRThread style. Then we map that to an nsIThread. + */ +already_AddRefed<nsIThread> get_conn_async_thread(mozIStorageConnection* db) { + // Make sure we are tracking the current thread as the watched thread + watch_for_mutex_use_on_this_thread(); + + // - statement with nothing to bind + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement("SELECT 1"_ns, getter_AddRefs(stmt)); + blocking_async_execute(stmt); + stmt->Finalize(); + + nsCOMPtr<nsIThread> asyncThread = last_non_watched_thread; + + // Additionally, check that the thread we get as the background thread is the + // same one as the one we report from getInterface. + nsCOMPtr<nsIEventTarget> target = do_GetInterface(db); + nsCOMPtr<nsIThread> allegedAsyncThread = do_QueryInterface(target); + do_check_eq(allegedAsyncThread, asyncThread); + return asyncThread.forget(); +} + +#endif // storage_test_harness_h__ diff --git a/storage/test/gtest/storage_test_harness.h b/storage/test/gtest/storage_test_harness.h new file mode 100644 index 0000000000..7df951fedb --- /dev/null +++ b/storage/test/gtest/storage_test_harness.h @@ -0,0 +1,206 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef storage_test_harness_h__ +#define storage_test_harness_h__ + +#include "gtest/gtest.h" + +#include "prthread.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsComponentManagerUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsIThread.h" +#include "nsThreadUtils.h" +#include "mozilla/ReentrantMonitor.h" + +#include "mozIStorageService.h" +#include "mozIStorageConnection.h" +#include "mozIStorageStatementCallback.h" +#include "mozIStorageCompletionCallback.h" +#include "mozIStorageBindingParamsArray.h" +#include "mozIStorageBindingParams.h" +#include "mozIStorageAsyncStatement.h" +#include "mozIStorageStatement.h" +#include "mozIStoragePendingStatement.h" +#include "mozIStorageError.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIEventTarget.h" + +#include "sqlite3.h" + +#define do_check_true(aCondition) EXPECT_TRUE(aCondition) + +#define do_check_false(aCondition) EXPECT_FALSE(aCondition) + +#define do_check_success(aResult) do_check_true(NS_SUCCEEDED(aResult)) + +#define do_check_eq(aExpected, aActual) do_check_true(aExpected == aActual) + +#define do_check_ok(aInvoc) do_check_true((aInvoc) == SQLITE_OK) + +already_AddRefed<mozIStorageService> getService(); + +already_AddRefed<mozIStorageConnection> getMemoryDatabase(); + +already_AddRefed<mozIStorageConnection> getDatabase( + nsIFile* aDBFile = nullptr, + uint32_t aConnectionFlags = mozIStorageService::CONNECTION_DEFAULT); + +class AsyncStatementSpinner : public mozIStorageStatementCallback, + public mozIStorageCompletionCallback { + public: + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGESTATEMENTCALLBACK + NS_DECL_MOZISTORAGECOMPLETIONCALLBACK + + AsyncStatementSpinner(); + + void SpinUntilCompleted(); + + uint16_t completionReason; + + protected: + virtual ~AsyncStatementSpinner() {} + volatile bool mCompleted; +}; + +#define NS_DECL_ASYNCSTATEMENTSPINNER \ + NS_IMETHOD HandleResult(mozIStorageResultSet* aResultSet) override; + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * Execute an async statement, blocking the main thread until we get the + * callback completion notification. + */ +void blocking_async_execute(mozIStorageBaseStatement* stmt); + +/** + * Invoke AsyncClose on the given connection, blocking the main thread until we + * get the completion notification. + */ +void blocking_async_close(mozIStorageConnection* db); + +//////////////////////////////////////////////////////////////////////////////// +//// Mutex Watching + +/** + * Verify that mozIStorageAsyncStatement's life-cycle never triggers a mutex on + * the caller (generally main) thread. We do this by decorating the sqlite + * mutex logic with our own code that checks what thread it is being invoked on + * and sets a flag if it is invoked on the main thread. We are able to easily + * decorate the SQLite mutex logic because SQLite allows us to retrieve the + * current function pointers being used and then provide a new set. + */ + +extern sqlite3_mutex_methods orig_mutex_methods; +extern sqlite3_mutex_methods wrapped_mutex_methods; + +extern bool mutex_used_on_watched_thread; +extern PRThread* watched_thread; +/** + * Ugly hack to let us figure out what a connection's async thread is. If we + * were MOZILLA_INTERNAL_API and linked as such we could just include + * mozStorageConnection.h and just ask Connection directly. But that turns out + * poorly. + * + * When the thread a mutex is invoked on isn't watched_thread we save it to this + * variable. + */ +extern nsIThread* last_non_watched_thread; + +/** + * Set a flag if the mutex is used on the thread we are watching, but always + * call the real mutex function. + */ +extern "C" void wrapped_MutexEnter(sqlite3_mutex* mutex); + +extern "C" int wrapped_MutexTry(sqlite3_mutex* mutex); + +class HookSqliteMutex { + public: + HookSqliteMutex() { + // We need to initialize and teardown SQLite to get it to set up the + // default mutex handlers for us so we can steal them and wrap them. + do_check_ok(sqlite3_initialize()); + do_check_ok(sqlite3_shutdown()); + do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &orig_mutex_methods)); + do_check_ok( + ::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &wrapped_mutex_methods)); + wrapped_mutex_methods.xMutexEnter = wrapped_MutexEnter; + wrapped_mutex_methods.xMutexTry = wrapped_MutexTry; + do_check_ok(::sqlite3_config(SQLITE_CONFIG_MUTEX, &wrapped_mutex_methods)); + } + + ~HookSqliteMutex() { + do_check_ok(sqlite3_shutdown()); + do_check_ok(::sqlite3_config(SQLITE_CONFIG_MUTEX, &orig_mutex_methods)); + do_check_ok(sqlite3_initialize()); + } +}; + +/** + * Call to clear the watch state and to set the watching against this thread. + * + * Check |mutex_used_on_watched_thread| to see if the mutex has fired since + * this method was last called. Since we're talking about the current thread, + * there are no race issues to be concerned about + */ +void watch_for_mutex_use_on_this_thread(); + +//////////////////////////////////////////////////////////////////////////////// +//// Thread Wedgers + +/** + * A runnable that blocks until code on another thread invokes its unwedge + * method. By dispatching this to a thread you can ensure that no subsequent + * runnables dispatched to the thread will execute until you invoke unwedge. + * + * The wedger is self-dispatching, just construct it with its target. + */ +class ThreadWedger : public mozilla::Runnable { + public: + explicit ThreadWedger(nsIEventTarget* aTarget) + : mozilla::Runnable("ThreadWedger"), + mReentrantMonitor("thread wedger"), + unwedged(false) { + aTarget->Dispatch(this, aTarget->NS_DISPATCH_NORMAL); + } + + NS_IMETHOD Run() override { + mozilla::ReentrantMonitorAutoEnter automon(mReentrantMonitor); + + if (!unwedged) automon.Wait(); + + return NS_OK; + } + + void unwedge() { + mozilla::ReentrantMonitorAutoEnter automon(mReentrantMonitor); + unwedged = true; + automon.Notify(); + } + + private: + mozilla::ReentrantMonitor mReentrantMonitor MOZ_UNANNOTATED; + bool unwedged; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * A horrible hack to figure out what the connection's async thread is. By + * creating a statement and async dispatching we can tell from the mutex who + * is the async thread, PRThread style. Then we map that to an nsIThread. + */ +already_AddRefed<nsIThread> get_conn_async_thread(mozIStorageConnection* db); + +#endif // storage_test_harness_h__ diff --git a/storage/test/gtest/test_AsXXX_helpers.cpp b/storage/test/gtest/test_AsXXX_helpers.cpp new file mode 100644 index 0000000000..2320b8383c --- /dev/null +++ b/storage/test/gtest/test_AsXXX_helpers.cpp @@ -0,0 +1,111 @@ +/* + *Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +#include "storage_test_harness.h" +#include "mozIStorageRow.h" +#include "mozIStorageResultSet.h" +#include "nsComponentManagerUtils.h" + +/** + * This file tests AsXXX (AsInt32, AsInt64, ...) helpers. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Event Loop Spinning + +class Spinner : public AsyncStatementSpinner { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_ASYNCSTATEMENTSPINNER + Spinner() {} + + protected: + ~Spinner() override = default; +}; + +NS_IMPL_ISUPPORTS_INHERITED0(Spinner, AsyncStatementSpinner) + +NS_IMETHODIMP +Spinner::HandleResult(mozIStorageResultSet* aResultSet) { + nsCOMPtr<mozIStorageRow> row; + do_check_true(NS_SUCCEEDED(aResultSet->GetNextRow(getter_AddRefs(row))) && + row); + + do_check_eq(row->AsInt32(0), 0); + do_check_eq(row->AsInt64(0), 0); + do_check_eq(row->AsDouble(0), 0.0); + + uint32_t len = 100; + do_check_eq(row->AsSharedUTF8String(0, &len), (const char*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(row->AsSharedWString(0, &len), (const char16_t*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(row->AsSharedBlob(0, &len), (const uint8_t*)nullptr); + do_check_eq(len, 0); + + do_check_eq(row->IsNull(0), true); + return NS_OK; +} + +TEST(storage_AsXXX_helpers, NULLFallback) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + nsCOMPtr<mozIStorageStatement> stmt; + (void)db->CreateStatement("SELECT NULL"_ns, getter_AddRefs(stmt)); + + nsCOMPtr<mozIStorageValueArray> valueArray = do_QueryInterface(stmt); + do_check_true(valueArray); + + bool hasMore; + do_check_true(NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore); + + do_check_eq(stmt->AsInt32(0), 0); + do_check_eq(stmt->AsInt64(0), 0); + do_check_eq(stmt->AsDouble(0), 0.0); + uint32_t len = 100; + do_check_eq(stmt->AsSharedUTF8String(0, &len), (const char*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(stmt->AsSharedWString(0, &len), (const char16_t*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(stmt->AsSharedBlob(0, &len), (const uint8_t*)nullptr); + do_check_eq(len, 0); + do_check_eq(stmt->IsNull(0), true); + + do_check_eq(valueArray->AsInt32(0), 0); + do_check_eq(valueArray->AsInt64(0), 0); + do_check_eq(valueArray->AsDouble(0), 0.0); + len = 100; + do_check_eq(valueArray->AsSharedUTF8String(0, &len), (const char*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(valueArray->AsSharedWString(0, &len), (const char16_t*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(valueArray->AsSharedBlob(0, &len), (const uint8_t*)nullptr); + do_check_eq(len, 0); + do_check_eq(valueArray->IsNull(0), true); +} + +TEST(storage_AsXXX_helpers, asyncNULLFallback) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + nsCOMPtr<mozIStorageAsyncStatement> stmt; + (void)db->CreateAsyncStatement("SELECT NULL"_ns, getter_AddRefs(stmt)); + + nsCOMPtr<mozIStoragePendingStatement> pendingStmt; + do_check_true( + NS_SUCCEEDED(stmt->ExecuteAsync(nullptr, getter_AddRefs(pendingStmt)))); + do_check_true(pendingStmt); + stmt->Finalize(); + RefPtr<Spinner> asyncSpin(new Spinner()); + db->AsyncClose(asyncSpin); + asyncSpin->SpinUntilCompleted(); +} diff --git a/storage/test/gtest/test_StatementCache.cpp b/storage/test/gtest/test_StatementCache.cpp new file mode 100644 index 0000000000..9908a34d79 --- /dev/null +++ b/storage/test/gtest/test_StatementCache.cpp @@ -0,0 +1,132 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "mozilla/Attributes.h" +#include "mozilla/storage/StatementCache.h" +using namespace mozilla::storage; + +/** + * This file test our statement cache in StatementCache.h. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Helpers + +class SyncCache : public StatementCache<mozIStorageStatement> { + public: + explicit SyncCache(nsCOMPtr<mozIStorageConnection>& aConnection) + : StatementCache<mozIStorageStatement>(aConnection) {} +}; + +class AsyncCache : public StatementCache<mozIStorageAsyncStatement> { + public: + explicit AsyncCache(nsCOMPtr<mozIStorageConnection>& aConnection) + : StatementCache<mozIStorageAsyncStatement>(aConnection) {} +}; + +/** + * Wraps nsCString so we can not implement the same functions twice for each + * type. + */ +class StringWrapper : public nsCString { + public: + MOZ_IMPLICIT StringWrapper(const char* aOther) { this->Assign(aOther); } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Test Functions + +// This is some gtest magic that allows us to parameterize tests by |const +// char[]| and |StringWrapper|. +template <typename T> +class storage_StatementCache : public ::testing::Test {}; +typedef ::testing::Types<const char[], StringWrapper> TwoStringTypes; + +TYPED_TEST_SUITE(storage_StatementCache, TwoStringTypes); +TYPED_TEST(storage_StatementCache, GetCachedStatement) { + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + SyncCache cache(db); + + TypeParam sql = "SELECT * FROM sqlite_master"; + + // Make sure we get a statement back with the right state. + nsCOMPtr<mozIStorageStatement> stmt = cache.GetCachedStatement(sql); + do_check_true(stmt); + int32_t state; + do_check_success(stmt->GetState(&state)); + do_check_eq(mozIStorageBaseStatement::MOZ_STORAGE_STATEMENT_READY, state); + + // Check to make sure we get the same copy the second time we ask. + nsCOMPtr<mozIStorageStatement> stmt2 = cache.GetCachedStatement(sql); + do_check_true(stmt2); + do_check_eq(stmt.get(), stmt2.get()); +} + +TYPED_TEST_SUITE(storage_StatementCache, TwoStringTypes); +TYPED_TEST(storage_StatementCache, FinalizeStatements) { + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + SyncCache cache(db); + + TypeParam sql = "SELECT * FROM sqlite_master"; + + // Get a statement, and then tell the cache to finalize. + nsCOMPtr<mozIStorageStatement> stmt = cache.GetCachedStatement(sql); + do_check_true(stmt); + + cache.FinalizeStatements(); + + // We should be in an invalid state at this point. + int32_t state; + do_check_success(stmt->GetState(&state)); + do_check_eq(mozIStorageBaseStatement::MOZ_STORAGE_STATEMENT_INVALID, state); + + // Should be able to close the database now too. + do_check_success(db->Close()); +} + +TYPED_TEST_SUITE(storage_StatementCache, TwoStringTypes); +TYPED_TEST(storage_StatementCache, GetCachedAsyncStatement) { + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + AsyncCache cache(db); + + TypeParam sql = "SELECT * FROM sqlite_master"; + + // Make sure we get a statement back with the right state. + nsCOMPtr<mozIStorageAsyncStatement> stmt = cache.GetCachedStatement(sql); + do_check_true(stmt); + int32_t state; + do_check_success(stmt->GetState(&state)); + do_check_eq(mozIStorageBaseStatement::MOZ_STORAGE_STATEMENT_READY, state); + + // Check to make sure we get the same copy the second time we ask. + nsCOMPtr<mozIStorageAsyncStatement> stmt2 = cache.GetCachedStatement(sql); + do_check_true(stmt2); + do_check_eq(stmt.get(), stmt2.get()); +} + +TYPED_TEST_SUITE(storage_StatementCache, TwoStringTypes); +TYPED_TEST(storage_StatementCache, FinalizeAsyncStatements) { + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + AsyncCache cache(db); + + TypeParam sql = "SELECT * FROM sqlite_master"; + + // Get a statement, and then tell the cache to finalize. + nsCOMPtr<mozIStorageAsyncStatement> stmt = cache.GetCachedStatement(sql); + do_check_true(stmt); + + cache.FinalizeStatements(); + + // We should be in an invalid state at this point. + int32_t state; + do_check_success(stmt->GetState(&state)); + do_check_eq(mozIStorageBaseStatement::MOZ_STORAGE_STATEMENT_INVALID, state); + + // Should be able to close the database now too. + do_check_success(db->AsyncClose(nullptr)); +} diff --git a/storage/test/gtest/test_asyncStatementExecution_transaction.cpp b/storage/test/gtest/test_asyncStatementExecution_transaction.cpp new file mode 100644 index 0000000000..0e7dec1e8a --- /dev/null +++ b/storage/test/gtest/test_asyncStatementExecution_transaction.cpp @@ -0,0 +1,445 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +#include "storage_test_harness.h" + +#include "mozStorageConnection.h" + +#include "sqlite3.h" + +using namespace mozilla; +using namespace mozilla::storage; + +//////////////////////////////////////////////////////////////////////////////// +//// Helpers + +/** + * Commit hook to detect transactions. + * + * @param aArg + * An integer pointer that will be incremented for each commit. + */ +int commit_hook(void* aArg) { + int* arg = static_cast<int*>(aArg); + (*arg)++; + return 0; +} + +/** + * Executes the passed-in statements and checks if a transaction is created. + * When done statements are finalized and database connection is closed. + * + * @param aDB + * The database connection. + * @param aStmts + * Vector of statements. + * @param aStmtsLen + * Number of statements. + * @param aTransactionExpected + * Whether a transaction is expected or not. + */ +void check_transaction(mozIStorageConnection* aDB, + const nsTArray<RefPtr<mozIStorageBaseStatement>>& aStmts, + bool aTransactionExpected) { + // -- install a transaction commit hook. + int commit = 0; + static_cast<Connection*>(aDB)->setCommitHook(commit_hook, &commit); + + RefPtr<AsyncStatementSpinner> asyncSpin(new AsyncStatementSpinner()); + nsCOMPtr<mozIStoragePendingStatement> asyncPend; + do_check_success( + aDB->ExecuteAsync(aStmts, asyncSpin, getter_AddRefs(asyncPend))); + do_check_true(asyncPend); + + // -- complete the execution + asyncSpin->SpinUntilCompleted(); + + // -- uninstall the transaction commit hook. + static_cast<Connection*>(aDB)->setCommitHook(nullptr); + + // -- check transaction + do_check_eq(aTransactionExpected, !!commit); + + // -- check that only one transaction was created. + if (aTransactionExpected) { + do_check_eq(1, commit); + } + + // -- cleanup + for (uint32_t i = 0; i < aStmts.Length(); ++i) { + aStmts[i]->Finalize(); + } + blocking_async_close(aDB); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +/** + * Test that executing multiple readonly AsyncStatements doesn't create a + * transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleAsyncReadStatements) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt1; + db->CreateAsyncStatement("SELECT * FROM sqlite_master"_ns, + getter_AddRefs(stmt1)); + + nsCOMPtr<mozIStorageAsyncStatement> stmt2; + db->CreateAsyncStatement("SELECT * FROM sqlite_master"_ns, + getter_AddRefs(stmt2)); + + nsTArray<RefPtr<mozIStorageBaseStatement>> stmts = { + ToRefPtr(std::move(stmt1)), + ToRefPtr(std::move(stmt2)), + }; + + check_transaction(db, stmts.Clone(), false); +} + +/** + * Test that executing multiple readonly Statements doesn't create a + * transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleReadStatements) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt1; + db->CreateStatement("SELECT * FROM sqlite_master"_ns, getter_AddRefs(stmt1)); + + nsCOMPtr<mozIStorageStatement> stmt2; + db->CreateStatement("SELECT * FROM sqlite_master"_ns, getter_AddRefs(stmt2)); + + nsTArray<RefPtr<mozIStorageBaseStatement>> stmts = { + ToRefPtr(std::move(stmt1)), + ToRefPtr(std::move(stmt2)), + }; + + check_transaction(db, stmts, false); +} + +/** + * Test that executing multiple AsyncStatements causing writes creates a + * transaction. + */ +TEST(storage_asyncStatementExecution_transaction, + MultipleAsyncReadWriteStatements) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt1; + db->CreateAsyncStatement("SELECT * FROM sqlite_master"_ns, + getter_AddRefs(stmt1)); + + nsCOMPtr<mozIStorageAsyncStatement> stmt2; + db->CreateAsyncStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt2)); + + nsTArray<RefPtr<mozIStorageBaseStatement>> stmts = { + ToRefPtr(std::move(stmt1)), + ToRefPtr(std::move(stmt2)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing multiple Statements causing writes creates a transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleReadWriteStatements) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt1; + db->CreateStatement("SELECT * FROM sqlite_master"_ns, getter_AddRefs(stmt1)); + + nsCOMPtr<mozIStorageStatement> stmt2; + db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt2)); + + nsTArray<RefPtr<mozIStorageBaseStatement>> stmts = { + ToRefPtr(std::move(stmt1)), + ToRefPtr(std::move(stmt2)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing multiple AsyncStatements causing writes creates a + * single transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleAsyncWriteStatements) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt1; + db->CreateAsyncStatement("CREATE TABLE test1 (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt1)); + + nsCOMPtr<mozIStorageAsyncStatement> stmt2; + db->CreateAsyncStatement("CREATE TABLE test2 (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt2)); + + nsTArray<RefPtr<mozIStorageBaseStatement>> stmts = { + ToRefPtr(std::move(stmt1)), + ToRefPtr(std::move(stmt2)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing multiple Statements causing writes creates a + * single transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleWriteStatements) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt1; + db->CreateStatement("CREATE TABLE test1 (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt1)); + + nsCOMPtr<mozIStorageStatement> stmt2; + db->CreateStatement("CREATE TABLE test2 (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt2)); + + nsTArray<RefPtr<mozIStorageBaseStatement>> stmts = { + ToRefPtr(std::move(stmt1)), + ToRefPtr(std::move(stmt2)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing a single read-only AsyncStatement doesn't create a + * transaction. + */ +TEST(storage_asyncStatementExecution_transaction, SingleAsyncReadStatement) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement("SELECT * FROM sqlite_master"_ns, + getter_AddRefs(stmt)); + + nsTArray<RefPtr<mozIStorageBaseStatement>> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, false); +} + +/** + * Test that executing a single read-only Statement doesn't create a + * transaction. + */ +TEST(storage_asyncStatementExecution_transaction, SingleReadStatement) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt; + db->CreateStatement("SELECT * FROM sqlite_master"_ns, getter_AddRefs(stmt)); + + nsTArray<RefPtr<mozIStorageBaseStatement>> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, false); +} + +/** + * Test that executing a single AsyncStatement causing writes creates a + * transaction. + */ +TEST(storage_asyncStatementExecution_transaction, SingleAsyncWriteStatement) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt)); + + nsTArray<RefPtr<mozIStorageBaseStatement>> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing a single Statement causing writes creates a transaction. + */ +TEST(storage_asyncStatementExecution_transaction, SingleWriteStatement) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt; + db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt)); + + nsTArray<RefPtr<mozIStorageBaseStatement>> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing a single read-only AsyncStatement with multiple params + * doesn't create a transaction. + */ +TEST(storage_asyncStatementExecution_transaction, + MultipleParamsAsyncReadStatement) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement("SELECT :param FROM sqlite_master"_ns, + getter_AddRefs(stmt)); + + // -- bind multiple BindingParams + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (int32_t i = 0; i < 2; i++) { + nsCOMPtr<mozIStorageBindingParams> params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName("param"_ns, 1); + paramsArray->AddParams(params); + } + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + + nsTArray<RefPtr<mozIStorageBaseStatement>> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, false); +} + +/** + * Test that executing a single read-only Statement with multiple params + * doesn't create a transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleParamsReadStatement) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt; + db->CreateStatement("SELECT :param FROM sqlite_master"_ns, + getter_AddRefs(stmt)); + + // -- bind multiple BindingParams + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (int32_t i = 0; i < 2; i++) { + nsCOMPtr<mozIStorageBindingParams> params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName("param"_ns, 1); + paramsArray->AddParams(params); + } + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + + nsTArray<RefPtr<mozIStorageBaseStatement>> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, false); +} + +/** + * Test that executing a single write AsyncStatement with multiple params + * creates a transaction. + */ +TEST(storage_asyncStatementExecution_transaction, + MultipleParamsAsyncWriteStatement) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create a table for writes + nsCOMPtr<mozIStorageStatement> tableStmt; + db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(tableStmt)); + tableStmt->Execute(); + tableStmt->Finalize(); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement("DELETE FROM test WHERE id = :param"_ns, + getter_AddRefs(stmt)); + + // -- bind multiple BindingParams + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (int32_t i = 0; i < 2; i++) { + nsCOMPtr<mozIStorageBindingParams> params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName("param"_ns, 1); + paramsArray->AddParams(params); + } + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + + nsTArray<RefPtr<mozIStorageBaseStatement>> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing a single write Statement with multiple params + * creates a transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleParamsWriteStatement) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create a table for writes + nsCOMPtr<mozIStorageStatement> tableStmt; + db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(tableStmt)); + tableStmt->Execute(); + tableStmt->Finalize(); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt; + db->CreateStatement("DELETE FROM test WHERE id = :param"_ns, + getter_AddRefs(stmt)); + + // -- bind multiple BindingParams + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (int32_t i = 0; i < 2; i++) { + nsCOMPtr<mozIStorageBindingParams> params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName("param"_ns, 1); + paramsArray->AddParams(params); + } + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + + nsTArray<RefPtr<mozIStorageBaseStatement>> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, true); +} diff --git a/storage/test/gtest/test_async_callbacks_with_spun_event_loops.cpp b/storage/test/gtest/test_async_callbacks_with_spun_event_loops.cpp new file mode 100644 index 0000000000..bd437b61ec --- /dev/null +++ b/storage/test/gtest/test_async_callbacks_with_spun_event_loops.cpp @@ -0,0 +1,146 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +#include "storage_test_harness.h" +#include "prthread.h" +#include "nsIInterfaceRequestorUtils.h" +#include "mozilla/Attributes.h" + +#include "sqlite3.h" + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * Spins the events loop for current thread until aCondition is true. + */ +void spin_events_loop_until_true(const bool* const aCondition) { + nsCOMPtr<nsIThread> thread(::do_GetCurrentThread()); + nsresult rv = NS_OK; + bool processed = true; + while (!(*aCondition) && NS_SUCCEEDED(rv)) { + rv = thread->ProcessNextEvent(true, &processed); + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageStatementCallback implementation + +class UnownedCallback final : public mozIStorageStatementCallback { + public: + NS_DECL_ISUPPORTS + + // Whether the object has been destroyed. + static bool sAlive; + // Whether the first result was received. + static bool sResult; + // Whether an error was received. + static bool sError; + + explicit UnownedCallback(mozIStorageConnection* aDBConn) + : mDBConn(aDBConn), mCompleted(false) { + sAlive = true; + sResult = false; + sError = false; + } + + private: + ~UnownedCallback() { + sAlive = false; + blocking_async_close(mDBConn); + } + + public: + NS_IMETHOD HandleResult(mozIStorageResultSet* aResultSet) override { + sResult = true; + spin_events_loop_until_true(&mCompleted); + if (!sAlive) { + MOZ_CRASH("The statement callback was destroyed prematurely."); + } + return NS_OK; + } + + NS_IMETHOD HandleError(mozIStorageError* aError) override { + sError = true; + spin_events_loop_until_true(&mCompleted); + if (!sAlive) { + MOZ_CRASH("The statement callback was destroyed prematurely."); + } + return NS_OK; + } + + NS_IMETHOD HandleCompletion(uint16_t aReason) override { + mCompleted = true; + return NS_OK; + } + + protected: + nsCOMPtr<mozIStorageConnection> mDBConn; + bool mCompleted; +}; + +NS_IMPL_ISUPPORTS(UnownedCallback, mozIStorageStatementCallback) + +bool UnownedCallback::sAlive = false; +bool UnownedCallback::sResult = false; +bool UnownedCallback::sError = false; + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +TEST(storage_async_callbacks_with_spun_event_loops, + SpinEventsLoopInHandleResult) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create a test table and populate it. + nsCOMPtr<mozIStorageStatement> stmt; + db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt)); + stmt->Execute(); + stmt->Finalize(); + + db->CreateStatement("INSERT INTO test (id) VALUES (?)"_ns, + getter_AddRefs(stmt)); + for (int32_t i = 0; i < 30; ++i) { + stmt->BindInt32ByIndex(0, i); + stmt->Execute(); + stmt->Reset(); + } + stmt->Finalize(); + + db->CreateStatement("SELECT * FROM test"_ns, getter_AddRefs(stmt)); + nsCOMPtr<mozIStoragePendingStatement> ps; + do_check_success( + stmt->ExecuteAsync(new UnownedCallback(db), getter_AddRefs(ps))); + stmt->Finalize(); + + spin_events_loop_until_true(&UnownedCallback::sResult); +} + +TEST(storage_async_callbacks_with_spun_event_loops, SpinEventsLoopInHandleError) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create a test table and populate it. + nsCOMPtr<mozIStorageStatement> stmt; + db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt)); + stmt->Execute(); + stmt->Finalize(); + + db->CreateStatement("INSERT INTO test (id) VALUES (1)"_ns, + getter_AddRefs(stmt)); + stmt->Execute(); + stmt->Finalize(); + + // This will cause a constraint error. + db->CreateStatement("INSERT INTO test (id) VALUES (1)"_ns, + getter_AddRefs(stmt)); + nsCOMPtr<mozIStoragePendingStatement> ps; + do_check_success( + stmt->ExecuteAsync(new UnownedCallback(db), getter_AddRefs(ps))); + stmt->Finalize(); + + spin_events_loop_until_true(&UnownedCallback::sError); +} diff --git a/storage/test/gtest/test_binding_params.cpp b/storage/test/gtest/test_binding_params.cpp new file mode 100644 index 0000000000..a9ca1799f9 --- /dev/null +++ b/storage/test/gtest/test_binding_params.cpp @@ -0,0 +1,186 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "mozilla/ArrayUtils.h" +#include "mozStorageHelper.h" + +using namespace mozilla; + +/** + * This file tests binding and reading out string parameters through the + * mozIStorageStatement API. + */ + +TEST(storage_binding_params, ASCIIString) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create table with a single string column. + (void)db->ExecuteSimpleSQL("CREATE TABLE test (str STRING)"_ns); + + // Create statements to INSERT and SELECT the string. + nsCOMPtr<mozIStorageStatement> insert, select; + (void)db->CreateStatement("INSERT INTO test (str) VALUES (?1)"_ns, + getter_AddRefs(insert)); + (void)db->CreateStatement("SELECT str FROM test"_ns, getter_AddRefs(select)); + + // Roundtrip a string through the table, and ensure it comes out as expected. + nsAutoCString inserted("I'm an ASCII string"); + { + mozStorageStatementScoper scoper(insert); + bool hasResult; + do_check_true(NS_SUCCEEDED(insert->BindUTF8StringByIndex(0, inserted))); + do_check_true(NS_SUCCEEDED(insert->ExecuteStep(&hasResult))); + do_check_false(hasResult); + } + + nsAutoCString result; + { + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetUTF8String(0, result))); + } + + do_check_true(result == inserted); + + (void)db->ExecuteSimpleSQL("DELETE FROM test"_ns); +} + +TEST(storage_binding_params, CString) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create table with a single string column. + (void)db->ExecuteSimpleSQL("CREATE TABLE test (str STRING)"_ns); + + // Create statements to INSERT and SELECT the string. + nsCOMPtr<mozIStorageStatement> insert, select; + (void)db->CreateStatement("INSERT INTO test (str) VALUES (?1)"_ns, + getter_AddRefs(insert)); + (void)db->CreateStatement("SELECT str FROM test"_ns, getter_AddRefs(select)); + + // Roundtrip a string through the table, and ensure it comes out as expected. + static const char sCharArray[] = + "I'm not a \xff\x00\xac\xde\xbb ASCII string!"; + nsAutoCString inserted(sCharArray, ArrayLength(sCharArray) - 1); + do_check_true(inserted.Length() == ArrayLength(sCharArray) - 1); + { + mozStorageStatementScoper scoper(insert); + bool hasResult; + do_check_true(NS_SUCCEEDED(insert->BindUTF8StringByIndex(0, inserted))); + do_check_true(NS_SUCCEEDED(insert->ExecuteStep(&hasResult))); + do_check_false(hasResult); + } + + { + nsAutoCString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetUTF8String(0, result))); + + do_check_true(result == inserted); + } + + (void)db->ExecuteSimpleSQL("DELETE FROM test"_ns); +} + +TEST(storage_binding_params, UTFStrings) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create table with a single string column. + (void)db->ExecuteSimpleSQL("CREATE TABLE test (str STRING)"_ns); + + // Create statements to INSERT and SELECT the string. + nsCOMPtr<mozIStorageStatement> insert, select; + (void)db->CreateStatement("INSERT INTO test (str) VALUES (?1)"_ns, + getter_AddRefs(insert)); + (void)db->CreateStatement("SELECT str FROM test"_ns, getter_AddRefs(select)); + + // Roundtrip a UTF8 string through the table, using UTF8 input and output. + static const char sCharArray[] = R"(I'm a ûüâäç UTF8 string!)"; + nsAutoCString insertedUTF8(sCharArray, ArrayLength(sCharArray) - 1); + do_check_true(insertedUTF8.Length() == ArrayLength(sCharArray) - 1); + NS_ConvertUTF8toUTF16 insertedUTF16(insertedUTF8); + do_check_true(insertedUTF8 == NS_ConvertUTF16toUTF8(insertedUTF16)); + { + mozStorageStatementScoper scoper(insert); + bool hasResult; + do_check_true(NS_SUCCEEDED(insert->BindUTF8StringByIndex(0, insertedUTF8))); + do_check_true(NS_SUCCEEDED(insert->ExecuteStep(&hasResult))); + do_check_false(hasResult); + } + + { + nsAutoCString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetUTF8String(0, result))); + + do_check_true(result == insertedUTF8); + } + + // Use UTF8 input and UTF16 output. + { + nsAutoString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetString(0, result))); + + do_check_true(result == insertedUTF16); + } + + (void)db->ExecuteSimpleSQL("DELETE FROM test"_ns); + + // Roundtrip the same string using UTF16 input and UTF8 output. + { + mozStorageStatementScoper scoper(insert); + bool hasResult; + do_check_true(NS_SUCCEEDED(insert->BindStringByIndex(0, insertedUTF16))); + do_check_true(NS_SUCCEEDED(insert->ExecuteStep(&hasResult))); + do_check_false(hasResult); + } + + { + nsAutoCString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetUTF8String(0, result))); + + do_check_true(result == insertedUTF8); + } + + // Use UTF16 input and UTF16 output. + { + nsAutoString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetString(0, result))); + + do_check_true(result == insertedUTF16); + } + + (void)db->ExecuteSimpleSQL("DELETE FROM test"_ns); +} diff --git a/storage/test/gtest/test_deadlock_detector.cpp b/storage/test/gtest/test_deadlock_detector.cpp new file mode 100644 index 0000000000..2679cf26f1 --- /dev/null +++ b/storage/test/gtest/test_deadlock_detector.cpp @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: sw=2 ts=4 et : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Note: This file is essentially a copy of +// xpcom/tests/gtest/TestDeadlockDetector.cpp, but all mutexes were turned into +// SQLiteMutexes. We use #include and some macros to avoid actual source code +// duplication. + +#include "mozilla/CondVar.h" +#include "mozilla/RecursiveMutex.h" +#include "mozilla/ReentrantMonitor.h" +#include "SQLiteMutex.h" + +// We need this one so _gdb_sleep_duration is also in "storage" namespace +#include "mozilla/gtest/MozHelpers.h" + +#include "gtest/gtest.h" + +using namespace mozilla; + +/** + * Helper class to allocate a sqlite3_mutex for our SQLiteMutex. + */ +class TestMutex : public mozilla::storage::SQLiteMutex { + public: + explicit TestMutex(const char* aName) + : mozilla::storage::SQLiteMutex(aName), + mInner(sqlite3_mutex_alloc(SQLITE_MUTEX_FAST)) { + NS_ASSERTION(mInner, "could not allocate a sqlite3_mutex"); + initWithMutex(mInner); + } + + ~TestMutex() { sqlite3_mutex_free(mInner); } + + void Lock() { lock(); } + + void Unlock() { unlock(); } + + private: + sqlite3_mutex* mInner; +}; + +// These are the two macros that differentiate this file from the XPCOM one. +#define MUTEX TestMutex +#define TESTNAME(name) storage_##name + +// Bug 1473531: the test storage_DeadlockDetectorTest.storage_Sanity5DeathTest +// times out on macosx ccov builds +#if defined(XP_MACOSX) && defined(MOZ_CODE_COVERAGE) +# define DISABLE_STORAGE_SANITY5_DEATH_TEST +#endif + +#include "../../../xpcom/tests/gtest/TestDeadlockDetector.cpp" diff --git a/storage/test/gtest/test_file_perms.cpp b/storage/test/gtest/test_file_perms.cpp new file mode 100644 index 0000000000..5e54f082ce --- /dev/null +++ b/storage/test/gtest/test_file_perms.cpp @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" +#include "nsIFile.h" +#include "prio.h" + +/** + * This file tests that the file permissions of the sqlite files match what + * we request they be + */ + +TEST(storage_file_perms, Test) +{ + nsCOMPtr<mozIStorageConnection> db(getDatabase()); + nsCOMPtr<nsIFile> dbFile; + do_check_success(db->GetDatabaseFile(getter_AddRefs(dbFile))); + + uint32_t perms = 0; + do_check_success(dbFile->GetPermissions(&perms)); + + // This reflexts the permissions defined by SQLITE_DEFAULT_FILE_PERMISSIONS in + // third_party/sqlite3/src/Makefile.in and must be kept in sync with that +#ifdef ANDROID + do_check_true(perms == (PR_IRUSR | PR_IWUSR)); +#elif defined(XP_WIN) + do_check_true(perms == (PR_IRUSR | PR_IWUSR | PR_IRGRP | PR_IWGRP | PR_IROTH | + PR_IWOTH)); +#else + do_check_true(perms == (PR_IRUSR | PR_IWUSR | PR_IRGRP | PR_IROTH)); +#endif +} diff --git a/storage/test/gtest/test_interruptSynchronousConnection.cpp b/storage/test/gtest/test_interruptSynchronousConnection.cpp new file mode 100644 index 0000000000..dfa19bc86e --- /dev/null +++ b/storage/test/gtest/test_interruptSynchronousConnection.cpp @@ -0,0 +1,81 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "storage_test_harness.h" + +#include "mozilla/SpinEventLoopUntil.h" + +class SynchronousConnectionInterruptionTest : public ::testing::Test { + protected: + void SetUp() override { + mConnection = + getDatabase(nullptr, mozIStorageService::CONNECTION_INTERRUPTIBLE); + ASSERT_TRUE(mConnection); + + ASSERT_EQ(NS_OK, NS_NewNamedThread("Test Thread", getter_AddRefs(mThread))); + } + + void TearDown() override { + // We might close the database connection early in test cases. + mozilla::Unused << mConnection->Close(); + + ASSERT_EQ(NS_OK, mThread->Shutdown()); + } + + nsCOMPtr<mozIStorageConnection> mConnection; + + nsCOMPtr<nsIThread> mThread; + + bool mDone = false; +}; + +TEST_F(SynchronousConnectionInterruptionTest, + shouldBeAbleToInterruptInfiniteOperation) { + // Delay is modest because we don't want to get interrupted by + // some unrelated hang or memory guard + const uint32_t delayMs = 500; + + ASSERT_EQ(NS_OK, mThread->DelayedDispatch( + NS_NewRunnableFunction( + "InterruptRunnable", + [this]() { + ASSERT_EQ(NS_OK, mConnection->Interrupt()); + mDone = true; + }), + delayMs)); + + const nsCString infiniteQuery = + "WITH RECURSIVE test(n) " + "AS (VALUES(1) UNION ALL SELECT n + 1 FROM test) " + "SELECT t.n FROM test, test AS t;"_ns; + nsCOMPtr<mozIStorageStatement> stmt; + ASSERT_EQ(NS_OK, + mConnection->CreateStatement(infiniteQuery, getter_AddRefs(stmt))); + ASSERT_EQ(NS_ERROR_ABORT, stmt->Execute()); + ASSERT_EQ(NS_OK, stmt->Finalize()); + + ASSERT_TRUE(mDone); + + ASSERT_EQ(NS_OK, mConnection->Close()); +} + +TEST_F(SynchronousConnectionInterruptionTest, interruptAfterCloseWillFail) { + ASSERT_EQ(NS_OK, mConnection->Close()); + + ASSERT_EQ( + NS_OK, + mThread->Dispatch(NS_NewRunnableFunction("InterruptRunnable", [this]() { + ASSERT_EQ(NS_ERROR_NOT_INITIALIZED, mConnection->Interrupt()); + mDone = true; + }))); + + ASSERT_TRUE(mozilla::SpinEventLoopUntil("interruptAfterCloseWillFail"_ns, + [this]() { return mDone; })); + + ASSERT_EQ(NS_ERROR_NOT_INITIALIZED, mConnection->Close()); +} diff --git a/storage/test/gtest/test_mutex.cpp b/storage/test/gtest/test_mutex.cpp new file mode 100644 index 0000000000..db1a645e09 --- /dev/null +++ b/storage/test/gtest/test_mutex.cpp @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "SQLiteMutex.h" + +using namespace mozilla; +using namespace mozilla::storage; + +/** + * This file test our sqlite3_mutex wrapper in SQLiteMutex.h. + */ + +TEST(storage_mutex, AutoLock) +{ + int lockTypes[] = { + SQLITE_MUTEX_FAST, + SQLITE_MUTEX_RECURSIVE, + }; + for (int lockType : lockTypes) { + // Get our test mutex (we have to allocate a SQLite mutex of the right type + // too!). + SQLiteMutex mutex("TestMutex"); + sqlite3_mutex* inner = sqlite3_mutex_alloc(lockType); + do_check_true(inner); + mutex.initWithMutex(inner); + + // And test that our automatic locking wrapper works as expected. + mutex.assertNotCurrentThreadOwns(); + { + SQLiteMutexAutoLock lockedScope(mutex); + mutex.assertCurrentThreadOwns(); + } + mutex.assertNotCurrentThreadOwns(); + + // Free the wrapped mutex - we don't need it anymore. + sqlite3_mutex_free(inner); + } +} + +TEST(storage_mutex, AutoUnlock) +{ + int lockTypes[] = { + SQLITE_MUTEX_FAST, + SQLITE_MUTEX_RECURSIVE, + }; + for (int lockType : lockTypes) { + // Get our test mutex (we have to allocate a SQLite mutex of the right type + // too!). + SQLiteMutex mutex("TestMutex"); + sqlite3_mutex* inner = sqlite3_mutex_alloc(lockType); + do_check_true(inner); + mutex.initWithMutex(inner); + + // And test that our automatic unlocking wrapper works as expected. + { + SQLiteMutexAutoLock lockedScope(mutex); + + { + SQLiteMutexAutoUnlock unlockedScope(mutex); + mutex.assertNotCurrentThreadOwns(); + } + mutex.assertCurrentThreadOwns(); + } + + // Free the wrapped mutex - we don't need it anymore. + sqlite3_mutex_free(inner); + } +} diff --git a/storage/test/gtest/test_spinningSynchronousClose.cpp b/storage/test/gtest/test_spinningSynchronousClose.cpp new file mode 100644 index 0000000000..81f9bee11a --- /dev/null +++ b/storage/test/gtest/test_spinningSynchronousClose.cpp @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" +#include "prinrval.h" + +using namespace mozilla; + +/** + * Helper to verify that the event loop was spun. As long as this is dispatched + * prior to a call to Close()/SpinningSynchronousClose() we are guaranteed this + * will be run if the event loop is spun to perform a close. This is because + * SpinningSynchronousClose must spin the event loop to realize the close + * completed and our runnable will already be enqueued and therefore run before + * the AsyncCloseConnection's callback. Note that this invariant may be + * violated if our runnables end up in different queues thanks to Quantum + * changes, so this test may need to be updated if the close dispatch changes. + */ +class CompletionRunnable final : public Runnable { + public: + explicit CompletionRunnable() + : Runnable("CompletionRunnable"), mDone(false) {} + + NS_IMETHOD Run() override { + mDone = true; + return NS_OK; + } + + bool mDone; +}; + +// Can only run in optimized builds, or it would assert. +#ifndef DEBUG +TEST(storage_spinningSynchronousClose, CloseOnAsync) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + // Run an async statement. + nsCOMPtr<mozIStorageAsyncStatement> stmt; + do_check_success(db->CreateAsyncStatement( + "CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, getter_AddRefs(stmt))); + nsCOMPtr<mozIStoragePendingStatement> p; + do_check_success(stmt->ExecuteAsync(nullptr, getter_AddRefs(p))); + do_check_success(stmt->Finalize()); + + // Wrongly use Close() instead of AsyncClose(). + RefPtr<CompletionRunnable> event = new CompletionRunnable(); + NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); + do_check_false(NS_SUCCEEDED(db->Close())); + do_check_true(event->mDone); +} +#endif + +TEST(storage_spinningSynchronousClose, spinningSynchronousCloseOnAsync) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + // Run an async statement. + nsCOMPtr<mozIStorageAsyncStatement> stmt; + do_check_success(db->CreateAsyncStatement( + "CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, getter_AddRefs(stmt))); + nsCOMPtr<mozIStoragePendingStatement> p; + do_check_success(stmt->ExecuteAsync(nullptr, getter_AddRefs(p))); + do_check_success(stmt->Finalize()); + + // Use the spinning close API. + RefPtr<CompletionRunnable> event = new CompletionRunnable(); + NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); + do_check_success(db->SpinningSynchronousClose()); + do_check_true(event->mDone); +} diff --git a/storage/test/gtest/test_statement_scoper.cpp b/storage/test/gtest/test_statement_scoper.cpp new file mode 100644 index 0000000000..3327b0ff6b --- /dev/null +++ b/storage/test/gtest/test_statement_scoper.cpp @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "mozStorageHelper.h" + +/** + * This file test our statement scoper in mozStorageHelper.h. + */ + +TEST(storage_statement_scoper, automatic_reset) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Need to create a table to populate sqlite_master with an entry. + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + + nsCOMPtr<mozIStorageStatement> stmt; + (void)db->CreateStatement("SELECT * FROM sqlite_master"_ns, + getter_AddRefs(stmt)); + + // Reality check - make sure we start off in the right state. + int32_t state = -1; + (void)stmt->GetState(&state); + do_check_true(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_READY); + + // Start executing the statement, which will put it into an executing state. + { + mozStorageStatementScoper scoper(stmt); + bool hasMore; + do_check_true(NS_SUCCEEDED(stmt->ExecuteStep(&hasMore))); + + // Reality check that we are executing. + state = -1; + (void)stmt->GetState(&state); + do_check_true(state == + mozIStorageStatement::MOZ_STORAGE_STATEMENT_EXECUTING); + } + + // And we should be ready again. + state = -1; + (void)stmt->GetState(&state); + do_check_true(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_READY); +} + +TEST(storage_statement_scoper, Abandon) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Need to create a table to populate sqlite_master with an entry. + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + + nsCOMPtr<mozIStorageStatement> stmt; + (void)db->CreateStatement("SELECT * FROM sqlite_master"_ns, + getter_AddRefs(stmt)); + + // Reality check - make sure we start off in the right state. + int32_t state = -1; + (void)stmt->GetState(&state); + do_check_true(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_READY); + + // Start executing the statement, which will put it into an executing state. + { + mozStorageStatementScoper scoper(stmt); + bool hasMore; + do_check_true(NS_SUCCEEDED(stmt->ExecuteStep(&hasMore))); + + // Reality check that we are executing. + state = -1; + (void)stmt->GetState(&state); + do_check_true(state == + mozIStorageStatement::MOZ_STORAGE_STATEMENT_EXECUTING); + + // And call Abandon. We should not reset now when we fall out of scope. + scoper.Abandon(); + } + + // And we should still be executing. + state = -1; + (void)stmt->GetState(&state); + do_check_true(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_EXECUTING); +} diff --git a/storage/test/gtest/test_transaction_helper.cpp b/storage/test/gtest/test_transaction_helper.cpp new file mode 100644 index 0000000000..d11f631967 --- /dev/null +++ b/storage/test/gtest/test_transaction_helper.cpp @@ -0,0 +1,184 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "mozStorageHelper.h" +#include "mozStorageConnection.h" + +using namespace mozilla; +using namespace mozilla::storage; + +bool has_transaction(mozIStorageConnection* aDB) { + return !(static_cast<Connection*>(aDB)->getAutocommit()); +} + +/** + * This file tests our Transaction helper in mozStorageHelper.h. + */ + +TEST(storage_transaction_helper, Commit) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create a table in a transaction, call Commit, and make sure that it does + // exists after the transaction falls out of scope. + { + mozStorageTransaction transaction(db, false); + do_check_success(transaction.Start()); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + (void)transaction.Commit(); + } + do_check_false(has_transaction(db)); + + bool exists = false; + (void)db->TableExists("test"_ns, &exists); + do_check_true(exists); +} + +TEST(storage_transaction_helper, Rollback) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create a table in a transaction, call Rollback, and make sure that it does + // not exists after the transaction falls out of scope. + { + mozStorageTransaction transaction(db, true); + do_check_success(transaction.Start()); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + (void)transaction.Rollback(); + } + do_check_false(has_transaction(db)); + + bool exists = true; + (void)db->TableExists("test"_ns, &exists); + do_check_false(exists); +} + +TEST(storage_transaction_helper, AutoCommit) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create a table in a transaction, and make sure that it exists after the + // transaction falls out of scope. This means the Commit was successful. + { + mozStorageTransaction transaction(db, true); + do_check_success(transaction.Start()); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + } + do_check_false(has_transaction(db)); + + bool exists = false; + (void)db->TableExists("test"_ns, &exists); + do_check_true(exists); +} + +TEST(storage_transaction_helper, AutoRollback) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create a table in a transaction, and make sure that it does not exists + // after the transaction falls out of scope. This means the Rollback was + // successful. + { + mozStorageTransaction transaction(db, false); + do_check_success(transaction.Start()); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + } + do_check_false(has_transaction(db)); + + bool exists = true; + (void)db->TableExists("test"_ns, &exists); + do_check_false(exists); +} + +TEST(storage_transaction_helper, null_database_connection) +{ + // We permit the use of the Transaction helper when passing a null database + // in, so we need to make sure this still works without crashing. + mozStorageTransaction transaction(nullptr, false); + do_check_success(transaction.Start()); + do_check_true(NS_SUCCEEDED(transaction.Commit())); + do_check_true(NS_SUCCEEDED(transaction.Rollback())); +} + +TEST(storage_transaction_helper, async_Commit) +{ + HookSqliteMutex hook; + + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- wedge the thread + nsCOMPtr<nsIThread> target(get_conn_async_thread(db)); + do_check_true(target); + RefPtr<ThreadWedger> wedger(new ThreadWedger(target)); + + { + mozStorageTransaction transaction( + db, false, mozIStorageConnection::TRANSACTION_DEFERRED, true); + do_check_success(transaction.Start()); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + (void)transaction.Commit(); + } + do_check_true(has_transaction(db)); + + // -- unwedge the async thread + wedger->unwedge(); + + // Ensure the transaction has done its job by enqueueing an async execution. + nsCOMPtr<mozIStorageAsyncStatement> stmt; + (void)db->CreateAsyncStatement("SELECT NULL"_ns, getter_AddRefs(stmt)); + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(has_transaction(db)); + bool exists = false; + (void)db->TableExists("test"_ns, &exists); + do_check_true(exists); + + blocking_async_close(db); +} + +TEST(storage_transaction_helper, Nesting) +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + { + mozStorageTransaction transaction(db, false); + do_check_success(transaction.Start()); + do_check_true(has_transaction(db)); + do_check_success( + db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns)); + + { + mozStorageTransaction nestedTransaction(db, false); + do_check_success(nestedTransaction.Start()); + do_check_true(has_transaction(db)); + do_check_success(db->ExecuteSimpleSQL( + "CREATE TABLE nested_test (id INTEGER PRIMARY KEY)"_ns)); + +#ifndef MOZ_DIAGNOSTIC_ASSERT_ENABLED + do_check_true(transaction.Commit() == NS_ERROR_NOT_AVAILABLE); + do_check_true(transaction.Rollback() == NS_ERROR_NOT_AVAILABLE); +#endif + } + + bool exists = false; + do_check_success(db->TableExists("nested_test"_ns, &exists)); + do_check_false(exists); + + (void)transaction.Commit(); + } + do_check_false(has_transaction(db)); + + bool exists = false; + do_check_success(db->TableExists("test"_ns, &exists)); + do_check_true(exists); +} diff --git a/storage/test/gtest/test_true_async.cpp b/storage/test/gtest/test_true_async.cpp new file mode 100644 index 0000000000..3b54b73b3a --- /dev/null +++ b/storage/test/gtest/test_true_async.cpp @@ -0,0 +1,161 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +TEST(storage_true_async, TrueAsyncStatement) +{ + HookSqliteMutex hook; + + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Start watching for forbidden mutex usage. + watch_for_mutex_use_on_this_thread(); + + // - statement with nothing to bind + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt)); + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(mutex_used_on_watched_thread); + + // - statement with something to bind ordinally + db->CreateAsyncStatement("INSERT INTO test (id) VALUES (?)"_ns, + getter_AddRefs(stmt)); + stmt->BindInt32ByIndex(0, 1); + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(mutex_used_on_watched_thread); + + // - statement with something to bind by name + db->CreateAsyncStatement("INSERT INTO test (id) VALUES (:id)"_ns, + getter_AddRefs(stmt)); + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + nsCOMPtr<mozIStorageBindingParams> params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName("id"_ns, 2); + paramsArray->AddParams(params); + params = nullptr; + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(mutex_used_on_watched_thread); + + // - now, make sure creating a sync statement does trigger our guard. + // (If this doesn't happen, our test is bunk and it's important to know that.) + nsCOMPtr<mozIStorageStatement> syncStmt; + db->CreateStatement("SELECT * FROM test"_ns, getter_AddRefs(syncStmt)); + syncStmt->Finalize(); + do_check_true(mutex_used_on_watched_thread); + + blocking_async_close(db); +} + +/** + * Test that cancellation before a statement is run successfully stops the + * statement from executing. + */ +TEST(storage_true_async, AsyncCancellation) +{ + HookSqliteMutex hook; + + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- wedge the thread + nsCOMPtr<nsIThread> target(get_conn_async_thread(db)); + do_check_true(target); + RefPtr<ThreadWedger> wedger(new ThreadWedger(target)); + + // -- create statements and cancel them + // - async + nsCOMPtr<mozIStorageAsyncStatement> asyncStmt; + db->CreateAsyncStatement( + "CREATE TABLE asyncTable (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(asyncStmt)); + + RefPtr<AsyncStatementSpinner> asyncSpin(new AsyncStatementSpinner()); + nsCOMPtr<mozIStoragePendingStatement> asyncPend; + (void)asyncStmt->ExecuteAsync(asyncSpin, getter_AddRefs(asyncPend)); + do_check_true(asyncPend); + asyncPend->Cancel(); + + // - sync + nsCOMPtr<mozIStorageStatement> syncStmt; + db->CreateStatement("CREATE TABLE syncTable (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(syncStmt)); + + RefPtr<AsyncStatementSpinner> syncSpin(new AsyncStatementSpinner()); + nsCOMPtr<mozIStoragePendingStatement> syncPend; + (void)syncStmt->ExecuteAsync(syncSpin, getter_AddRefs(syncPend)); + do_check_true(syncPend); + syncPend->Cancel(); + + // -- unwedge the async thread + wedger->unwedge(); + + // -- verify that both statements report they were canceled + asyncSpin->SpinUntilCompleted(); + do_check_true(asyncSpin->completionReason == + mozIStorageStatementCallback::REASON_CANCELED); + + syncSpin->SpinUntilCompleted(); + do_check_true(syncSpin->completionReason == + mozIStorageStatementCallback::REASON_CANCELED); + + // -- verify that neither statement constructed their tables + nsresult rv; + bool exists; + rv = db->TableExists("asyncTable"_ns, &exists); + do_check_true(rv == NS_OK); + do_check_false(exists); + rv = db->TableExists("syncTable"_ns, &exists); + do_check_true(rv == NS_OK); + do_check_false(exists); + + // -- cleanup + asyncStmt->Finalize(); + syncStmt->Finalize(); + blocking_async_close(db); +} + +/** + * Test that the destructor for an asynchronous statement which has a + * sqlite3_stmt will dispatch that statement to the async thread for + * finalization rather than trying to finalize it on the main thread + * (and thereby running afoul of our mutex use detector). + */ +TEST(storage_true_async, AsyncDestructorFinalizesOnAsyncThread) +{ + HookSqliteMutex hook; + + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + watch_for_mutex_use_on_this_thread(); + + // -- create an async statement + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt)); + + // -- execute it so it gets a sqlite3_stmt that needs to be finalized + blocking_async_execute(stmt); + do_check_false(mutex_used_on_watched_thread); + + // -- forget our reference + stmt = nullptr; + + // -- verify the mutex was not touched + do_check_false(mutex_used_on_watched_thread); + + // -- make sure the statement actually gets finalized / cleanup + // the close will assert if we failed to finalize! + blocking_async_close(db); +} diff --git a/storage/test/gtest/test_unlock_notify.cpp b/storage/test/gtest/test_unlock_notify.cpp new file mode 100644 index 0000000000..93cc5b09a8 --- /dev/null +++ b/storage/test/gtest/test_unlock_notify.cpp @@ -0,0 +1,234 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim set:sw=2 ts=2 et lcs=trail\:.,tab\:>~ : */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "mozilla/ReentrantMonitor.h" +#include "nsThreadUtils.h" +#include "mozIStorageStatement.h" + +/** + * This file tests that our implementation around sqlite3_unlock_notify works + * as expected. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Helpers + +enum State { STARTING, WRITE_LOCK, READ_LOCK, TEST_DONE }; + +class DatabaseLocker : public mozilla::Runnable { + public: + explicit DatabaseLocker(const char* aSQL, nsIFile* aDBFile = nullptr) + : mozilla::Runnable("DatabaseLocker"), + monitor("DatabaseLocker::monitor"), + mSQL(aSQL), + mState(STARTING), + mDBFile(aDBFile) {} + + void RunInBackground() { + (void)NS_NewNamedThread("DatabaseLocker", getter_AddRefs(mThread)); + do_check_true(mThread); + + do_check_success(mThread->Dispatch(this, NS_DISPATCH_NORMAL)); + } + + void Shutdown() { + if (mThread) { + mThread->Shutdown(); + } + } + + NS_IMETHOD Run() override { + mozilla::ReentrantMonitorAutoEnter lock(monitor); + + nsCOMPtr<mozIStorageConnection> db(getDatabase(mDBFile)); + + nsCString sql(mSQL); + nsCOMPtr<mozIStorageStatement> stmt; + do_check_success(db->CreateStatement(sql, getter_AddRefs(stmt))); + + bool hasResult; + do_check_success(stmt->ExecuteStep(&hasResult)); + + Notify(WRITE_LOCK); + WaitFor(TEST_DONE); + + return NS_OK; + } + + void WaitFor(State aState) { + monitor.AssertCurrentThreadIn(); + while (mState != aState) { + do_check_success(monitor.Wait()); + } + } + + void Notify(State aState) { + monitor.AssertCurrentThreadIn(); + mState = aState; + do_check_success(monitor.Notify()); + } + + mozilla::ReentrantMonitor monitor MOZ_UNANNOTATED; + + protected: + nsCOMPtr<nsIThread> mThread; + const char* const mSQL; + State mState; + nsCOMPtr<nsIFile> mDBFile; +}; + +class DatabaseTester : public DatabaseLocker { + public: + DatabaseTester(mozIStorageConnection* aConnection, const char* aSQL) + : DatabaseLocker(aSQL), mConnection(aConnection) {} + + NS_IMETHOD Run() override { + mozilla::ReentrantMonitorAutoEnter lock(monitor); + WaitFor(READ_LOCK); + + nsCString sql(mSQL); + nsCOMPtr<mozIStorageStatement> stmt; + do_check_success(mConnection->CreateStatement(sql, getter_AddRefs(stmt))); + + bool hasResult; + nsresult rv = stmt->ExecuteStep(&hasResult); + do_check_eq(rv, NS_ERROR_FILE_IS_LOCKED); + + // Finalize our statement and null out our connection before notifying to + // ensure that we close on the proper thread. + rv = stmt->Finalize(); + do_check_eq(rv, NS_ERROR_FILE_IS_LOCKED); + mConnection = nullptr; + + Notify(TEST_DONE); + + return NS_OK; + } + + private: + nsCOMPtr<mozIStorageConnection> mConnection; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Test Functions + +void setup() { + nsCOMPtr<mozIStorageConnection> db(getDatabase()); + + // Create and populate a dummy table. + nsresult rv = db->ExecuteSimpleSQL(nsLiteralCString( + "CREATE TABLE test (id INTEGER PRIMARY KEY, data STRING)")); + do_check_success(rv); + rv = db->ExecuteSimpleSQL("INSERT INTO test (data) VALUES ('foo')"_ns); + do_check_success(rv); + rv = db->ExecuteSimpleSQL("INSERT INTO test (data) VALUES ('bar')"_ns); + do_check_success(rv); + rv = + db->ExecuteSimpleSQL("CREATE UNIQUE INDEX unique_data ON test (data)"_ns); + do_check_success(rv); +} + +void test_step_locked_does_not_block_main_thread() { + nsCOMPtr<mozIStorageConnection> db(getDatabase()); + + // Need to prepare our statement ahead of time so we make sure to only test + // step and not prepare. + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = db->CreateStatement( + "INSERT INTO test (data) VALUES ('test1')"_ns, getter_AddRefs(stmt)); + do_check_success(rv); + + nsCOMPtr<nsIFile> dbFile; + db->GetDatabaseFile(getter_AddRefs(dbFile)); + RefPtr<DatabaseLocker> locker( + new DatabaseLocker("SELECT * FROM test", dbFile)); + do_check_true(locker); + { + mozilla::ReentrantMonitorAutoEnter lock(locker->monitor); + locker->RunInBackground(); + + // Wait for the locker to notify us that it has locked the database + // properly. + locker->WaitFor(WRITE_LOCK); + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + do_check_eq(rv, NS_ERROR_FILE_IS_LOCKED); + + locker->Notify(TEST_DONE); + } + locker->Shutdown(); +} + +void test_drop_index_does_not_loop() { + nsCOMPtr<mozIStorageConnection> db(getDatabase()); + + // Need to prepare our statement ahead of time so we make sure to only test + // step and not prepare. + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = + db->CreateStatement("SELECT * FROM test"_ns, getter_AddRefs(stmt)); + do_check_success(rv); + + RefPtr<DatabaseTester> tester = + new DatabaseTester(db, "DROP INDEX unique_data"); + do_check_true(tester); + { + mozilla::ReentrantMonitorAutoEnter lock(tester->monitor); + tester->RunInBackground(); + + // Hold a read lock on the database, and then let the tester try to execute. + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + do_check_success(rv); + do_check_true(hasResult); + tester->Notify(READ_LOCK); + + // Make sure the tester finishes its test before we move on. + tester->WaitFor(TEST_DONE); + } + tester->Shutdown(); +} + +void test_drop_table_does_not_loop() { + nsCOMPtr<mozIStorageConnection> db(getDatabase()); + + // Need to prepare our statement ahead of time so we make sure to only test + // step and not prepare. + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = + db->CreateStatement("SELECT * FROM test"_ns, getter_AddRefs(stmt)); + do_check_success(rv); + + RefPtr<DatabaseTester> tester(new DatabaseTester(db, "DROP TABLE test")); + do_check_true(tester); + { + mozilla::ReentrantMonitorAutoEnter lock(tester->monitor); + tester->RunInBackground(); + + // Hold a read lock on the database, and then let the tester try to execute. + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + do_check_success(rv); + do_check_true(hasResult); + tester->Notify(READ_LOCK); + + // Make sure the tester finishes its test before we move on. + tester->WaitFor(TEST_DONE); + } + tester->Shutdown(); +} + +TEST(storage_unlock_notify, Test) +{ + // These must execute in order. + setup(); + test_step_locked_does_not_block_main_thread(); + test_drop_index_does_not_loop(); + test_drop_table_does_not_loop(); +} diff --git a/storage/test/moz.build b/storage/test/moz.build new file mode 100644 index 0000000000..7265db4d7b --- /dev/null +++ b/storage/test/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"] + +TEST_DIRS += ["gtest"] + +TESTING_JS_MODULES += [ + "unit/VacuumParticipant.sys.mjs", +] diff --git a/storage/test/unit/VacuumParticipant.sys.mjs b/storage/test/unit/VacuumParticipant.sys.mjs new file mode 100644 index 0000000000..9f1c39826e --- /dev/null +++ b/storage/test/unit/VacuumParticipant.sys.mjs @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This testing component is used in test_vacuum* tests. + +const CAT_NAME = "vacuum-participant"; +const CONTRACT_ID = "@unit.test.com/test-vacuum-participant;1"; + +import { MockRegistrar } from "resource://testing-common/MockRegistrar.sys.mjs"; +import { TestUtils } from "resource://testing-common/TestUtils.sys.mjs"; + +export class VacuumParticipant { + #dbConn; + #expectedPageSize = 0; + #useIncrementalVacuum = false; + #grant = false; + + /** + * Build a VacuumParticipant instance. + * Note: After creation you must await instance.promiseRegistered() to ensure + * Category Caches have been updated. + * + * @param {mozIStorageAsyncConnection} databaseConnection + * The connection to be vacuumed. + * @param {Number} [expectedPageSize] + * Used to change the database page size. + * @param {boolean} [useIncrementalVacuum] + * Whether to enable incremental vacuum on the database. + * @param {boolean} [grant] + * Whether the vacuum operation should be granted. + */ + constructor( + databaseConnection, + { expectedPageSize = 0, useIncrementalVacuum = false, grant = true } = {} + ) { + this.#dbConn = databaseConnection; + + // Register as the only participant. + this.#unregisterAllParticipants(); + this.#registerAsParticipant(); + + this.#expectedPageSize = expectedPageSize; + this.#useIncrementalVacuum = useIncrementalVacuum; + this.#grant = grant; + + this.QueryInterface = ChromeUtils.generateQI([ + "mozIStorageVacuumParticipant", + ]); + } + + promiseRegistered() { + // The category manager dispatches change notifications to the main thread, + // so we must wait one tick. + return TestUtils.waitForTick(); + } + + #registerAsParticipant() { + MockRegistrar.register(CONTRACT_ID, this); + Services.catMan.addCategoryEntry( + CAT_NAME, + "vacuumParticipant", + CONTRACT_ID, + false, + false + ); + } + + #unregisterAllParticipants() { + // First unregister other participants. + for (let { data: entry } of Services.catMan.enumerateCategory(CAT_NAME)) { + Services.catMan.deleteCategoryEntry("vacuum-participant", entry, false); + } + } + + async dispose() { + this.#unregisterAllParticipants(); + MockRegistrar.unregister(CONTRACT_ID); + await new Promise(resolve => { + this.#dbConn.asyncClose(resolve); + }); + } + + get expectedDatabasePageSize() { + return this.#expectedPageSize; + } + + get useIncrementalVacuum() { + return this.#useIncrementalVacuum; + } + + get databaseConnection() { + return this.#dbConn; + } + + onBeginVacuum() { + if (!this.#grant) { + return false; + } + Services.obs.notifyObservers(null, "test-begin-vacuum"); + return true; + } + onEndVacuum(succeeded) { + Services.obs.notifyObservers( + null, + succeeded ? "test-end-vacuum-success" : "test-end-vacuum-failure" + ); + } +} diff --git a/storage/test/unit/baddataDB.sqlite b/storage/test/unit/baddataDB.sqlite Binary files differnew file mode 100644 index 0000000000..5b2f9da3d6 --- /dev/null +++ b/storage/test/unit/baddataDB.sqlite diff --git a/storage/test/unit/corruptDB.sqlite b/storage/test/unit/corruptDB.sqlite Binary files differnew file mode 100644 index 0000000000..b234246cac --- /dev/null +++ b/storage/test/unit/corruptDB.sqlite diff --git a/storage/test/unit/fakeDB.sqlite b/storage/test/unit/fakeDB.sqlite new file mode 100644 index 0000000000..5f7498bfc2 --- /dev/null +++ b/storage/test/unit/fakeDB.sqlite @@ -0,0 +1 @@ +BACON diff --git a/storage/test/unit/goodDB.sqlite b/storage/test/unit/goodDB.sqlite Binary files differnew file mode 100644 index 0000000000..b06884672f --- /dev/null +++ b/storage/test/unit/goodDB.sqlite diff --git a/storage/test/unit/head_storage.js b/storage/test/unit/head_storage.js new file mode 100644 index 0000000000..3cddc36801 --- /dev/null +++ b/storage/test/unit/head_storage.js @@ -0,0 +1,412 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", + Sqlite: "resource://gre/modules/Sqlite.sys.mjs", + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", + TestUtils: "resource://testing-common/TestUtils.sys.mjs", +}); + +const OPEN_HISTOGRAM = "SQLITE_STORE_OPEN"; +const QUERY_HISTOGRAM = "SQLITE_STORE_QUERY"; + +const TELEMETRY_VALUES = { + success: 0, + failure: 1, + access: 2, + diskio: 3, + corrupt: 4, + busy: 5, + misuse: 6, + diskspace: 7, +}; + +do_get_profile(); +var gDBConn = null; + +const TEST_DB_NAME = "test_storage.sqlite"; +function getTestDB() { + var db = Services.dirsvc.get("ProfD", Ci.nsIFile); + db.append(TEST_DB_NAME); + return db; +} + +/** + * Obtains a corrupt database to test against. + */ +function getCorruptDB() { + return do_get_file("corruptDB.sqlite"); +} + +/** + * Obtains a fake (non-SQLite format) database to test against. + */ +function getFakeDB() { + return do_get_file("fakeDB.sqlite"); +} + +/** + * Delete the test database file. + */ +function deleteTestDB() { + print("*** Storage Tests: Trying to remove file!"); + var dbFile = getTestDB(); + if (dbFile.exists()) { + try { + dbFile.remove(false); + } catch (e) { + /* stupid windows box */ + } + } +} + +function cleanup() { + // close the connection + print("*** Storage Tests: Trying to close!"); + getOpenedDatabase().close(); + + // we need to null out the database variable to get a new connection the next + // time getOpenedDatabase is called + gDBConn = null; + + // removing test db + deleteTestDB(); +} + +/** + * Use asyncClose to cleanup a connection. Synchronous by means of internally + * spinning an event loop. + */ +function asyncCleanup() { + let closed = false; + + // close the connection + print("*** Storage Tests: Trying to asyncClose!"); + getOpenedDatabase().asyncClose(function () { + closed = true; + }); + + let tm = Cc["@mozilla.org/thread-manager;1"].getService(); + tm.spinEventLoopUntil("Test(head_storage.js:asyncCleanup)", () => closed); + + // we need to null out the database variable to get a new connection the next + // time getOpenedDatabase is called + gDBConn = null; + + // removing test db + deleteTestDB(); +} + +/** + * Get a connection to the test database. Creates and caches the connection + * if necessary, otherwise reuses the existing cached connection. This + * connection shares its cache. + * + * @returns the mozIStorageConnection for the file. + */ +function getOpenedDatabase(connectionFlags = 0) { + if (!gDBConn) { + gDBConn = Services.storage.openDatabase(getTestDB(), connectionFlags); + + // Clear out counts for any queries that occured while opening the database. + TelemetryTestUtils.getAndClearKeyedHistogram(OPEN_HISTOGRAM); + TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM); + } + return gDBConn; +} + +/** + * Get a connection to the test database. Creates and caches the connection + * if necessary, otherwise reuses the existing cached connection. This + * connection doesn't share its cache. + * + * @returns the mozIStorageConnection for the file. + */ +function getOpenedUnsharedDatabase() { + if (!gDBConn) { + gDBConn = Services.storage.openUnsharedDatabase(getTestDB()); + } + return gDBConn; +} + +/** + * Obtains a specific database to use. + * + * @param aFile + * The nsIFile representing the db file to open. + * @returns the mozIStorageConnection for the file. + */ +function getDatabase(aFile) { + return Services.storage.openDatabase(aFile); +} + +function createStatement(aSQL) { + return getOpenedDatabase().createStatement(aSQL); +} + +/** + * Creates an asynchronous SQL statement. + * + * @param aSQL + * The SQL to parse into a statement. + * @returns a mozIStorageAsyncStatement from aSQL. + */ +function createAsyncStatement(aSQL) { + return getOpenedDatabase().createAsyncStatement(aSQL); +} + +/** + * Invoke the given function and assert that it throws an exception expressing + * the provided error code in its 'result' attribute. JS function expressions + * can be used to do this concisely. + * + * Example: + * expectError(Cr.NS_ERROR_INVALID_ARG, () => explodingFunction()); + * + * @param aErrorCode + * The error code to expect from invocation of aFunction. + * @param aFunction + * The function to invoke and expect an XPCOM-style error from. + */ +function expectError(aErrorCode, aFunction) { + let exceptionCaught = false; + try { + aFunction(); + } catch (e) { + if (e.result != aErrorCode) { + do_throw( + "Got an exception, but the result code was not the expected " + + "one. Expected " + + aErrorCode + + ", got " + + e.result + ); + } + exceptionCaught = true; + } + if (!exceptionCaught) { + do_throw(aFunction + " should have thrown an exception but did not!"); + } +} + +/** + * Run a query synchronously and verify that we get back the expected results. + * + * @param aSQLString + * The SQL string for the query. + * @param aBind + * The value to bind at index 0. + * @param aResults + * A list of the expected values returned in the sole result row. + * Express blobs as lists. + */ +function verifyQuery(aSQLString, aBind, aResults) { + let stmt = getOpenedDatabase().createStatement(aSQLString); + stmt.bindByIndex(0, aBind); + try { + Assert.ok(stmt.executeStep()); + let nCols = stmt.numEntries; + if (aResults.length != nCols) { + do_throw( + "Expected " + + aResults.length + + " columns in result but " + + "there are only " + + aResults.length + + "!" + ); + } + for (let iCol = 0; iCol < nCols; iCol++) { + let expectedVal = aResults[iCol]; + let valType = stmt.getTypeOfIndex(iCol); + if (expectedVal === null) { + Assert.equal(stmt.VALUE_TYPE_NULL, valType); + Assert.ok(stmt.getIsNull(iCol)); + } else if (typeof expectedVal == "number") { + if (Math.floor(expectedVal) == expectedVal) { + Assert.equal(stmt.VALUE_TYPE_INTEGER, valType); + Assert.equal(expectedVal, stmt.getInt32(iCol)); + } else { + Assert.equal(stmt.VALUE_TYPE_FLOAT, valType); + Assert.equal(expectedVal, stmt.getDouble(iCol)); + } + } else if (typeof expectedVal == "string") { + Assert.equal(stmt.VALUE_TYPE_TEXT, valType); + Assert.equal(expectedVal, stmt.getUTF8String(iCol)); + } else { + // blob + Assert.equal(stmt.VALUE_TYPE_BLOB, valType); + let count = { value: 0 }, + blob = { value: null }; + stmt.getBlob(iCol, count, blob); + Assert.equal(count.value, expectedVal.length); + for (let i = 0; i < count.value; i++) { + Assert.equal(expectedVal[i], blob.value[i]); + } + } + } + } finally { + stmt.finalize(); + } +} + +/** + * Return the number of rows in the able with the given name using a synchronous + * query. + * + * @param aTableName + * The name of the table. + * @return The number of rows. + */ +function getTableRowCount(aTableName) { + var currentRows = 0; + var countStmt = getOpenedDatabase().createStatement( + "SELECT COUNT(1) AS count FROM " + aTableName + ); + try { + Assert.ok(countStmt.executeStep()); + currentRows = countStmt.row.count; + } finally { + countStmt.finalize(); + } + return currentRows; +} + +// Promise-Returning Functions + +function asyncClone(db, readOnly) { + return new Promise((resolve, reject) => { + db.asyncClone(readOnly, function (status, db2) { + if (Components.isSuccessCode(status)) { + resolve(db2); + } else { + reject(status); + } + }); + }); +} + +function asyncClose(db) { + return new Promise((resolve, reject) => { + db.asyncClose(function (status) { + if (Components.isSuccessCode(status)) { + resolve(); + } else { + reject(status); + } + }); + }); +} + +function mapOptionsToFlags(aOptions, aMapping) { + let result = aMapping.default; + Object.entries(aOptions || {}).forEach(([optionName, isTrue]) => { + if (aMapping.hasOwnProperty(optionName) && isTrue) { + result |= aMapping[optionName]; + } + }); + return result; +} + +function getOpenFlagsMap() { + return { + default: Ci.mozIStorageService.OPEN_DEFAULT, + shared: Ci.mozIStorageService.OPEN_SHARED, + readOnly: Ci.mozIStorageService.OPEN_READONLY, + ignoreLockingMode: Ci.mozIStorageService.OPEN_IGNORE_LOCKING_MODE, + }; +} + +function getConnectionFlagsMap() { + return { + default: Ci.mozIStorageService.CONNECTION_DEFAULT, + interruptible: Ci.mozIStorageService.CONNECTION_INTERRUPTIBLE, + }; +} + +function openAsyncDatabase(file, options) { + return new Promise((resolve, reject) => { + const openFlags = mapOptionsToFlags(options, getOpenFlagsMap()); + const connectionFlags = mapOptionsToFlags(options, getConnectionFlagsMap()); + + Services.storage.openAsyncDatabase( + file, + openFlags, + connectionFlags, + function (status, db) { + if (Components.isSuccessCode(status)) { + resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection)); + } else { + reject(status); + } + } + ); + }); +} + +function executeAsync(statement, onResult) { + return new Promise((resolve, reject) => { + statement.executeAsync({ + handleError(error) { + reject(error); + }, + handleResult(result) { + if (onResult) { + onResult(result); + } + }, + handleCompletion(result) { + resolve(result); + }, + }); + }); +} + +function executeMultipleStatementsAsync(db, statements, onResult) { + return new Promise((resolve, reject) => { + db.executeAsync(statements, { + handleError(error) { + reject(error); + }, + handleResult(result) { + if (onResult) { + onResult(result); + } + }, + handleCompletion(result) { + resolve(result); + }, + }); + }); +} + +function executeSimpleSQLAsync(db, query, onResult) { + return new Promise((resolve, reject) => { + db.executeSimpleSQLAsync(query, { + handleError(error) { + reject(error); + }, + handleResult(result) { + if (onResult) { + onResult(result); + } else { + do_throw("No results were expected"); + } + }, + handleCompletion(result) { + resolve(result); + }, + }); + }); +} + +cleanup(); diff --git a/storage/test/unit/locale_collation.txt b/storage/test/unit/locale_collation.txt new file mode 100644 index 0000000000..86f50579bb --- /dev/null +++ b/storage/test/unit/locale_collation.txt @@ -0,0 +1,174 @@ + +! +" +# +$ +% +& +' +( +) +* ++ +, +- +. +/ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +: +; +< += +> +? +@ +A +B +C +D +E +F +G +H +I +J +K +L +M +N +O +P +Q +R +S +T +U +V +W +X +Y +Z +[ +\ +] +^ +_ +` +a +b +c +d +e +f +g +h +i +j +k +l +m +n +o +p +q +r +s +t +u +v +w +x +y +z +{ +| +} +~ +ludwig van beethoven +Ludwig van Beethoven +Ludwig van beethoven +Jane +jane +JANE +jAne +jaNe +janE +JAne +JaNe +JanE +JANe +JaNE +JAnE +jANE +Umberto Eco +Umberto eco +umberto eco +umberto Eco +UMBERTO ECO +ace +bash +*ace +!ace +%ace +~ace +#ace +cork +denizen +[denizen] +(denizen) +{denizen} +/denizen/ +#denizen# +$denizen$ +@denizen +elf +full +gnome +gnomic investigation of typological factors in the grammaticalization process of Malayo-Polynesian substaratum in the protoAltaic vocabulary core in the proto-layers of pre-historic Japanese +gnomic investigation of typological factors in the grammaticalization process of Malayo-Polynesian substaratum in the protoAltaic vocabulary core in the proto-layers of pre-historic Javanese +hint +Internationalization +Zinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalization +Zinternationalization internationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizatio +n +Zinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalization internationalizationinternationalizationinternationalizationinternationalizationinternationalization +ZinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizaTioninternationalizationinternationalizationinternationalizationinternationalization +ZinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizaTion +jostle +kin +Laymen +lumens +Lumens +motleycrew +motley crew +niven's creative talents +nivens creative talents +opie +posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the Rockies +posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the Rokkies +posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the rockies +quilt's +quilts +quilt +Rondo +street +tamale oxidization and iodization in rapid progress +tamale oxidization and iodization in rapid Progress +until +vera +Wobble +Xanadu's legenary imaginary floccinaucinihilipilification in localization of theoretical portions of glottochronological understanding of the phoneme +Xanadu's legenary imaginary floccinaucinihilipilification in localization of theoretical portions of glottochronological understanding of the phoname +yearn +zodiac +a +å diff --git a/storage/test/unit/test_bug-365166.js b/storage/test/unit/test_bug-365166.js new file mode 100644 index 0000000000..aef53e60f2 --- /dev/null +++ b/storage/test/unit/test_bug-365166.js @@ -0,0 +1,23 @@ +// Testcase for bug 365166 - crash [@ strlen] calling +// mozIStorageStatement::getColumnName of a statement created with +// "PRAGMA user_version" or "PRAGMA schema_version" +function run_test() { + test("user"); + test("schema"); + + function test(param) { + var colName = param + "_version"; + var sql = "PRAGMA " + colName; + + var file = getTestDB(); + var conn = Services.storage.openDatabase(file); + var statement = conn.createStatement(sql); + try { + // This shouldn't crash: + Assert.equal(statement.getColumnName(0), colName); + } finally { + statement.reset(); + statement.finalize(); + } + } +} diff --git a/storage/test/unit/test_bug-393952.js b/storage/test/unit/test_bug-393952.js new file mode 100644 index 0000000000..a91bcb034e --- /dev/null +++ b/storage/test/unit/test_bug-393952.js @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Testcase for bug 393952: crash when I try to VACUUM and one of the tables +// has a UNIQUE text column. StorageUnicodeFunctions::likeFunction() +// needs to handle null aArgv[0] and aArgv[1] + +function setup() { + getOpenedDatabase().createTable("t1", "x TEXT UNIQUE"); + + var stmt = createStatement("INSERT INTO t1 (x) VALUES ('a')"); + stmt.execute(); + stmt.reset(); + stmt.finalize(); +} + +function test_vacuum() { + var stmt = createStatement("VACUUM;"); + stmt.executeStep(); + stmt.reset(); + stmt.finalize(); +} + +var tests = [test_vacuum]; + +function run_test() { + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_bug-429521.js b/storage/test/unit/test_bug-429521.js new file mode 100644 index 0000000000..1e647e984a --- /dev/null +++ b/storage/test/unit/test_bug-429521.js @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function setup() { + getOpenedDatabase().createTable("t1", "x TEXT"); + + var stmt = createStatement( + "INSERT INTO t1 (x) VALUES ('/mozilla.org/20070129_1/Europe/Berlin')" + ); + stmt.execute(); + stmt.finalize(); +} + +function test_bug429521() { + var stmt = createStatement( + "SELECT DISTINCT(zone) FROM (" + + "SELECT x AS zone FROM t1 WHERE x LIKE '/mozilla.org%'" + + ");" + ); + + print("*** test_bug429521: started"); + + try { + while (stmt.executeStep()) { + print("*** test_bug429521: step() Read wrapper.row.zone"); + + // BUG: the print commands after the following statement + // are never executed. Script stops immediately. + stmt.row.zone; + + print("*** test_bug429521: step() Read wrapper.row.zone finished"); + } + } catch (e) { + print("*** test_bug429521: " + e); + } + + print("*** test_bug429521: finished"); + + stmt.finalize(); +} + +function run_test() { + setup(); + + test_bug429521(); + + cleanup(); +} diff --git a/storage/test/unit/test_bug-444233.js b/storage/test/unit/test_bug-444233.js new file mode 100644 index 0000000000..934f69ad60 --- /dev/null +++ b/storage/test/unit/test_bug-444233.js @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function setup() { + // Create the table + getOpenedDatabase().createTable( + "test_bug444233", + "id INTEGER PRIMARY KEY, value TEXT" + ); + + // Insert dummy data, using wrapper methods + var stmt = createStatement( + "INSERT INTO test_bug444233 (value) VALUES (:value)" + ); + stmt.params.value = "value1"; + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO test_bug444233 (value) VALUES (:value)"); + stmt.params.value = "value2"; + stmt.execute(); + stmt.finalize(); +} + +function test_bug444233() { + print("*** test_bug444233: started"); + + // Check that there are 2 results + var stmt = createStatement("SELECT COUNT(*) AS number FROM test_bug444233"); + Assert.ok(stmt.executeStep()); + Assert.equal(2, stmt.row.number); + stmt.reset(); + stmt.finalize(); + + print("*** test_bug444233: doing delete"); + + // Now try to delete using IN + // Cheating since we know ids are 1,2 + try { + var ids = [1, 2]; + stmt = createStatement("DELETE FROM test_bug444233 WHERE id IN (:ids)"); + stmt.params.ids = ids; + } catch (e) { + print("*** test_bug444233: successfully caught exception"); + } + stmt.finalize(); +} + +function run_test() { + setup(); + test_bug444233(); + cleanup(); +} diff --git a/storage/test/unit/test_cache_size.js b/storage/test/unit/test_cache_size.js new file mode 100644 index 0000000000..bd56de64cd --- /dev/null +++ b/storage/test/unit/test_cache_size.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This file tests that dbs of various page sizes are using the right cache +// size (bug 703113). + +/** + * In order to change the cache size, we must open a DB, change the page + * size, create a table, close the DB, then re-open the DB. We then check + * the cache size after reopening. + * + * @param dbOpener + * function that opens the DB specified in file + * @param file + * file holding the database + * @param pageSize + * the DB's page size + * @param expectedCacheSize + * the expected cache size for the given page size + */ +function check_size(dbOpener, file, pageSize, expectedCacheSize) { + // Open the DB, immediately change its page size. + let db = dbOpener(file); + db.executeSimpleSQL("PRAGMA page_size = " + pageSize); + + // Check the page size change worked. + let stmt = db.createStatement("PRAGMA page_size"); + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.row.page_size, pageSize); + stmt.finalize(); + + // Create a simple table. + db.executeSimpleSQL("CREATE TABLE test ( id INTEGER PRIMARY KEY )"); + + // Close and re-open the DB. + db.close(); + db = dbOpener(file); + + // Check cache size is as expected. + stmt = db.createStatement("PRAGMA cache_size"); + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.row.cache_size, expectedCacheSize); + stmt.finalize(); +} + +function new_file(name) { + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append(name + ".sqlite"); + Assert.ok(!file.exists()); + return file; +} + +function run_test() { + const kExpectedCacheSize = -2048; // 2MiB + + let pageSizes = [1024, 4096, 32768]; + + for (let i = 0; i < pageSizes.length; i++) { + let pageSize = pageSizes[i]; + check_size( + getDatabase, + new_file("shared" + pageSize), + pageSize, + kExpectedCacheSize + ); + check_size( + Services.storage.openUnsharedDatabase, + new_file("unshared" + pageSize), + pageSize, + kExpectedCacheSize + ); + } +} diff --git a/storage/test/unit/test_chunk_growth.js b/storage/test/unit/test_chunk_growth.js new file mode 100644 index 0000000000..4bcdf86ce3 --- /dev/null +++ b/storage/test/unit/test_chunk_growth.js @@ -0,0 +1,51 @@ +// This file tests SQLITE_FCNTL_CHUNK_SIZE behaves as expected + +function run_sql(d, sql) { + var stmt = d.createStatement(sql); + stmt.execute(); + stmt.finalize(); +} + +function new_file(name) { + var file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append(name); + return file; +} + +function get_size(name) { + return new_file(name).fileSize; +} + +function run_test() { + const filename = "chunked.sqlite"; + const CHUNK_SIZE = 512 * 1024; + var d = getDatabase(new_file(filename)); + try { + d.setGrowthIncrement(CHUNK_SIZE, ""); + } catch (e) { + if (e.result != Cr.NS_ERROR_FILE_TOO_BIG) { + throw e; + } + print("Too little free space to set CHUNK_SIZE!"); + return; + } + run_sql(d, "CREATE TABLE bloat(data varchar)"); + + var orig_size = get_size(filename); + /* Dump in at least 32K worth of data. + * While writing ensure that the file size growth in chunksize set above. + */ + const str1024 = new Array(1024).join("T"); + for (var i = 0; i < 32; i++) { + run_sql(d, "INSERT INTO bloat VALUES('" + str1024 + "')"); + var size = get_size(filename); + // Must not grow in small increments. + Assert.ok(size == orig_size || size >= CHUNK_SIZE); + } + /* In addition to growing in chunk-size increments, the db + * should shrink in chunk-size increments too. + */ + run_sql(d, "DELETE FROM bloat"); + run_sql(d, "VACUUM"); + Assert.ok(get_size(filename) >= CHUNK_SIZE); +} diff --git a/storage/test/unit/test_connection_asyncClose.js b/storage/test/unit/test_connection_asyncClose.js new file mode 100644 index 0000000000..2d89087c5c --- /dev/null +++ b/storage/test/unit/test_connection_asyncClose.js @@ -0,0 +1,128 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Thorough branch coverage for asyncClose. + * + * Coverage of asyncClose by connection state at time of AsyncClose invocation: + * - (asyncThread && mDBConn) => AsyncCloseConnection used, actually closes + * - test_asyncClose_does_not_complete_before_statements + * - test_double_asyncClose_throws + * - test_asyncClose_does_not_throw_without_callback + * - (asyncThread && !mDBConn) => AsyncCloseConnection used, although no close + * is required. Note that this is only possible in the event that + * openAsyncDatabase was used and we failed to open the database. + * Additionally, the async connection will never be exposed to the caller and + * AsyncInitDatabase will be the one to (automatically) call AsyncClose. + * - test_asyncClose_failed_open + * - (!asyncThread && mDBConn) => Close() invoked, actually closes + * - test_asyncClose_on_sync_db + * - (!asyncThread && !mDBConn) => Close() invoked, no close needed, errors. + * This happens if the database has already been closed. + * - test_double_asyncClose_throws + */ + +/** + * Sanity check that our close indeed happens after asynchronously executed + * statements scheduled during the same turn of the event loop. Note that we + * just care that the statement says it completed without error, we're not + * worried that the close will happen and then the statement will magically + * complete. + */ +add_task(async function test_asyncClose_does_not_complete_before_statements() { + let db = Services.storage.openDatabase(getTestDB()); + let stmt = db.createStatement("SELECT * FROM sqlite_master"); + // Issue the executeAsync but don't yield for it... + let asyncStatementPromise = executeAsync(stmt); + stmt.finalize(); + + // Issue the close. (And now the order of yielding doesn't matter.) + // Branch coverage: (asyncThread && mDBConn) + await asyncClose(db); + equal( + await asyncStatementPromise, + Ci.mozIStorageStatementCallback.REASON_FINISHED + ); +}); + +/** + * Open an async database (ensures the async thread is created) and then invoke + * AsyncClose() twice without yielding control flow. The first will initiate + * the actual async close after calling setClosedState which synchronously + * impacts what the second call will observe. The second call will then see the + * async thread is not available and fall back to invoking Close() which will + * notice the mDBConn is already gone. + */ +if (!AppConstants.DEBUG) { + add_task(async function test_double_asyncClose_throws() { + let db = await openAsyncDatabase(getTestDB()); + + // (Don't yield control flow yet, save the promise for after we make the + // second call.) + // Branch coverage: (asyncThread && mDBConn) + let realClosePromise = await asyncClose(db); + try { + // Branch coverage: (!asyncThread && !mDBConn) + db.asyncClose(); + ok(false, "should have thrown"); + } catch (e) { + equal(e.result, Cr.NS_ERROR_NOT_INITIALIZED); + } + + await realClosePromise; + }); +} + +/** + * Create a sync db connection and never take it asynchronous and then call + * asyncClose on it. This will bring the async thread to life to perform the + * shutdown to avoid blocking the main thread, although we won't be able to + * tell the difference between this happening and the method secretly shunting + * to close(). + */ +add_task(async function test_asyncClose_on_sync_db() { + let db = Services.storage.openDatabase(getTestDB()); + + // Branch coverage: (!asyncThread && mDBConn) + await asyncClose(db); + ok(true, "closed sync connection asynchronously"); +}); + +/** + * Fail to asynchronously open a DB in order to get an async thread existing + * without having an open database and asyncClose invoked. As per the file + * doc-block, note that asyncClose will automatically be invoked by the + * AsyncInitDatabase when it fails to open the database. We will never be + * provided with a reference to the connection and so cannot call AsyncClose on + * it ourselves. + */ +add_task(async function test_asyncClose_failed_open() { + // This will fail and the promise will be rejected. + let openPromise = openAsyncDatabase(getFakeDB()); + await openPromise.then( + () => { + ok(false, "we should have failed to open the db; this test is broken!"); + }, + () => { + ok(true, "correctly failed to open db; bg asyncClose should happen"); + } + ); + // (NB: we are unable to observe the thread shutdown, but since we never open + // a database, this test is not going to interfere with other tests so much.) +}); + +// THE TEST BELOW WANTS TO BE THE LAST TEST WE RUN. DO NOT MAKE IT SAD. +/** + * Verify that asyncClose without a callback does not explode. Without a + * callback the shutdown is not actually observable, so we run this test last + * in order to avoid weird overlaps. + */ +add_task(async function test_asyncClose_does_not_throw_without_callback() { + let db = await openAsyncDatabase(getTestDB()); + // Branch coverage: (asyncThread && mDBConn) + db.asyncClose(); + ok(true, "if we shutdown cleanly and do not crash, then we succeeded"); +}); +// OBEY SHOUTING UPPER-CASE COMMENTS. +// ADD TESTS ABOVE THE FORMER TEST, NOT BELOW IT. diff --git a/storage/test/unit/test_connection_executeAsync.js b/storage/test/unit/test_connection_executeAsync.js new file mode 100644 index 0000000000..a77299ba3b --- /dev/null +++ b/storage/test/unit/test_connection_executeAsync.js @@ -0,0 +1,175 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This file tests the functionality of mozIStorageConnection::executeAsync for + * both mozIStorageStatement and mozIStorageAsyncStatement. + * + * A single database connection is used for the entirety of the test, which is + * a legacy thing, but we otherwise use the modern promise-based driver and + * async helpers. + */ + +const INTEGER = 1; +const TEXT = "this is test text"; +const REAL = 3.23; +const BLOB = [1, 2]; + +add_task(async function test_first_create_and_add() { + // synchronously open the database and let gDBConn hold onto it because we + // use this database + let db = getOpenedDatabase(); + // synchronously set up our table *that will be used for the rest of the file* + db.executeSimpleSQL( + "CREATE TABLE test (" + + "id INTEGER, " + + "string TEXT, " + + "number REAL, " + + "nuller NULL, " + + "blober BLOB" + + ")" + ); + + let stmts = []; + stmts[0] = db.createStatement( + "INSERT INTO test (id, string, number, nuller, blober) VALUES (?, ?, ?, ?, ?)" + ); + stmts[0].bindByIndex(0, INTEGER); + stmts[0].bindByIndex(1, TEXT); + stmts[0].bindByIndex(2, REAL); + stmts[0].bindByIndex(3, null); + stmts[0].bindBlobByIndex(4, BLOB, BLOB.length); + stmts[1] = getOpenedDatabase().createAsyncStatement( + "INSERT INTO test (string, number, nuller, blober) VALUES (?, ?, ?, ?)" + ); + stmts[1].bindByIndex(0, TEXT); + stmts[1].bindByIndex(1, REAL); + stmts[1].bindByIndex(2, null); + stmts[1].bindBlobByIndex(3, BLOB, BLOB.length); + + // asynchronously execute the statements + let execResult = await executeMultipleStatementsAsync( + db, + stmts, + function (aResultSet) { + ok(false, "we only did inserts so we should not have gotten results!"); + } + ); + equal( + Ci.mozIStorageStatementCallback.REASON_FINISHED, + execResult, + "execution should have finished successfully." + ); + + // Check that the result is in the table + let stmt = db.createStatement( + "SELECT string, number, nuller, blober FROM test WHERE id = ?" + ); + stmt.bindByIndex(0, INTEGER); + try { + Assert.ok(stmt.executeStep()); + Assert.equal(TEXT, stmt.getString(0)); + Assert.equal(REAL, stmt.getDouble(1)); + Assert.ok(stmt.getIsNull(2)); + let count = { value: 0 }; + let blob = { value: null }; + stmt.getBlob(3, count, blob); + Assert.equal(BLOB.length, count.value); + for (let i = 0; i < BLOB.length; i++) { + Assert.equal(BLOB[i], blob.value[i]); + } + } finally { + stmt.finalize(); + } + + // Make sure we have two rows in the table + stmt = db.createStatement("SELECT COUNT(1) FROM test"); + try { + Assert.ok(stmt.executeStep()); + Assert.equal(2, stmt.getInt32(0)); + } finally { + stmt.finalize(); + } + + stmts[0].finalize(); + stmts[1].finalize(); +}); + +add_task(async function test_last_multiple_bindings_on_statements() { + // This tests to make sure that we pass all the statements multiply bound + // parameters when we call executeAsync. + const AMOUNT_TO_ADD = 5; + const ITERATIONS = 5; + + let stmts = []; + let db = getOpenedDatabase(); + let sqlString = + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (:int, :text, :real, :null, :blob)"; + // We run the same statement twice, and should insert 2 * AMOUNT_TO_ADD. + for (let i = 0; i < ITERATIONS; i++) { + // alternate the type of statement we create + if (i % 2) { + stmts[i] = db.createStatement(sqlString); + } else { + stmts[i] = db.createAsyncStatement(sqlString); + } + + let params = stmts[i].newBindingParamsArray(); + for (let j = 0; j < AMOUNT_TO_ADD; j++) { + let bp = params.newBindingParams(); + bp.bindByName("int", INTEGER); + bp.bindByName("text", TEXT); + bp.bindByName("real", REAL); + bp.bindByName("null", null); + bp.bindBlobByName("blob", BLOB); + params.addParams(bp); + } + stmts[i].bindParameters(params); + } + + // Get our current number of rows in the table. + let currentRows = 0; + let countStmt = getOpenedDatabase().createStatement( + "SELECT COUNT(1) AS count FROM test" + ); + try { + Assert.ok(countStmt.executeStep()); + currentRows = countStmt.row.count; + } finally { + countStmt.reset(); + } + + // Execute asynchronously. + let execResult = await executeMultipleStatementsAsync( + db, + stmts, + function (aResultSet) { + ok(false, "we only did inserts so we should not have gotten results!"); + } + ); + equal( + Ci.mozIStorageStatementCallback.REASON_FINISHED, + execResult, + "execution should have finished successfully." + ); + + // Check to make sure we added all of our rows. + try { + Assert.ok(countStmt.executeStep()); + Assert.equal(currentRows + ITERATIONS * AMOUNT_TO_ADD, countStmt.row.count); + } finally { + countStmt.finalize(); + } + + stmts.forEach(stmt => stmt.finalize()); + + // we are the last test using this connection and since it has gone async + // we *must* call asyncClose on it. + await asyncClose(db); + gDBConn = null; +}); + +// If you add a test down here you will need to move the asyncClose or clean +// things up a little more. diff --git a/storage/test/unit/test_connection_executeSimpleSQLAsync.js b/storage/test/unit/test_connection_executeSimpleSQLAsync.js new file mode 100644 index 0000000000..00bdda7e03 --- /dev/null +++ b/storage/test/unit/test_connection_executeSimpleSQLAsync.js @@ -0,0 +1,94 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This file tests the functionality of + * mozIStorageAsyncConnection::executeSimpleSQLAsync. + */ + +const INTEGER = 1; +const TEXT = "this is test text"; +const REAL = 3.23; + +add_task(async function test_create_and_add() { + let adb = await openAsyncDatabase(getTestDB()); + + let completion = await executeSimpleSQLAsync( + adb, + "CREATE TABLE test (id INTEGER, string TEXT, number REAL)" + ); + + Assert.equal(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion); + + completion = await executeSimpleSQLAsync( + adb, + "INSERT INTO test (id, string, number) " + + "VALUES (" + + INTEGER + + ', "' + + TEXT + + '", ' + + REAL + + ")" + ); + + Assert.equal(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion); + + let result = null; + + completion = await executeSimpleSQLAsync( + adb, + "SELECT string, number FROM test WHERE id = 1", + function (aResultSet) { + result = aResultSet.getNextRow(); + Assert.equal(2, result.numEntries); + Assert.equal(TEXT, result.getString(0)); + Assert.equal(REAL, result.getDouble(1)); + } + ); + + Assert.equal(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion); + Assert.notEqual(result, null); + result = null; + + await executeSimpleSQLAsync( + adb, + "SELECT COUNT(0) FROM test", + function (aResultSet) { + result = aResultSet.getNextRow(); + Assert.equal(1, result.getInt32(0)); + } + ); + + Assert.notEqual(result, null); + + await asyncClose(adb); +}); + +add_task(async function test_asyncClose_does_not_complete_before_statement() { + let adb = await openAsyncDatabase(getTestDB()); + let executed = false; + + let reason = await executeSimpleSQLAsync( + adb, + "SELECT * FROM test", + function (aResultSet) { + let result = aResultSet.getNextRow(); + + Assert.notEqual(result, null); + Assert.equal(3, result.numEntries); + Assert.equal(INTEGER, result.getInt32(0)); + Assert.equal(TEXT, result.getString(1)); + Assert.equal(REAL, result.getDouble(2)); + executed = true; + } + ); + + Assert.equal(Ci.mozIStorageStatementCallback.REASON_FINISHED, reason); + + // Ensure that the statement executed to completion. + Assert.ok(executed); + + await asyncClose(adb); +}); diff --git a/storage/test/unit/test_connection_failsafe_close.js b/storage/test/unit/test_connection_failsafe_close.js new file mode 100644 index 0000000000..d4fa6d1503 --- /dev/null +++ b/storage/test/unit/test_connection_failsafe_close.js @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This file tests edge-cases related to mozStorageService::unregisterConnection + * in the face of failsafe closing at destruction time which results in + * SpinningSynchronousClose being invoked which can "resurrect" the connection + * and result in a second call to unregisterConnection. + * + * See https://bugzilla.mozilla.org/show_bug.cgi?id=1413501 for more context. + */ + +add_task(async function test_failsafe_close_of_async_connection() { + // get the db + let db = getOpenedDatabase(); + + // do something async + let callbackInvoked = new Promise(resolve => { + db.executeSimpleSQLAsync("CREATE TABLE test (id INTEGER)", { + handleCompletion: resolve, + }); + }); + + // drop our reference and force a GC so the only live reference is owned by + // the async statement. + db = gDBConn = null; + // (we don't need to cycle collect) + Cu.forceGC(); + + // now we need to wait for that callback to have completed. + await callbackInvoked; + + Assert.ok(true, "if we shutdown cleanly and do not crash, then we succeeded"); +}); diff --git a/storage/test/unit/test_connection_interrupt.js b/storage/test/unit/test_connection_interrupt.js new file mode 100644 index 0000000000..e257e9af82 --- /dev/null +++ b/storage/test/unit/test_connection_interrupt.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This file tests the functionality of mozIStorageAsyncConnection::interrupt +// in the asynchronous case. +add_task(async function test_wr_async_conn() { + // Interrupt cannot be used on R/W async connections. + let db = await openAsyncDatabase(getTestDB()); + await db.interrupt(); + info("should be able to interrupt a R/W async connection"); + await asyncClose(db); +}); + +add_task(async function test_closed_conn() { + let db = await openAsyncDatabase(getTestDB(), { readOnly: true }); + await asyncClose(db); + Assert.throws( + () => db.interrupt(), + /NS_ERROR_NOT_INITIALIZED/, + "interrupt() should throw if invoked on a closed connection" + ); +}); + +add_task( + { + // We use a timeout in the test that may be insufficient on Android emulators. + // We don't really need the Android coverage, so skip on Android. + skip_if: () => AppConstants.platform == "android", + }, + async function test_async_conn() { + let db = await openAsyncDatabase(getTestDB(), { readOnly: true }); + // This query is built to hang forever. + let stmt = db.createAsyncStatement(` + WITH RECURSIVE test(n) AS ( + VALUES(1) + UNION ALL + SELECT n + 1 FROM test + ) + SELECT t.n + FROM test,test AS t`); + + let completePromise = new Promise((resolve, reject) => { + let listener = { + handleResult(aResultSet) { + reject(); + }, + handleError(aError) { + reject(); + }, + handleCompletion(aReason) { + resolve(aReason); + }, + }; + stmt.executeAsync(listener); + stmt.finalize(); + }); + + // Wait for the statement to be executing. + // This is not rock-solid, see the discussion in bug 1320301. A better + // approach will be evaluated in a separate bug. + await new Promise(resolve => do_timeout(500, resolve)); + + db.interrupt(); + + Assert.equal( + await completePromise, + Ci.mozIStorageStatementCallback.REASON_CANCELED, + "Should have been canceled" + ); + + await asyncClose(db); + } +); + +add_task( + { + // We use a timeout in the test that may be insufficient on Android emulators. + // We don't really need the Android coverage, so skip on Android. + skip_if: () => AppConstants.platform == "android", + }, + async function test_async_conn() { + let db = await openAsyncDatabase(getTestDB()); + // This query is built to hang forever. + let stmt = db.createAsyncStatement(` + WITH RECURSIVE test(n) AS ( + VALUES(1) + UNION ALL + SELECT n + 1 FROM test + ) + SELECT t.n + FROM test,test AS t`); + + let completePromise = new Promise((resolve, reject) => { + let listener = { + handleResult(aResultSet) { + reject(); + }, + handleError(aError) { + reject(); + }, + handleCompletion(aReason) { + resolve(aReason); + }, + }; + stmt.executeAsync(listener); + stmt.finalize(); + }); + + // Wait for the statement to be executing. + // This is not rock-solid, see the discussion in bug 1320301. A better + // approach will be evaluated in a separate bug. + await new Promise(resolve => do_timeout(500, resolve)); + + // We are going to interrupt a database connection + db.interrupt(); + + Assert.equal( + await completePromise, + Ci.mozIStorageStatementCallback.REASON_CANCELED, + "Should have been able to cancel even for R/W database" + ); + + await asyncClose(db); + } +); diff --git a/storage/test/unit/test_default_journal_size_limit.js b/storage/test/unit/test_default_journal_size_limit.js new file mode 100644 index 0000000000..f2d28b9aa4 --- /dev/null +++ b/storage/test/unit/test_default_journal_size_limit.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests defaul journal_size_limit + +async function check_journal_size(db) { + let stmt = db.createAsyncStatement("PRAGMA journal_size_limit"); + let value = await new Promise((resolve, reject) => { + stmt.executeAsync({ + handleResult(resultSet) { + resolve(resultSet.getNextRow().getResultByIndex(0)); + }, + handleError(error) { + reject(); + }, + handleCompletion() {}, + }); + }); + Assert.greater(value, 0, "There is a positive journal_size_limit"); + stmt.finalize(); + await new Promise(resolve => db.asyncClose(resolve)); +} + +async function getDbPath(name) { + let path = PathUtils.join(PathUtils.profileDir, name + ".sqlite"); + Assert.ok(!(await IOUtils.exists(path))); + return path; +} + +add_task(async function () { + await check_journal_size( + Services.storage.openDatabase( + new FileUtils.File(await getDbPath("journal")) + ) + ); + await check_journal_size( + Services.storage.openUnsharedDatabase( + new FileUtils.File(await getDbPath("journalUnshared")) + ) + ); + await check_journal_size( + await openAsyncDatabase(new FileUtils.File(await getDbPath("journalAsync"))) + ); +}); diff --git a/storage/test/unit/test_js_helpers.js b/storage/test/unit/test_js_helpers.js new file mode 100644 index 0000000000..da533a78fc --- /dev/null +++ b/storage/test/unit/test_js_helpers.js @@ -0,0 +1,150 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sw=2 ts=2 sts=2 et : */ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests that the JS language helpers in various ways. + */ + +// Test Functions + +function test_params_enumerate() { + let stmt = createStatement("SELECT * FROM test WHERE id IN (:a, :b, :c)"); + + // Make sure they are right. + let expected = [0, 1, 2, "a", "b", "c", "length"]; + let index = 0; + for (let name in stmt.params) { + if (name == "QueryInterface") { + continue; + } + Assert.equal(name, expected[index++]); + } + Assert.equal(index, 7); +} + +function test_params_prototype() { + let stmt = createStatement("SELECT * FROM sqlite_master"); + + // Set a property on the prototype and make sure it exist (will not be a + // bindable parameter, however). + Object.getPrototypeOf(stmt.params).test = 2; + Assert.equal(stmt.params.test, 2); + + delete Object.getPrototypeOf(stmt.params).test; + stmt.finalize(); +} + +function test_row_prototype() { + let stmt = createStatement("SELECT * FROM sqlite_master"); + + Assert.ok(stmt.executeStep()); + + // Set a property on the prototype and make sure it exists (will not be in the + // results, however). + Object.getPrototypeOf(stmt.row).test = 2; + Assert.equal(stmt.row.test, 2); + + // Clean up after ourselves. + delete Object.getPrototypeOf(stmt.row).test; + stmt.finalize(); +} + +function test_row_enumerate() { + let stmt = createStatement("SELECT * FROM test"); + + Assert.ok(stmt.executeStep()); + + let expected = ["id", "string"]; + let expected_values = [123, "foo"]; + let index = 0; + for (let name in stmt.row) { + Assert.equal(name, expected[index]); + Assert.equal(stmt.row[name], expected_values[index]); + index++; + } + Assert.equal(index, 2); + + // Save off the row helper, then forget the statement and trigger a GC. We + // want to ensure that if the row helper is retained but the statement is + // destroyed, that no crash occurs and that the late access attempt simply + // throws an error. + let savedOffRow = stmt.row; + stmt = null; + Cu.forceGC(); + Assert.throws( + () => { + return savedOffRow.string; + }, + /NS_ERROR_NOT_INITIALIZED/, + "GC'ed statement should throw" + ); +} + +function test_params_gets_sync() { + // Added for bug 562866. + /* + let stmt = createStatement( + "SELECT * FROM test WHERE id IN (:a, :b, :c)" + ); + + // Make sure we do not assert in getting the value. + let originalCount = Object.getOwnPropertyNames(stmt.params).length; + let expected = ["a", "b", "c"]; + for (let name of expected) { + stmt.params[name]; + } + + // Now make sure we didn't magically get any additional properties. + let finalCount = Object.getOwnPropertyNames(stmt.params).length; + do_check_eq(originalCount + expected.length, finalCount); + */ +} + +function test_params_gets_async() { + // Added for bug 562866. + /* + let stmt = createAsyncStatement( + "SELECT * FROM test WHERE id IN (:a, :b, :c)" + ); + + // Make sure we do not assert in getting the value. + let originalCount = Object.getOwnPropertyNames(stmt.params).length; + let expected = ["a", "b", "c"]; + for (let name of expected) { + stmt.params[name]; + } + + // Now make sure we didn't magically get any additional properties. + let finalCount = Object.getOwnPropertyNames(stmt.params).length; + do_check_eq(originalCount + expected.length, finalCount); + */ +} + +// Test Runner + +var tests = [ + test_params_enumerate, + test_params_prototype, + test_row_enumerate, + test_row_prototype, + test_params_gets_sync, + test_params_gets_async, +]; +function run_test() { + cleanup(); + + // Create our database. + getOpenedDatabase().executeSimpleSQL( + "CREATE TABLE test (id INTEGER PRIMARY KEY, string TEXT)" + ); + getOpenedDatabase().executeSimpleSQL( + "INSERT INTO test (id, string) VALUES (123, 'foo')" + ); + + // Run the tests. + tests.forEach(test => test()); +} diff --git a/storage/test/unit/test_levenshtein.js b/storage/test/unit/test_levenshtein.js new file mode 100644 index 0000000000..a92b3a9559 --- /dev/null +++ b/storage/test/unit/test_levenshtein.js @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the Levenshtein Distance function we've registered. + +function createUtf16Database() { + print("Creating the in-memory UTF-16-encoded database."); + let conn = Services.storage.openSpecialDatabase("memory"); + conn.executeSimpleSQL("PRAGMA encoding = 'UTF-16'"); + + print("Make sure the encoding was set correctly and is now UTF-16."); + let stmt = conn.createStatement("PRAGMA encoding"); + Assert.ok(stmt.executeStep()); + let enc = stmt.getString(0); + stmt.finalize(); + + // The value returned will actually be UTF-16le or UTF-16be. + Assert.ok(enc === "UTF-16le" || enc === "UTF-16be"); + + return conn; +} + +function check_levenshtein(db, s, t, expectedDistance) { + var stmt = db.createStatement("SELECT levenshteinDistance(:s, :t) AS result"); + stmt.params.s = s; + stmt.params.t = t; + try { + Assert.ok(stmt.executeStep()); + Assert.equal(expectedDistance, stmt.row.result); + } finally { + stmt.reset(); + stmt.finalize(); + } +} + +function testLevenshtein(db) { + // Basic tests. + check_levenshtein(db, "", "", 0); + check_levenshtein(db, "foo", "", 3); + check_levenshtein(db, "", "bar", 3); + check_levenshtein(db, "yellow", "hello", 2); + check_levenshtein(db, "gumbo", "gambol", 2); + check_levenshtein(db, "kitten", "sitten", 1); + check_levenshtein(db, "sitten", "sittin", 1); + check_levenshtein(db, "sittin", "sitting", 1); + check_levenshtein(db, "kitten", "sitting", 3); + check_levenshtein(db, "Saturday", "Sunday", 3); + check_levenshtein(db, "YHCQPGK", "LAHYQQKPGKA", 6); + + // Test SQL NULL handling. + check_levenshtein(db, "foo", null, null); + check_levenshtein(db, null, "bar", null); + check_levenshtein(db, null, null, null); + + // The levenshteinDistance function allocates temporary memory on the stack + // if it can. Test some strings long enough to force a heap allocation. + var dots1000 = Array(1001).join("."); + var dashes1000 = Array(1001).join("-"); + check_levenshtein(db, dots1000, dashes1000, 1000); +} + +function run_test() { + testLevenshtein(getOpenedDatabase()); + testLevenshtein(createUtf16Database()); +} diff --git a/storage/test/unit/test_like.js b/storage/test/unit/test_like.js new file mode 100644 index 0000000000..91674a8816 --- /dev/null +++ b/storage/test/unit/test_like.js @@ -0,0 +1,199 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests our LIKE implementation since we override it for unicode + +function setup() { + getOpenedDatabase().createTable("t1", "x TEXT"); + + var stmt = createStatement("INSERT INTO t1 (x) VALUES ('a')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('ab')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('abc')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('abcd')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('acd')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('abd')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('bc')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('bcd')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('xyz')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('ABC')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('CDE')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('ABC abc xyz')"); + stmt.execute(); + stmt.finalize(); +} + +function test_count() { + var stmt = createStatement("SELECT count(*) FROM t1;"); + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.getInt32(0), 12); + stmt.reset(); + stmt.finalize(); +} + +function test_like_1() { + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, "abc"); + var solutions = ["abc", "ABC"]; + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(!stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_2() { + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, "ABC"); + var solutions = ["abc", "ABC"]; + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(!stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_3() { + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, "aBc"); + var solutions = ["abc", "ABC"]; + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(!stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_4() { + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, "abc%"); + var solutions = ["abc", "abcd", "ABC", "ABC abc xyz"]; + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(!stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_5() { + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, "a_c"); + var solutions = ["abc", "ABC"]; + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(!stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_6() { + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, "ab%d"); + var solutions = ["abcd", "abd"]; + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(!stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_7() { + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, "a_c%"); + var solutions = ["abc", "abcd", "ABC", "ABC abc xyz"]; + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(!stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_8() { + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, "%bcd"); + var solutions = ["abcd", "bcd"]; + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(!stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +var tests = [ + test_count, + test_like_1, + test_like_2, + test_like_3, + test_like_4, + test_like_5, + test_like_6, + test_like_7, + test_like_8, +]; + +function run_test() { + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_like_escape.js b/storage/test/unit/test_like_escape.js new file mode 100644 index 0000000000..66d6d1d583 --- /dev/null +++ b/storage/test/unit/test_like_escape.js @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const LATIN1_AE = "\xc6"; +const LATIN1_ae = "\xe6"; + +function setup() { + getOpenedDatabase().createTable("t1", "x TEXT"); + + var stmt = createStatement( + "INSERT INTO t1 (x) VALUES ('foo/bar_baz%20cheese')" + ); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES (?1)"); + // insert LATIN_ae, but search on LATIN_AE + stmt.bindByIndex(0, "foo%20" + LATIN1_ae + "/_bar"); + stmt.execute(); + stmt.finalize(); +} + +function test_escape_for_like_ascii() { + const paramForLike = "oo/bar_baz%20chees"; + const escapeChar = "/"; + + for (const utf8 of [false, true]) { + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?1 ESCAPE '/'"); + var paramForLikeEscaped = utf8 + ? stmt.escapeUTF8StringForLIKE(paramForLike, escapeChar) + : stmt.escapeStringForLIKE(paramForLike, escapeChar); + // verify that we escaped / _ and % + Assert.equal(paramForLikeEscaped, "oo//bar/_baz/%20chees"); + // prepend and append with % for "contains" + stmt.bindByIndex(0, "%" + paramForLikeEscaped + "%"); + stmt.executeStep(); + Assert.equal("foo/bar_baz%20cheese", stmt.getString(0)); + stmt.finalize(); + } +} + +function test_escape_for_like_non_ascii() { + const paramForLike = "oo%20" + LATIN1_AE + "/_ba"; + const escapeChar = "/"; + + for (const utf8 of [false, true]) { + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?1 ESCAPE '/'"); + var paramForLikeEscaped = utf8 + ? stmt.escapeUTF8StringForLIKE(paramForLike, escapeChar) + : stmt.escapeStringForLIKE(paramForLike, escapeChar); + // verify that we escaped / _ and % + Assert.equal(paramForLikeEscaped, "oo/%20" + LATIN1_AE + "///_ba"); + // prepend and append with % for "contains" + stmt.bindByIndex(0, "%" + paramForLikeEscaped + "%"); + stmt.executeStep(); + Assert.equal("foo%20" + LATIN1_ae + "/_bar", stmt.getString(0)); + stmt.finalize(); + } +} + +var tests = [test_escape_for_like_ascii, test_escape_for_like_non_ascii]; + +function run_test() { + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_locale_collation.js b/storage/test/unit/test_locale_collation.js new file mode 100644 index 0000000000..96f2415451 --- /dev/null +++ b/storage/test/unit/test_locale_collation.js @@ -0,0 +1,291 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Bug 499990 - Locale-aware collation + * + * Tests our custom, locale-aware collating sequences. + */ + +// The name of the file containing the strings we'll sort during this test. +// The file's data is taken from intl/locale/tests/sort/us-ascii_base.txt and +// and intl/locale/tests/sort/us-ascii_sort.txt. +const DATA_BASENAME = "locale_collation.txt"; + +// The test data from DATA_BASENAME is read into this array. +var gStrings; + +// A connection to our in-memory UTF-16-encoded database. +var gUtf16Conn; + +// Helper Functions + +/** + * Since we create a UTF-16 database we have to clean it up, in addition to + * the normal cleanup of Storage tests. + */ +function cleanupLocaleTests() { + print("-- Cleaning up test_locale_collation.js suite."); + gUtf16Conn.close(); + cleanup(); +} + +/** + * Creates a test database similar to the default one created in + * head_storage.js, except that this one uses UTF-16 encoding. + * + * @return A connection to the database. + */ +function createUtf16Database() { + print("Creating the in-memory UTF-16-encoded database."); + let conn = Services.storage.openSpecialDatabase("memory"); + conn.executeSimpleSQL("PRAGMA encoding = 'UTF-16'"); + + print("Make sure the encoding was set correctly and is now UTF-16."); + let stmt = conn.createStatement("PRAGMA encoding"); + Assert.ok(stmt.executeStep()); + let enc = stmt.getString(0); + stmt.finalize(); + + // The value returned will actually be UTF-16le or UTF-16be. + Assert.ok(enc === "UTF-16le" || enc === "UTF-16be"); + + return conn; +} + +/** + * Compares aActual to aExpected, ensuring that the numbers and orderings of + * the two arrays' elements are the same. + * + * @param aActual + * An array of strings retrieved from the database. + * @param aExpected + * An array of strings to which aActual should be equivalent. + */ +function ensureResultsAreCorrect(aActual, aExpected) { + print("Actual results: " + aActual); + print("Expected results: " + aExpected); + + Assert.equal(aActual.length, aExpected.length); + for (let i = 0; i < aActual.length; i++) { + Assert.equal(aActual[i], aExpected[i]); + } +} + +/** + * Synchronously SELECTs all rows from the test table of the given database + * using the given collation. + * + * @param aCollation + * The name of one of our custom locale collations. The rows are + * ordered by this collation. + * @param aConn + * A connection to either the UTF-8 database or the UTF-16 database. + * @return The resulting strings in an array. + */ +function getResults(aCollation, aConn) { + let results = []; + let stmt = aConn.createStatement( + "SELECT t FROM test ORDER BY t COLLATE " + aCollation + " ASC" + ); + while (stmt.executeStep()) { + results.push(stmt.row.t); + } + stmt.finalize(); + return results; +} + +/** + * Inserts strings into our test table of the given database in the order given. + * + * @param aStrings + * An array of strings. + * @param aConn + * A connection to either the UTF-8 database or the UTF-16 database. + */ +function initTableWithStrings(aStrings, aConn) { + print("Initializing test table."); + + aConn.executeSimpleSQL("DROP TABLE IF EXISTS test"); + aConn.createTable("test", "t TEXT"); + let stmt = aConn.createStatement("INSERT INTO test (t) VALUES (:t)"); + aStrings.forEach(function (str) { + stmt.params.t = str; + stmt.execute(); + stmt.reset(); + }); + stmt.finalize(); +} + +/** + * Returns a sorting function suitable for passing to Array.prototype.sort(). + * The returned function uses the application's locale to compare strings. + * + * @param aCollation + * The name of one of our custom locale collations. The sorting + * strength is computed from this value. + * @return A function to use as a sorting callback. + */ +function localeCompare(aCollation) { + let sensitivity; + + switch (aCollation) { + case "locale": + sensitivity = "base"; + break; + case "locale_case_sensitive": + sensitivity = "case"; + break; + case "locale_accent_sensitive": + sensitivity = "accent"; + break; + case "locale_case_accent_sensitive": + sensitivity = "variant"; + break; + default: + do_throw("Error in test: unknown collation '" + aCollation + "'"); + break; + } + const collation = new Intl.Collator("en", { sensitivity }); + return function (aStr1, aStr2) { + return collation.compare(aStr1, aStr2); + }; +} + +/** + * Reads in the test data from the file DATA_BASENAME and returns it as an array + * of strings. + * + * @return The test data as an array of strings. + */ +function readTestData() { + print("Reading in test data."); + + let file = do_get_file(DATA_BASENAME); + + let istream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + istream.init(file, -1, -1, 0); + istream.QueryInterface(Ci.nsILineInputStream); + + let line = {}; + let lines = []; + while (istream.readLine(line)) { + lines.push(line.value); + } + istream.close(); + + return lines; +} + +/** + * Gets the results from the given database using the given collation and + * ensures that they match gStrings sorted by the same collation. + * + * @param aCollation + * The name of one of our custom locale collations. The rows from the + * database and the expected results are ordered by this collation. + * @param aConn + * A connection to either the UTF-8 database or the UTF-16 database. + */ +function runTest(aCollation, aConn) { + ensureResultsAreCorrect( + getResults(aCollation, aConn), + gStrings.slice(0).sort(localeCompare(aCollation)) + ); +} + +/** + * Gets the results from the UTF-8 database using the given collation and + * ensures that they match gStrings sorted by the same collation. + * + * @param aCollation + * The name of one of our custom locale collations. The rows from the + * database and the expected results are ordered by this collation. + */ +function runUtf8Test(aCollation) { + runTest(aCollation, getOpenedDatabase()); +} + +/** + * Gets the results from the UTF-16 database using the given collation and + * ensures that they match gStrings sorted by the same collation. + * + * @param aCollation + * The name of one of our custom locale collations. The rows from the + * database and the expected results are ordered by this collation. + */ +function runUtf16Test(aCollation) { + runTest(aCollation, gUtf16Conn); +} + +/** + * Sets up the test suite. + */ +function setup() { + print("-- Setting up the test_locale_collation.js suite."); + + gStrings = readTestData(); + + initTableWithStrings(gStrings, getOpenedDatabase()); + + gUtf16Conn = createUtf16Database(); + initTableWithStrings(gStrings, gUtf16Conn); +} + +// Test Runs + +var gTests = [ + { + desc: "Case and accent sensitive UTF-8", + run: () => runUtf8Test("locale_case_accent_sensitive"), + }, + + { + desc: "Case sensitive, accent insensitive UTF-8", + run: () => runUtf8Test("locale_case_sensitive"), + }, + + { + desc: "Case insensitive, accent sensitive UTF-8", + run: () => runUtf8Test("locale_accent_sensitive"), + }, + + { + desc: "Case and accent insensitive UTF-8", + run: () => runUtf8Test("locale"), + }, + + { + desc: "Case and accent sensitive UTF-16", + run: () => runUtf16Test("locale_case_accent_sensitive"), + }, + + { + desc: "Case sensitive, accent insensitive UTF-16", + run: () => runUtf16Test("locale_case_sensitive"), + }, + + { + desc: "Case insensitive, accent sensitive UTF-16", + run: () => runUtf16Test("locale_accent_sensitive"), + }, + + { + desc: "Case and accent insensitive UTF-16", + run: () => runUtf16Test("locale"), + }, +]; + +function run_test() { + setup(); + gTests.forEach(function (test) { + print("-- Running test: " + test.desc); + test.run(); + }); + cleanupLocaleTests(); +} diff --git a/storage/test/unit/test_minimizeMemory.js b/storage/test/unit/test_minimizeMemory.js new file mode 100644 index 0000000000..e694ddc186 --- /dev/null +++ b/storage/test/unit/test_minimizeMemory.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This file tests that invoking `Service::minimizeMemory` succeeds for sync +// and async connections. + +function minimizeMemory() { + Services.storage + .QueryInterface(Ci.nsIObserver) + .observe(null, "memory-pressure", null); +} + +add_task(async function test_minimizeMemory_async_connection() { + let db = await openAsyncDatabase(getTestDB()); + minimizeMemory(); + await asyncClose(db); +}); + +add_task(async function test_minimizeMemory_sync_connection() { + let db = getOpenedDatabase(); + minimizeMemory(); + db.close(); +}); diff --git a/storage/test/unit/test_page_size_is_32k.js b/storage/test/unit/test_page_size_is_32k.js new file mode 100644 index 0000000000..4a91d13fdc --- /dev/null +++ b/storage/test/unit/test_page_size_is_32k.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This file tests that dbs are using 32k pagesize + +const kExpectedPageSize = 32768; // 32K +const kExpectedCacheSize = -2048; // 2MiB + +function check_size(db) { + var stmt = db.createStatement("PRAGMA page_size"); + stmt.executeStep(); + Assert.equal(stmt.getInt32(0), kExpectedPageSize); + stmt.finalize(); + stmt = db.createStatement("PRAGMA cache_size"); + stmt.executeStep(); + Assert.equal(stmt.getInt32(0), kExpectedCacheSize); + stmt.finalize(); +} + +function new_file(name) { + var file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append(name + ".sqlite"); + Assert.ok(!file.exists()); + return file; +} + +function run_test() { + check_size(getDatabase(new_file("shared32k"))); + check_size(Services.storage.openUnsharedDatabase(new_file("unshared32k"))); +} diff --git a/storage/test/unit/test_persist_journal.js b/storage/test/unit/test_persist_journal.js new file mode 100644 index 0000000000..1eea5c703e --- /dev/null +++ b/storage/test/unit/test_persist_journal.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the journal persists on close. + +async function check_journal_persists(db, journal) { + let path = db.databaseFile.path; + info(`testing ${path}`); + await new Promise(resolve => { + db.executeSimpleSQLAsync(`PRAGMA journal_mode = ${journal}`, { + handleCompletion: resolve, + }); + }); + + await new Promise(resolve => { + db.executeSimpleSQLAsync("CREATE TABLE test (id INTEGER PRIMARY KEY)", { + handleCompletion: resolve, + }); + }); + + if (journal == "wal") { + Assert.ok(await IOUtils.exists(path + "-wal"), "-wal exists before close"); + Assert.greater( + (await IOUtils.stat(path + "-wal")).size, + 0, + "-wal size is non-zero" + ); + } else { + Assert.ok( + await IOUtils.exists(path + "-journal"), + "-journal exists before close" + ); + Assert.equal( + (await IOUtils.stat(path + "-journal")).size, + 0, + "-journal is truncated after every transaction" + ); + } + + await new Promise(resolve => db.asyncClose(resolve)); + + if (journal == "wal") { + Assert.ok(await IOUtils.exists(path + "-wal"), "-wal persists after close"); + Assert.equal( + (await IOUtils.stat(path + "-wal")).size, + 0, + "-wal has been truncated" + ); + } else { + Assert.ok( + await IOUtils.exists(path + "-journal"), + "-journal persists after close" + ); + Assert.equal( + (await IOUtils.stat(path + "-journal")).size, + 0, + "-journal has been truncated" + ); + } +} + +async function getDbPath(name) { + let path = PathUtils.join(PathUtils.profileDir, name + ".sqlite"); + Assert.ok(!(await IOUtils.exists(path)), "database should not exist"); + return path; +} + +add_task(async function () { + for (let journal of ["truncate", "wal"]) { + await check_journal_persists( + Services.storage.openDatabase( + new FileUtils.File(await getDbPath(`shared-${journal}`)) + ), + journal + ); + await check_journal_persists( + Services.storage.openUnsharedDatabase( + new FileUtils.File(await getDbPath(`unshared-${journal}`)) + ), + journal + ); + await check_journal_persists( + await openAsyncDatabase( + new FileUtils.File(await getDbPath(`async-${journal}`)) + ), + journal + ); + } +}); diff --git a/storage/test/unit/test_readonly-immutable-nolock_vfs.js b/storage/test/unit/test_readonly-immutable-nolock_vfs.js new file mode 100644 index 0000000000..5c11150b0a --- /dev/null +++ b/storage/test/unit/test_readonly-immutable-nolock_vfs.js @@ -0,0 +1,47 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the readonly-immutable-nolock VFS. + +add_task(async function test() { + const path = PathUtils.join(PathUtils.profileDir, "ro"); + await IOUtils.makeDirectory(path); + const dbpath = PathUtils.join(path, "test-immutable.sqlite"); + + let conn = await Sqlite.openConnection({ path: dbpath }); + await conn.execute("PRAGMA journal_mode = WAL"); + await conn.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)"); + Assert.ok(await IOUtils.exists(dbpath + "-wal"), "wal journal exists"); + await conn.close(); + + // The wal should have been merged at this point, but just in case... + info("Remove auxiliary files and set the folder as readonly"); + await IOUtils.remove(dbpath + "-wal", { ignoreAbsent: true }); + await IOUtils.setPermissions(path, 0o555); + registerCleanupFunction(async () => { + await IOUtils.setPermissions(path, 0o777); + await IOUtils.remove(path, { recursive: true }); + }); + + // Windows doesn't disallow creating files in read only folders. + if (AppConstants.platform == "macosx" || AppConstants.platform == "linux") { + await Assert.rejects( + Sqlite.openConnection({ path: dbpath, readOnly: true }), + /NS_ERROR_FILE/, + "Should not be able to open the db because it can't create a wal journal" + ); + } + + // Open the database with ignoreLockingMode. + let conn2 = await Sqlite.openConnection({ + path: dbpath, + ignoreLockingMode: true, + }); + await conn2.execute("SELECT * FROM sqlite_master"); + Assert.ok( + !(await IOUtils.exists(dbpath + "-wal")), + "wal journal was not created" + ); + await conn2.close(); +}); diff --git a/storage/test/unit/test_retry_on_busy.js b/storage/test/unit/test_retry_on_busy.js new file mode 100644 index 0000000000..bf7cdafdad --- /dev/null +++ b/storage/test/unit/test_retry_on_busy.js @@ -0,0 +1,199 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +function getProfileFile(name) { + let file = do_get_profile(); + file.append(name); + return file; +} + +function promiseAsyncDatabase(name, openOptions = 0) { + return new Promise((resolve, reject) => { + let file = getProfileFile(name); + const connOptions = Ci.mozIStorageService.CONNECTION_DEFAULT; + Services.storage.openAsyncDatabase( + file, + openOptions, + connOptions, + (status, connection) => { + if (!Components.isSuccessCode(status)) { + reject(new Error(`Failed to open database: ${status}`)); + } else { + connection.QueryInterface(Ci.mozIStorageAsyncConnection); + resolve(connection); + } + } + ); + }); +} + +function promiseClone(db, readOnly = false) { + return new Promise((resolve, reject) => { + db.asyncClone(readOnly, (status, clone) => { + if (!Components.isSuccessCode(status)) { + reject(new Error(`Failed to clone connection: ${status}`)); + } else { + clone.QueryInterface(Ci.mozIStorageAsyncConnection); + resolve(clone); + } + }); + }); +} + +function promiseClose(db) { + return new Promise((resolve, reject) => { + db.asyncClose(status => { + if (!Components.isSuccessCode(status)) { + reject(new Error(`Failed to close connection: ${status}`)); + } else { + resolve(); + } + }); + }); +} + +function promiseExecuteStatement(statement) { + return new Promise((resolve, reject) => { + let rows = []; + statement.executeAsync({ + handleResult(resultSet) { + let row = null; + do { + row = resultSet.getNextRow(); + if (row) { + rows.push(row); + } + } while (row); + }, + handleError(error) { + reject(new Error(`Failed to execute statement: ${error.message}`)); + }, + handleCompletion(reason) { + if (reason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { + resolve(rows); + } else { + reject(new Error("Statement failed to execute or was cancelled")); + } + }, + }); + }); +} + +add_task(async function test_retry_on_busy() { + info("Open first writer in WAL mode and set up schema"); + let db1 = await promiseAsyncDatabase("retry-on-busy.sqlite"); + + let walStmt = db1.createAsyncStatement(`PRAGMA journal_mode = WAL`); + await promiseExecuteStatement(walStmt); + let createAStmt = db1.createAsyncStatement(`CREATE TABLE a( + b INTEGER PRIMARY KEY + )`); + await promiseExecuteStatement(createAStmt); + let createPrevAStmt = db1.createAsyncStatement(`CREATE TEMP TABLE prevA( + curB INTEGER PRIMARY KEY, + prevB INTEGER NOT NULL + )`); + await promiseExecuteStatement(createPrevAStmt); + let createATriggerStmt = db1.createAsyncStatement(` + CREATE TEMP TRIGGER a_afterinsert_trigger + AFTER UPDATE ON a FOR EACH ROW + BEGIN + REPLACE INTO prevA(curB, prevB) VALUES(NEW.b, OLD.b); + END`); + await promiseExecuteStatement(createATriggerStmt); + + info("Open second writer"); + let db2 = await promiseClone(db1); + + info("Attach second writer to new database"); + let attachStmt = db2.createAsyncStatement(`ATTACH :path AS newDB`); + attachStmt.bindByName( + "path", + getProfileFile("retry-on-busy-attach.sqlite").path + ); + await promiseExecuteStatement(attachStmt); + + info("Create triggers on second writer"); + let createCStmt = db2.createAsyncStatement(`CREATE TABLE newDB.c( + d INTEGER PRIMARY KEY + )`); + await promiseExecuteStatement(createCStmt); + let createCTriggerStmt = db2.createAsyncStatement(` + CREATE TEMP TRIGGER c_afterdelete_trigger + AFTER DELETE ON c FOR EACH ROW + BEGIN + INSERT INTO a(b) VALUES(OLD.d); + END`); + await promiseExecuteStatement(createCTriggerStmt); + + info("Begin transaction on second writer"); + let begin2Stmt = db2.createAsyncStatement("BEGIN IMMEDIATE"); + await promiseExecuteStatement(begin2Stmt); + + info( + "Begin transaction on first writer; should busy-wait until second writer is done" + ); + let begin1Stmt = db1.createAsyncStatement("BEGIN IMMEDIATE"); + let promise1Began = promiseExecuteStatement(begin1Stmt); + let update1Stmt = db1.createAsyncStatement(`UPDATE a SET b = 3 WHERE b = 1`); + let promise1Updated = promiseExecuteStatement(update1Stmt); + + info("Wait 5 seconds"); + await new Promise(resolve => setTimeout(resolve, 5000)); + + info("Commit transaction on second writer"); + let insertIntoA2Stmt = db2.createAsyncStatement(`INSERT INTO a(b) VALUES(1)`); + await promiseExecuteStatement(insertIntoA2Stmt); + let insertIntoC2Stmt = db2.createAsyncStatement(`INSERT INTO c(d) VALUES(2)`); + await promiseExecuteStatement(insertIntoC2Stmt); + let deleteFromC2Stmt = db2.createAsyncStatement(`DELETE FROM c`); + await promiseExecuteStatement(deleteFromC2Stmt); + let commit2Stmt = db2.createAsyncStatement("COMMIT"); + await promiseExecuteStatement(commit2Stmt); + + info("Await and commit transaction on first writer"); + await promise1Began; + await promise1Updated; + + let commit1Stmt = db1.createAsyncStatement("COMMIT"); + await promiseExecuteStatement(commit1Stmt); + + info("Verify our writes succeeded"); + let select1Stmt = db2.createAsyncStatement("SELECT b FROM a"); + let rows = await promiseExecuteStatement(select1Stmt); + deepEqual( + rows.map(row => row.getResultByName("b")), + [2, 3] + ); + + info("Clean up"); + for (let stmt of [ + walStmt, + createAStmt, + createPrevAStmt, + createATriggerStmt, + attachStmt, + createCStmt, + createCTriggerStmt, + begin2Stmt, + begin1Stmt, + insertIntoA2Stmt, + insertIntoC2Stmt, + deleteFromC2Stmt, + + commit2Stmt, + update1Stmt, + commit1Stmt, + select1Stmt, + ]) { + stmt.finalize(); + } + await promiseClose(db1); + await promiseClose(db2); +}); diff --git a/storage/test/unit/test_sqlite_secure_delete.js b/storage/test/unit/test_sqlite_secure_delete.js new file mode 100644 index 0000000000..7beb1f1764 --- /dev/null +++ b/storage/test/unit/test_sqlite_secure_delete.js @@ -0,0 +1,78 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + *vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This file tests to make sure that SQLite was compiled with + * SQLITE_SECURE_DELETE=1. + */ + +// Helper Methods + +/** + * Reads the contents of a file and returns it as a string. + * + * @param aFile + * The file to return from. + * @return the contents of the file in the form of a string. + */ +function getFileContents(aFile) { + let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(aFile, -1, 0, 0); + + let bstream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + bstream.setInputStream(fstream); + return bstream.readBytes(bstream.available()); +} + +// Tests + +add_test(function test_delete_removes_data() { + const TEST_STRING = "SomeRandomStringToFind"; + + let file = getTestDB(); + let db = Services.storage.openDatabase(file); + + // Create the table and insert the data. + db.createTable("test", "data TEXT"); + let stmt = db.createStatement("INSERT INTO test VALUES(:data)"); + stmt.params.data = TEST_STRING; + try { + stmt.execute(); + } finally { + stmt.finalize(); + } + + // Make sure this test is actually testing what it thinks by making sure the + // string shows up in the database. Because the previous statement was + // automatically wrapped in a transaction, the contents are already on disk. + let contents = getFileContents(file); + Assert.notEqual(-1, contents.indexOf(TEST_STRING)); + + // Delete the data, and then close the database. + stmt = db.createStatement("DELETE FROM test WHERE data = :data"); + stmt.params.data = TEST_STRING; + try { + stmt.execute(); + } finally { + stmt.finalize(); + } + db.close(); + + // Check the file to see if the string can be found. + contents = getFileContents(file); + Assert.equal(-1, contents.indexOf(TEST_STRING)); + + run_next_test(); +}); + +function run_test() { + cleanup(); + run_next_test(); +} diff --git a/storage/test/unit/test_statement_executeAsync.js b/storage/test/unit/test_statement_executeAsync.js new file mode 100644 index 0000000000..dab15121a5 --- /dev/null +++ b/storage/test/unit/test_statement_executeAsync.js @@ -0,0 +1,1045 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This file tests the functionality of mozIStorageBaseStatement::executeAsync + * for both mozIStorageStatement and mozIStorageAsyncStatement. + */ + +// This file uses the internal _quit from testing/xpcshell/head.js */ +/* global _quit */ + +const INTEGER = 1; +const TEXT = "this is test text"; +const REAL = 3.23; +const BLOB = [1, 2]; + +/** + * Execute the given statement asynchronously, spinning an event loop until the + * async statement completes. + * + * @param aStmt + * The statement to execute. + * @param [aOptions={}] + * @param [aOptions.error=false] + * If true we should expect an error whose code we do not care about. If + * a numeric value, that's the error code we expect and require. If we + * are expecting an error, we expect a completion reason of REASON_ERROR. + * Otherwise we expect no error notification and a completion reason of + * REASON_FINISHED. + * @param [aOptions.cancel] + * If true we cancel the pending statement and additionally return the + * pending statement in case you want to further manipulate it. + * @param [aOptions.returnPending=false] + * If true we keep the pending statement around and return it to you. We + * normally avoid doing this to try and minimize the amount of time a + * reference is held to the returned pending statement. + * @param [aResults] + * If omitted, we assume no results rows are expected. If it is a + * number, we assume it is the number of results rows expected. If it is + * a function, we assume it is a function that takes the 1) result row + * number, 2) result tuple, 3) call stack for the original call to + * execAsync as arguments. If it is a list, we currently assume it is a + * list of functions where each function is intended to evaluate the + * result row at that ordinal position and takes the result tuple and + * the call stack for the original call. + */ +function execAsync(aStmt, aOptions, aResults) { + let caller = Components.stack.caller; + if (aOptions == null) { + aOptions = {}; + } + + let resultsExpected; + let resultsChecker; + if (aResults == null) { + resultsExpected = 0; + } else if (typeof aResults == "number") { + resultsExpected = aResults; + } else if (typeof aResults == "function") { + resultsChecker = aResults; + } else { + // array + resultsExpected = aResults.length; + resultsChecker = function (aResultNum, aTup, aCaller) { + aResults[aResultNum](aTup, aCaller); + }; + } + let resultsSeen = 0; + + let errorCodeExpected = false; + let reasonExpected = Ci.mozIStorageStatementCallback.REASON_FINISHED; + let altReasonExpected = null; + if ("error" in aOptions) { + errorCodeExpected = aOptions.error; + if (errorCodeExpected) { + reasonExpected = Ci.mozIStorageStatementCallback.REASON_ERROR; + } + } + let errorCodeSeen = false; + + if ("cancel" in aOptions && aOptions.cancel) { + altReasonExpected = Ci.mozIStorageStatementCallback.REASON_CANCELED; + } + + let completed = false; + + let listener = { + handleResult(aResultSet) { + let row, + resultsSeenThisCall = 0; + while ((row = aResultSet.getNextRow()) != null) { + if (resultsChecker) { + resultsChecker(resultsSeen, row, caller); + } + resultsSeen++; + resultsSeenThisCall++; + } + + if (!resultsSeenThisCall) { + do_throw("handleResult invoked with 0 result rows!"); + } + }, + handleError(aError) { + if (errorCodeSeen) { + do_throw("handleError called when we already had an error!"); + } + errorCodeSeen = aError.result; + }, + handleCompletion(aReason) { + if (completed) { + // paranoia check + do_throw("Received a second handleCompletion notification!", caller); + } + + if (resultsSeen != resultsExpected) { + do_throw( + "Expected " + + resultsExpected + + " rows of results but " + + "got " + + resultsSeen + + " rows!", + caller + ); + } + + if (errorCodeExpected && !errorCodeSeen) { + do_throw("Expected an error, but did not see one.", caller); + } else if (errorCodeExpected != errorCodeSeen) { + do_throw( + "Expected error code " + + errorCodeExpected + + " but got " + + errorCodeSeen, + caller + ); + } + + if (aReason != reasonExpected && aReason != altReasonExpected) { + do_throw( + "Expected reason " + + reasonExpected + + (altReasonExpected ? " or " + altReasonExpected : "") + + " but got " + + aReason, + caller + ); + } + + completed = true; + }, + }; + + let pending; + // Only get a pending reference if we're supposed to do. + // (note: This does not stop XPConnect from holding onto one currently.) + if ( + ("cancel" in aOptions && aOptions.cancel) || + ("returnPending" in aOptions && aOptions.returnPending) + ) { + pending = aStmt.executeAsync(listener); + } else { + aStmt.executeAsync(listener); + } + + if ("cancel" in aOptions && aOptions.cancel) { + pending.cancel(); + } + + Services.tm.spinEventLoopUntil( + "Test(test_statement_executeAsync.js:execAsync)", + () => completed || _quit + ); + + return pending; +} + +/** + * Make sure that illegal SQL generates the expected runtime error and does not + * result in any crashes. Async-only since the synchronous case generates the + * error synchronously (and is tested elsewhere). + */ +function test_illegal_sql_async_deferred() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM); + + // gibberish + let stmt = makeTestStatement("I AM A ROBOT. DO AS I SAY."); + execAsync(stmt, { error: Ci.mozIStorageError.ERROR }); + stmt.finalize(); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + TEST_DB_NAME, + TELEMETRY_VALUES.failure, + 1 + ); + histogram.clear(); + + // legal SQL syntax, but with semantics issues. + stmt = makeTestStatement("SELECT destination FROM funkytown"); + execAsync(stmt, { error: Ci.mozIStorageError.ERROR }); + stmt.finalize(); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + TEST_DB_NAME, + TELEMETRY_VALUES.failure, + 1 + ); + histogram.clear(); + + run_next_test(); +} +test_illegal_sql_async_deferred.asyncOnly = true; + +function test_create_table() { + // Ensure our table doesn't exist + Assert.ok(!getOpenedDatabase().tableExists("test")); + + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM); + + var stmt = makeTestStatement( + "CREATE TABLE test (" + + "id INTEGER, " + + "string TEXT, " + + "number REAL, " + + "nuller NULL, " + + "blober BLOB" + + ")" + ); + execAsync(stmt); + stmt.finalize(); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + TEST_DB_NAME, + TELEMETRY_VALUES.success, + 1 + ); + histogram.clear(); + + // Check that the table has been created + Assert.ok(getOpenedDatabase().tableExists("test")); + + histogram.clear(); + + // Verify that it's created correctly (this will throw if it wasn't) + let checkStmt = getOpenedDatabase().createStatement( + "SELECT id, string, number, nuller, blober FROM test" + ); + checkStmt.finalize(); + + // Nothing has executed so the histogram should be empty. + Assert.ok(!histogram.snapshot().values); + + run_next_test(); +} + +function test_add_data() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM); + + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (?, ?, ?, ?, ?)" + ); + stmt.bindBlobByIndex(4, BLOB, BLOB.length); + stmt.bindByIndex(3, null); + stmt.bindByIndex(2, REAL); + stmt.bindByIndex(1, TEXT); + stmt.bindByIndex(0, INTEGER); + + execAsync(stmt); + stmt.finalize(); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + TEST_DB_NAME, + TELEMETRY_VALUES.success, + 1 + ); + histogram.clear(); + + // Check that the result is in the table + verifyQuery( + "SELECT string, number, nuller, blober FROM test WHERE id = ?", + INTEGER, + [TEXT, REAL, null, BLOB] + ); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + TEST_DB_NAME, + TELEMETRY_VALUES.success, + 1 + ); + histogram.clear(); + + run_next_test(); +} + +function test_get_data() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM); + + var stmt = makeTestStatement( + "SELECT string, number, nuller, blober, id FROM test WHERE id = ?" + ); + stmt.bindByIndex(0, INTEGER); + execAsync(stmt, {}, [ + function (tuple) { + Assert.notEqual(null, tuple); + + // Check that it's what we expect + Assert.ok(!tuple.getIsNull(0)); + Assert.equal(tuple.getResultByName("string"), tuple.getResultByIndex(0)); + Assert.equal(TEXT, tuple.getResultByName("string")); + Assert.equal( + Ci.mozIStorageValueArray.VALUE_TYPE_TEXT, + tuple.getTypeOfIndex(0) + ); + + Assert.ok(!tuple.getIsNull(1)); + Assert.equal(tuple.getResultByName("number"), tuple.getResultByIndex(1)); + Assert.equal(REAL, tuple.getResultByName("number")); + Assert.equal( + Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT, + tuple.getTypeOfIndex(1) + ); + + Assert.ok(tuple.getIsNull(2)); + Assert.equal(tuple.getResultByName("nuller"), tuple.getResultByIndex(2)); + Assert.equal(null, tuple.getResultByName("nuller")); + Assert.equal( + Ci.mozIStorageValueArray.VALUE_TYPE_NULL, + tuple.getTypeOfIndex(2) + ); + + Assert.ok(!tuple.getIsNull(3)); + var blobByName = tuple.getResultByName("blober"); + Assert.equal(BLOB.length, blobByName.length); + var blobByIndex = tuple.getResultByIndex(3); + Assert.equal(BLOB.length, blobByIndex.length); + for (let i = 0; i < BLOB.length; i++) { + Assert.equal(BLOB[i], blobByName[i]); + Assert.equal(BLOB[i], blobByIndex[i]); + } + var count = { value: 0 }; + var blob = { value: null }; + tuple.getBlob(3, count, blob); + Assert.equal(BLOB.length, count.value); + for (let i = 0; i < BLOB.length; i++) { + Assert.equal(BLOB[i], blob.value[i]); + } + Assert.equal( + Ci.mozIStorageValueArray.VALUE_TYPE_BLOB, + tuple.getTypeOfIndex(3) + ); + + Assert.ok(!tuple.getIsNull(4)); + Assert.equal(tuple.getResultByName("id"), tuple.getResultByIndex(4)); + Assert.equal(INTEGER, tuple.getResultByName("id")); + Assert.equal( + Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER, + tuple.getTypeOfIndex(4) + ); + }, + ]); + stmt.finalize(); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + TEST_DB_NAME, + TELEMETRY_VALUES.success, + 1 + ); + histogram.clear(); + + run_next_test(); +} + +function test_tuple_out_of_bounds() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM); + + var stmt = makeTestStatement("SELECT string FROM test"); + execAsync(stmt, {}, [ + function (tuple) { + Assert.notEqual(null, tuple); + + // Check all out of bounds - should throw + var methods = [ + "getTypeOfIndex", + "getInt32", + "getInt64", + "getDouble", + "getUTF8String", + "getString", + "getIsNull", + ]; + for (var i in methods) { + try { + tuple[methods[i]](tuple.numEntries); + do_throw("did not throw :("); + } catch (e) { + Assert.equal(Cr.NS_ERROR_ILLEGAL_VALUE, e.result); + } + } + + // getBlob requires more args... + try { + var blob = { value: null }; + var size = { value: 0 }; + tuple.getBlob(tuple.numEntries, blob, size); + do_throw("did not throw :("); + } catch (e) { + Assert.equal(Cr.NS_ERROR_ILLEGAL_VALUE, e.result); + } + }, + ]); + stmt.finalize(); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + TEST_DB_NAME, + TELEMETRY_VALUES.success, + 1 + ); + histogram.clear(); + + run_next_test(); +} + +function test_no_listener_works_on_success() { + var stmt = makeTestStatement("DELETE FROM test WHERE id = ?"); + stmt.bindByIndex(0, 0); + stmt.executeAsync(); + stmt.finalize(); + + // Run the next test. + run_next_test(); +} + +function test_no_listener_works_on_results() { + var stmt = makeTestStatement("SELECT ?"); + stmt.bindByIndex(0, 1); + stmt.executeAsync(); + stmt.finalize(); + + // Run the next test. + run_next_test(); +} + +function test_no_listener_works_on_error() { + // commit without a transaction will trigger an error + var stmt = makeTestStatement("COMMIT"); + stmt.executeAsync(); + stmt.finalize(); + + // Run the next test. + run_next_test(); +} + +function test_partial_listener_works() { + var stmt = makeTestStatement("DELETE FROM test WHERE id = ?"); + stmt.bindByIndex(0, 0); + stmt.executeAsync({ + handleResult(aResultSet) {}, + }); + stmt.executeAsync({ + handleError(aError) {}, + }); + stmt.executeAsync({ + handleCompletion(aReason) {}, + }); + stmt.finalize(); + + // Run the next test. + run_next_test(); +} + +/** + * Dubious cancellation test that depends on system loading may or may not + * succeed in canceling things. It does at least test if calling cancel blows + * up. test_AsyncCancellation in test_true_async.cpp is our test that canceling + * actually works correctly. + */ +function test_immediate_cancellation() { + var stmt = makeTestStatement("DELETE FROM test WHERE id = ?"); + stmt.bindByIndex(0, 0); + execAsync(stmt, { cancel: true }); + stmt.finalize(); + run_next_test(); +} + +/** + * Test that calling cancel twice throws the second time. + */ +function test_double_cancellation() { + var stmt = makeTestStatement("DELETE FROM test WHERE id = ?"); + stmt.bindByIndex(0, 0); + let pendingStatement = execAsync(stmt, { cancel: true }); + // And cancel again - expect an exception + expectError(Cr.NS_ERROR_UNEXPECTED, () => pendingStatement.cancel()); + + stmt.finalize(); + run_next_test(); +} + +/** + * Verify that nothing untoward happens if we try and cancel something after it + * has fully run to completion. + */ +function test_cancellation_after_execution() { + var stmt = makeTestStatement("DELETE FROM test WHERE id = ?"); + stmt.bindByIndex(0, 0); + let pendingStatement = execAsync(stmt, { returnPending: true }); + // (the statement has fully executed at this point) + // canceling after the statement has run to completion should not throw! + pendingStatement.cancel(); + + stmt.finalize(); + run_next_test(); +} + +/** + * Verifies that a single statement can be executed more than once. Might once + * have been intended to also ensure that callback notifications were not + * incorrectly interleaved, but that part was brittle (it's totally fine for + * handleResult to get called multiple times) and not comprehensive. + */ +function test_double_execute() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM); + + var stmt = makeTestStatement("SELECT 1"); + execAsync(stmt, null, 1); + execAsync(stmt, null, 1); + stmt.finalize(); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + TEST_DB_NAME, + TELEMETRY_VALUES.success, + 2 + ); + histogram.clear(); + + run_next_test(); +} + +function test_finalized_statement_does_not_crash() { + var stmt = makeTestStatement("SELECT * FROM TEST"); + stmt.finalize(); + // we are concerned about a crash here; an error is fine. + try { + stmt.executeAsync(); + } catch (ex) { + // Do nothing. + } + + // Run the next test. + run_next_test(); +} + +/** + * Bind by mozIStorageBindingParams on the mozIStorageBaseStatement by index. + */ +function test_bind_direct_binding_params_by_index() { + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (?, ?, ?, ?, ?)" + ); + let insertId = nextUniqueId++; + stmt.bindByIndex(0, insertId); + stmt.bindByIndex(1, TEXT); + stmt.bindByIndex(2, REAL); + stmt.bindByIndex(3, null); + stmt.bindBlobByIndex(4, BLOB, BLOB.length); + execAsync(stmt); + stmt.finalize(); + verifyQuery( + "SELECT string, number, nuller, blober FROM test WHERE id = ?", + insertId, + [TEXT, REAL, null, BLOB] + ); + run_next_test(); +} + +/** + * Bind by mozIStorageBindingParams on the mozIStorageBaseStatement by name. + */ +function test_bind_direct_binding_params_by_name() { + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (:int, :text, :real, :null, :blob)" + ); + let insertId = nextUniqueId++; + stmt.bindByName("int", insertId); + stmt.bindByName("text", TEXT); + stmt.bindByName("real", REAL); + stmt.bindByName("null", null); + stmt.bindBlobByName("blob", BLOB); + execAsync(stmt); + stmt.finalize(); + verifyQuery( + "SELECT string, number, nuller, blober FROM test WHERE id = ?", + insertId, + [TEXT, REAL, null, BLOB] + ); + run_next_test(); +} + +function test_bind_js_params_helper_by_index() { + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (?, ?, ?, ?, NULL)" + ); + let insertId = nextUniqueId++; + // we cannot bind blobs this way; no blober + stmt.params[3] = null; + stmt.params[2] = REAL; + stmt.params[1] = TEXT; + stmt.params[0] = insertId; + execAsync(stmt); + stmt.finalize(); + verifyQuery( + "SELECT string, number, nuller FROM test WHERE id = ?", + insertId, + [TEXT, REAL, null] + ); + run_next_test(); +} + +function test_bind_js_params_helper_by_name() { + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (:int, :text, :real, :null, NULL)" + ); + let insertId = nextUniqueId++; + // we cannot bind blobs this way; no blober + stmt.params.null = null; + stmt.params.real = REAL; + stmt.params.text = TEXT; + stmt.params.int = insertId; + execAsync(stmt); + stmt.finalize(); + verifyQuery( + "SELECT string, number, nuller FROM test WHERE id = ?", + insertId, + [TEXT, REAL, null] + ); + run_next_test(); +} + +function test_bind_multiple_rows_by_index() { + const AMOUNT_TO_ADD = 5; + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (?, ?, ?, ?, ?)" + ); + var array = stmt.newBindingParamsArray(); + for (let i = 0; i < AMOUNT_TO_ADD; i++) { + let bp = array.newBindingParams(); + bp.bindByIndex(0, INTEGER); + bp.bindByIndex(1, TEXT); + bp.bindByIndex(2, REAL); + bp.bindByIndex(3, null); + bp.bindBlobByIndex(4, BLOB, BLOB.length); + array.addParams(bp); + Assert.equal(array.length, i + 1); + } + stmt.bindParameters(array); + + let rowCount = getTableRowCount("test"); + execAsync(stmt); + Assert.equal(rowCount + AMOUNT_TO_ADD, getTableRowCount("test")); + stmt.finalize(); + run_next_test(); +} + +function test_bind_multiple_rows_by_name() { + const AMOUNT_TO_ADD = 5; + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (:int, :text, :real, :null, :blob)" + ); + var array = stmt.newBindingParamsArray(); + for (let i = 0; i < AMOUNT_TO_ADD; i++) { + let bp = array.newBindingParams(); + bp.bindByName("int", INTEGER); + bp.bindByName("text", TEXT); + bp.bindByName("real", REAL); + bp.bindByName("null", null); + bp.bindBlobByName("blob", BLOB); + array.addParams(bp); + Assert.equal(array.length, i + 1); + } + stmt.bindParameters(array); + + let rowCount = getTableRowCount("test"); + execAsync(stmt); + Assert.equal(rowCount + AMOUNT_TO_ADD, getTableRowCount("test")); + stmt.finalize(); + run_next_test(); +} + +/** + * Verify that a mozIStorageStatement instance throws immediately when we + * try and bind to an illegal index. + */ +function test_bind_out_of_bounds_sync_immediate() { + let stmt = makeTestStatement("INSERT INTO test (id) VALUES (?)"); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + + // Check variant binding. + expectError(Cr.NS_ERROR_INVALID_ARG, () => bp.bindByIndex(1, INTEGER)); + // Check blob binding. + expectError(Cr.NS_ERROR_INVALID_ARG, () => + bp.bindBlobByIndex(1, BLOB, BLOB.length) + ); + + stmt.finalize(); + run_next_test(); +} +test_bind_out_of_bounds_sync_immediate.syncOnly = true; + +/** + * Verify that a mozIStorageAsyncStatement reports an error asynchronously when + * we bind to an illegal index. + */ +function test_bind_out_of_bounds_async_deferred() { + let stmt = makeTestStatement("INSERT INTO test (id) VALUES (?)"); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + + // There is no difference between variant and blob binding for async purposes. + bp.bindByIndex(1, INTEGER); + array.addParams(bp); + stmt.bindParameters(array); + execAsync(stmt, { error: Ci.mozIStorageError.RANGE }); + + stmt.finalize(); + run_next_test(); +} +test_bind_out_of_bounds_async_deferred.asyncOnly = true; + +function test_bind_no_such_name_sync_immediate() { + let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:foo)"); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + + // Check variant binding. + expectError(Cr.NS_ERROR_INVALID_ARG, () => + bp.bindByName("doesnotexist", INTEGER) + ); + // Check blob binding. + expectError(Cr.NS_ERROR_INVALID_ARG, () => + bp.bindBlobByName("doesnotexist", BLOB) + ); + + stmt.finalize(); + run_next_test(); +} +test_bind_no_such_name_sync_immediate.syncOnly = true; + +function test_bind_no_such_name_async_deferred() { + let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:foo)"); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + + bp.bindByName("doesnotexist", INTEGER); + array.addParams(bp); + stmt.bindParameters(array); + execAsync(stmt, { error: Ci.mozIStorageError.RANGE }); + + stmt.finalize(); + run_next_test(); +} +test_bind_no_such_name_async_deferred.asyncOnly = true; + +function test_bind_bogus_type_by_index() { + // We try to bind a JS Object here that should fail to bind. + let stmt = makeTestStatement("INSERT INTO test (blober) VALUES (?)"); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + Assert.throws(() => bp.bindByIndex(0, run_test), /NS_ERROR_UNEXPECTED/); + + stmt.finalize(); + run_next_test(); +} + +function test_bind_bogus_type_by_name() { + // We try to bind a JS Object here that should fail to bind. + let stmt = makeTestStatement("INSERT INTO test (blober) VALUES (:blob)"); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + Assert.throws(() => bp.bindByName("blob", run_test), /NS_ERROR_UNEXPECTED/); + + stmt.finalize(); + run_next_test(); +} + +function test_bind_params_already_locked() { + let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)"); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + bp.bindByName("int", INTEGER); + array.addParams(bp); + + // We should get an error after we call addParams and try to bind again. + expectError(Cr.NS_ERROR_UNEXPECTED, () => bp.bindByName("int", INTEGER)); + + stmt.finalize(); + run_next_test(); +} + +function test_bind_params_array_already_locked() { + let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)"); + + let array = stmt.newBindingParamsArray(); + let bp1 = array.newBindingParams(); + bp1.bindByName("int", INTEGER); + array.addParams(bp1); + let bp2 = array.newBindingParams(); + stmt.bindParameters(array); + bp2.bindByName("int", INTEGER); + + // We should get an error after we have bound the array to the statement. + expectError(Cr.NS_ERROR_UNEXPECTED, () => array.addParams(bp2)); + + stmt.finalize(); + run_next_test(); +} + +function test_no_binding_params_from_locked_array() { + let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)"); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + bp.bindByName("int", INTEGER); + array.addParams(bp); + stmt.bindParameters(array); + + // We should not be able to get a new BindingParams object after we have bound + // to the statement. + expectError(Cr.NS_ERROR_UNEXPECTED, () => array.newBindingParams()); + + stmt.finalize(); + run_next_test(); +} + +function test_not_right_owning_array() { + let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)"); + + let array1 = stmt.newBindingParamsArray(); + let array2 = stmt.newBindingParamsArray(); + let bp = array1.newBindingParams(); + bp.bindByName("int", INTEGER); + + // We should not be able to add bp to array2 since it was created from array1. + expectError(Cr.NS_ERROR_UNEXPECTED, () => array2.addParams(bp)); + + stmt.finalize(); + run_next_test(); +} + +function test_not_right_owning_statement() { + let stmt1 = makeTestStatement("INSERT INTO test (id) VALUES (:int)"); + let stmt2 = makeTestStatement("INSERT INTO test (id) VALUES (:int)"); + + let array1 = stmt1.newBindingParamsArray(); + stmt2.newBindingParamsArray(); + let bp = array1.newBindingParams(); + bp.bindByName("int", INTEGER); + array1.addParams(bp); + + // We should not be able to bind array1 since it was created from stmt1. + expectError(Cr.NS_ERROR_UNEXPECTED, () => stmt2.bindParameters(array1)); + + stmt1.finalize(); + stmt2.finalize(); + run_next_test(); +} + +function test_bind_empty_array() { + let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)"); + + let paramsArray = stmt.newBindingParamsArray(); + + // We should not be able to bind this array to the statement because it is + // empty. + expectError(Cr.NS_ERROR_UNEXPECTED, () => stmt.bindParameters(paramsArray)); + + stmt.finalize(); + run_next_test(); +} + +function test_multiple_results() { + let expectedResults = getTableRowCount("test"); + // Sanity check - we should have more than one result, but let's be sure. + Assert.ok(expectedResults > 1); + + // Now check that we get back two rows of data from our async query. + let stmt = makeTestStatement("SELECT * FROM test"); + execAsync(stmt, {}, expectedResults); + + stmt.finalize(); + run_next_test(); +} + +// Test Runner + +const TEST_PASS_SYNC = 0; +const TEST_PASS_ASYNC = 1; +/** + * We run 2 passes against the test. One where makeTestStatement generates + * synchronous (mozIStorageStatement) statements and one where it generates + * asynchronous (mozIStorageAsyncStatement) statements. + * + * Because of differences in the ability to know the number of parameters before + * dispatching, some tests are sync/async specific. These functions are marked + * with 'syncOnly' or 'asyncOnly' attributes and run_next_test knows what to do. + */ +var testPass = TEST_PASS_SYNC; + +/** + * Create a statement of the type under test per testPass. + * + * @param aSQL + * The SQL string from which to build a statement. + * @return a statement of the type under test per testPass. + */ +function makeTestStatement(aSQL) { + if (testPass == TEST_PASS_SYNC) { + return getOpenedDatabase().createStatement(aSQL); + } + return getOpenedDatabase().createAsyncStatement(aSQL); +} + +var tests = [ + test_illegal_sql_async_deferred, + test_create_table, + test_add_data, + test_get_data, + test_tuple_out_of_bounds, + test_no_listener_works_on_success, + test_no_listener_works_on_results, + test_no_listener_works_on_error, + test_partial_listener_works, + test_immediate_cancellation, + test_double_cancellation, + test_cancellation_after_execution, + test_double_execute, + test_finalized_statement_does_not_crash, + test_bind_direct_binding_params_by_index, + test_bind_direct_binding_params_by_name, + test_bind_js_params_helper_by_index, + test_bind_js_params_helper_by_name, + test_bind_multiple_rows_by_index, + test_bind_multiple_rows_by_name, + test_bind_out_of_bounds_sync_immediate, + test_bind_out_of_bounds_async_deferred, + test_bind_no_such_name_sync_immediate, + test_bind_no_such_name_async_deferred, + test_bind_bogus_type_by_index, + test_bind_bogus_type_by_name, + test_bind_params_already_locked, + test_bind_params_array_already_locked, + test_bind_empty_array, + test_no_binding_params_from_locked_array, + test_not_right_owning_array, + test_not_right_owning_statement, + test_multiple_results, +]; +var index = 0; + +const STARTING_UNIQUE_ID = 2; +var nextUniqueId = STARTING_UNIQUE_ID; + +function run_next_test() { + function _run_next_test() { + // use a loop so we can skip tests... + while (index < tests.length) { + let test = tests[index++]; + // skip tests not appropriate to the current test pass + if ( + (testPass == TEST_PASS_SYNC && "asyncOnly" in test) || + (testPass == TEST_PASS_ASYNC && "syncOnly" in test) + ) { + continue; + } + + // Asynchronous tests means that exceptions don't kill the test. + try { + print("****** Running the next test: " + test.name); + test(); + return; + } catch (e) { + do_throw(e); + } + } + + // if we only completed the first pass, move to the next pass + if (testPass == TEST_PASS_SYNC) { + print("********* Beginning mozIStorageAsyncStatement pass."); + testPass++; + index = 0; + // a new pass demands a new database + asyncCleanup(); + nextUniqueId = STARTING_UNIQUE_ID; + _run_next_test(); + return; + } + + // we did some async stuff; we need to clean up. + asyncCleanup(); + do_test_finished(); + } + + // Don't actually schedule another test if we're quitting. + if (!_quit) { + // For saner stacks, we execute this code RSN. + executeSoon(_run_next_test); + } +} + +function run_test() { + // Thunderbird doesn't have one or more of the probes used in this test. + // Ensure the data is collected anyway. + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + cleanup(); + + do_test_pending(); + run_next_test(); +} diff --git a/storage/test/unit/test_statement_wrapper_automatically.js b/storage/test/unit/test_statement_wrapper_automatically.js new file mode 100644 index 0000000000..8ff203f7c7 --- /dev/null +++ b/storage/test/unit/test_statement_wrapper_automatically.js @@ -0,0 +1,166 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + vim:set ts=2 sw=2 sts=2 et: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the functions of mozIStorageStatementWrapper + +function setup() { + getOpenedDatabase().createTable( + "test", + "id INTEGER PRIMARY KEY, val NONE,alt_val NONE" + ); +} + +/** + * A convenience wrapper for do_check_eq. Calls do_check_eq on aActualVal + * and aReturnedVal, with one caveat. + * + * Date objects are converted before parameter binding to PRTime's (microsecs + * since epoch). They are not reconverted when retrieved from the database. + * This function abstracts away this reconversion so that you can pass in, + * for example: + * + * checkVal(new Date(), aReturnedVal) // this + * checkVal(new Date().valueOf() * 1000.0, aReturnedVal) // instead of this + * + * Should any other types require conversion in the future, their conversions + * may also be abstracted away here. + * + * @param aActualVal + * the value inserted into the database + * @param aReturnedVal + * the value retrieved from the database + */ +function checkVal(aActualVal, aReturnedVal) { + if (aActualVal instanceof Date) { + aActualVal = aActualVal.valueOf() * 1000.0; + } + Assert.equal(aActualVal, aReturnedVal); +} + +/** + * Removes all rows from our test table. + */ +function clearTable() { + var stmt = createStatement("DELETE FROM test"); + stmt.execute(); + stmt.finalize(); + ensureNumRows(0); +} + +/** + * Ensures that the number of rows in our test table is equal to aNumRows. + * Calls do_check_eq on aNumRows and the value retrieved by SELECT'ing COUNT(*). + * + * @param aNumRows + * the number of rows our test table should contain + */ +function ensureNumRows(aNumRows) { + var stmt = createStatement("SELECT COUNT(*) AS number FROM test"); + Assert.ok(stmt.step()); + Assert.equal(aNumRows, stmt.row.number); + stmt.reset(); + stmt.finalize(); +} + +/** + * Inserts aVal into our test table and checks that insertion was successful by + * retrieving the newly inserted value from the database and comparing it + * against aVal. aVal is bound to a single parameter. + * + * @param aVal + * value to insert into our test table and check + */ +function insertAndCheckSingleParam(aVal) { + clearTable(); + + var stmt = createStatement("INSERT INTO test (val) VALUES (:val)"); + stmt.params.val = aVal; + stmt.execute(); + stmt.finalize(); + + ensureNumRows(1); + + stmt = createStatement("SELECT val FROM test WHERE id = 1"); + Assert.ok(stmt.step()); + checkVal(aVal, stmt.row.val); + stmt.reset(); + stmt.finalize(); +} + +/** + * Inserts aVal into our test table and checks that insertion was successful by + * retrieving the newly inserted value from the database and comparing it + * against aVal. aVal is bound to two separate parameters, both of which are + * checked against aVal. + * + * @param aVal + * value to insert into our test table and check + */ +function insertAndCheckMultipleParams(aVal) { + clearTable(); + + var stmt = createStatement( + "INSERT INTO test (val, alt_val) VALUES (:val, :val)" + ); + stmt.params.val = aVal; + stmt.execute(); + stmt.finalize(); + + ensureNumRows(1); + + stmt = createStatement("SELECT val, alt_val FROM test WHERE id = 1"); + Assert.ok(stmt.step()); + checkVal(aVal, stmt.row.val); + checkVal(aVal, stmt.row.alt_val); + stmt.reset(); + stmt.finalize(); +} + +/** + * A convenience function that prints out a description of aVal using + * aVal.toString and aVal.toSource. Output is useful when the test fails. + * + * @param aVal + * a value inserted or to be inserted into our test table + */ +function printValDesc(aVal) { + try { + var toSource = aVal.toSource(); + } catch (ex) { + toSource = ""; + } + print( + "Testing value: toString=" + + aVal + + (toSource ? " toSource=" + toSource : "") + ); +} + +function run_test() { + setup(); + + // function JSValStorageStatementBinder in + // storage/mozStorageStatementParams.cpp tells us that the following types + // and only the following types are valid as statement parameters: + var vals = [ + 1337, // int + 3.1337, // double + "foo", // string + true, // boolean + null, // null + new Date(), // Date object + ]; + + vals.forEach(function (val) { + printValDesc(val); + print("Single parameter"); + insertAndCheckSingleParam(val); + print("Multiple parameters"); + insertAndCheckMultipleParams(val); + }); + + cleanup(); +} diff --git a/storage/test/unit/test_storage_connection.js b/storage/test/unit/test_storage_connection.js new file mode 100644 index 0000000000..e7175a9613 --- /dev/null +++ b/storage/test/unit/test_storage_connection.js @@ -0,0 +1,997 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the functions of mozIStorageConnection + +function fetchAllNames(conn) { + let names = []; + let stmt = conn.createStatement(`SELECT name FROM test ORDER BY name`); + while (stmt.executeStep()) { + names.push(stmt.getUTF8String(0)); + } + stmt.finalize(); + return names; +} + +// Test Functions + +add_task(async function test_connectionReady_open() { + // there doesn't seem to be a way for the connection to not be ready (unless + // we close it with mozIStorageConnection::Close(), but we don't for this). + // It can only fail if GetPath fails on the database file, or if we run out + // of memory trying to use an in-memory database + + var msc = getOpenedDatabase(); + Assert.ok(msc.connectionReady); +}); + +add_task(async function test_connectionReady_closed() { + // This also tests mozIStorageConnection::Close() + + var msc = getOpenedDatabase(); + msc.close(); + Assert.ok(!msc.connectionReady); + gDBConn = null; // this is so later tests don't start to fail. +}); + +add_task(async function test_databaseFile() { + var msc = getOpenedDatabase(); + Assert.ok(getTestDB().equals(msc.databaseFile)); +}); + +add_task(async function test_tableExists_not_created() { + var msc = getOpenedDatabase(); + Assert.ok(!msc.tableExists("foo")); +}); + +add_task(async function test_indexExists_not_created() { + var msc = getOpenedDatabase(); + Assert.ok(!msc.indexExists("foo")); +}); + +add_task(async function test_temp_tableExists_and_indexExists() { + var msc = getOpenedDatabase(); + msc.executeSimpleSQL( + "CREATE TEMP TABLE test_temp(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)" + ); + Assert.ok(msc.tableExists("test_temp")); + + msc.executeSimpleSQL("CREATE INDEX test_temp_ind ON test_temp (name)"); + Assert.ok(msc.indexExists("test_temp_ind")); + + msc.executeSimpleSQL("DROP INDEX test_temp_ind"); + msc.executeSimpleSQL("DROP TABLE test_temp"); +}); + +add_task(async function test_createTable_not_created() { + var msc = getOpenedDatabase(); + msc.createTable("test", "id INTEGER PRIMARY KEY, name TEXT"); + Assert.ok(msc.tableExists("test")); +}); + +add_task(async function test_indexExists_created() { + var msc = getOpenedDatabase(); + msc.executeSimpleSQL("CREATE INDEX name_ind ON test (name)"); + Assert.ok(msc.indexExists("name_ind")); +}); + +add_task(async function test_createTable_already_created() { + var msc = getOpenedDatabase(); + Assert.ok(msc.tableExists("test")); + Assert.throws( + () => msc.createTable("test", "id INTEGER PRIMARY KEY, name TEXT"), + /NS_ERROR_FAILURE/ + ); +}); + +add_task(async function test_attach_createTable_tableExists_indexExists() { + var msc = getOpenedDatabase(); + var file = do_get_file("storage_attach.sqlite", true); + var msc2 = getDatabase(file); + msc.executeSimpleSQL("ATTACH DATABASE '" + file.path + "' AS sample"); + + Assert.ok(!msc.tableExists("sample.test")); + msc.createTable("sample.test", "id INTEGER PRIMARY KEY, name TEXT"); + Assert.ok(msc.tableExists("sample.test")); + Assert.throws( + () => msc.createTable("sample.test", "id INTEGER PRIMARY KEY, name TEXT"), + /NS_ERROR_FAILURE/ + ); + + Assert.ok(!msc.indexExists("sample.test_ind")); + msc.executeSimpleSQL("CREATE INDEX sample.test_ind ON test (name)"); + Assert.ok(msc.indexExists("sample.test_ind")); + + msc.executeSimpleSQL("DETACH DATABASE sample"); + msc2.close(); + try { + file.remove(false); + } catch (e) { + // Do nothing. + } +}); + +add_task(async function test_lastInsertRowID() { + var msc = getOpenedDatabase(); + msc.executeSimpleSQL("INSERT INTO test (name) VALUES ('foo')"); + Assert.equal(1, msc.lastInsertRowID); +}); + +add_task(async function test_transactionInProgress_no() { + var msc = getOpenedDatabase(); + Assert.ok(!msc.transactionInProgress); +}); + +add_task(async function test_transactionInProgress_yes() { + var msc = getOpenedDatabase(); + msc.beginTransaction(); + Assert.ok(msc.transactionInProgress); + msc.commitTransaction(); + Assert.ok(!msc.transactionInProgress); + + msc.beginTransaction(); + Assert.ok(msc.transactionInProgress); + msc.rollbackTransaction(); + Assert.ok(!msc.transactionInProgress); +}); + +add_task(async function test_commitTransaction_no_transaction() { + var msc = getOpenedDatabase(); + Assert.ok(!msc.transactionInProgress); + Assert.throws(() => msc.commitTransaction(), /NS_ERROR_UNEXPECTED/); +}); + +add_task(async function test_rollbackTransaction_no_transaction() { + var msc = getOpenedDatabase(); + Assert.ok(!msc.transactionInProgress); + Assert.throws(() => msc.rollbackTransaction(), /NS_ERROR_UNEXPECTED/); +}); + +add_task(async function test_get_schemaVersion_not_set() { + Assert.equal(0, getOpenedDatabase().schemaVersion); +}); + +add_task(async function test_set_schemaVersion() { + var msc = getOpenedDatabase(); + const version = 1; + msc.schemaVersion = version; + Assert.equal(version, msc.schemaVersion); +}); + +add_task(async function test_set_schemaVersion_same() { + var msc = getOpenedDatabase(); + const version = 1; + msc.schemaVersion = version; // should still work ok + Assert.equal(version, msc.schemaVersion); +}); + +add_task(async function test_set_schemaVersion_negative() { + var msc = getOpenedDatabase(); + const version = -1; + msc.schemaVersion = version; + Assert.equal(version, msc.schemaVersion); +}); + +add_task(async function test_createTable() { + var temp = getTestDB().parent; + temp.append("test_db_table"); + try { + var con = Services.storage.openDatabase(temp); + con.createTable("a", ""); + } catch (e) { + if (temp.exists()) { + try { + temp.remove(false); + } catch (e2) { + // Do nothing. + } + } + Assert.ok( + e.result == Cr.NS_ERROR_NOT_INITIALIZED || e.result == Cr.NS_ERROR_FAILURE + ); + } finally { + if (con) { + con.close(); + } + } +}); + +add_task(async function test_defaultSynchronousAtNormal() { + getOpenedDatabase(); + var stmt = createStatement("PRAGMA synchronous;"); + try { + stmt.executeStep(); + Assert.equal(1, stmt.getInt32(0)); + } finally { + stmt.reset(); + stmt.finalize(); + } +}); + +// must be ran before executeAsync tests +add_task(async function test_close_does_not_spin_event_loop() { + // We want to make sure that the event loop on the calling thread does not + // spin when close is called. + let event = { + ran: false, + run() { + this.ran = true; + }, + }; + + // Post the event before we call close, so it would run if the event loop was + // spun during close. + Services.tm.dispatchToMainThread(event); + + // Sanity check, then close the database. Afterwards, we should not have ran! + Assert.ok(!event.ran); + getOpenedDatabase().close(); + Assert.ok(!event.ran); + + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; +}); + +add_task( + async function test_asyncClose_succeeds_with_finalized_async_statement() { + // XXX this test isn't perfect since we can't totally control when events will + // run. If this paticular function fails randomly, it means we have a + // real bug. + + // We want to make sure we create a cached async statement to make sure that + // when we finalize our statement, we end up finalizing the async one too so + // close will succeed. + let stmt = createStatement("SELECT * FROM test"); + stmt.executeAsync(); + stmt.finalize(); + + await asyncClose(getOpenedDatabase()); + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; + } +); + +// Would assert on debug builds. +if (!AppConstants.DEBUG) { + add_task(async function test_close_then_release_statement() { + // Testing the behavior in presence of a bad client that finalizes + // statements after the database has been closed (typically by + // letting the gc finalize the statement). + let db = getOpenedDatabase(); + let stmt = createStatement( + "SELECT * FROM test -- test_close_then_release_statement" + ); + db.close(); + stmt.finalize(); // Finalize too late - this should not crash + + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; + }); + + add_task(async function test_asyncClose_then_release_statement() { + // Testing the behavior in presence of a bad client that finalizes + // statements after the database has been async closed (typically by + // letting the gc finalize the statement). + let db = getOpenedDatabase(); + let stmt = createStatement( + "SELECT * FROM test -- test_asyncClose_then_release_statement" + ); + await asyncClose(db); + stmt.finalize(); // Finalize too late - this should not crash + + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; + }); +} + +// In debug builds this would cause a fatal assertion. +if (!AppConstants.DEBUG) { + add_task(async function test_close_fails_with_async_statement_ran() { + let stmt = createStatement("SELECT * FROM test"); + stmt.executeAsync(); + stmt.finalize(); + + let db = getOpenedDatabase(); + Assert.throws(() => db.close(), /NS_ERROR_UNEXPECTED/); + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; + }); +} + +add_task(async function test_clone_optional_param() { + let db1 = Services.storage.openUnsharedDatabase(getTestDB()); + let db2 = db1.clone(); + Assert.ok(db2.connectionReady); + + // A write statement should not fail here. + let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)"); + stmt.params.name = "dwitte"; + stmt.execute(); + stmt.finalize(); + + // And a read statement should succeed. + stmt = db2.createStatement("SELECT * FROM test"); + Assert.ok(stmt.executeStep()); + stmt.finalize(); + + // Additionally check that it is a connection on the same database. + Assert.ok(db1.databaseFile.equals(db2.databaseFile)); + + db1.close(); + db2.close(); +}); + +async function standardAsyncTest(promisedDB, name, shouldInit = false) { + info("Performing standard async test " + name); + + let adb = await promisedDB; + Assert.ok(adb instanceof Ci.mozIStorageAsyncConnection); + Assert.ok(adb instanceof Ci.mozIStorageConnection); + + if (shouldInit) { + let stmt = adb.createAsyncStatement("CREATE TABLE test(name TEXT)"); + await executeAsync(stmt); + stmt.finalize(); + } + + // Generate a name to insert and fetch back + name = "worker bee " + Math.random() + " (" + name + ")"; + + let stmt = adb.createAsyncStatement("INSERT INTO test (name) VALUES (:name)"); + stmt.params.name = name; + let result = await executeAsync(stmt); + info("Request complete"); + stmt.finalize(); + Assert.ok(Components.isSuccessCode(result)); + info("Extracting data"); + stmt = adb.createAsyncStatement("SELECT * FROM test"); + let found = false; + await executeAsync(stmt, function (results) { + info("Data has been extracted"); + for ( + let row = results.getNextRow(); + row != null; + row = results.getNextRow() + ) { + if (row.getResultByName("name") == name) { + found = true; + break; + } + } + }); + Assert.ok(found); + stmt.finalize(); + await asyncClose(adb); + + info("Standard async test " + name + " complete"); +} + +add_task(async function test_open_async() { + await standardAsyncTest(openAsyncDatabase(getTestDB(), null), "default"); + await standardAsyncTest(openAsyncDatabase(getTestDB()), "no optional arg"); + await standardAsyncTest( + openAsyncDatabase(getTestDB(), { shared: false, interruptible: true }), + "non-default options" + ); + await standardAsyncTest( + openAsyncDatabase("memory"), + "in-memory database", + true + ); + await standardAsyncTest( + openAsyncDatabase("memory", { shared: false }), + "in-memory database and options", + true + ); + + info("Testing async opening with readonly option"); + const impliedReadOnlyOption = { ignoreLockingMode: true }; + + let raised = false; + let adb = await openAsyncDatabase(getTestDB(), impliedReadOnlyOption); + let stmt = adb.createAsyncStatement("CREATE TABLE test(name TEXT)"); + try { + await executeAsync(stmt); // This should throw + } catch (e) { + raised = true; + } finally { + if (stmt) { + stmt.finalize(); + } + if (adb) { + await asyncClose(adb); + } + } + + Assert.ok(raised); +}); + +add_task(async function test_async_open_with_shared_cache() { + info("Testing that opening with a shared cache doesn't break stuff"); + let adb = await openAsyncDatabase(getTestDB(), { shared: true }); + + let stmt = adb.createAsyncStatement("INSERT INTO test (name) VALUES (:name)"); + stmt.params.name = "clockworker"; + let result = await executeAsync(stmt); + info("Request complete"); + stmt.finalize(); + Assert.ok(Components.isSuccessCode(result)); + info("Extracting data"); + stmt = adb.createAsyncStatement("SELECT * FROM test"); + let found = false; + await executeAsync(stmt, function (results) { + info("Data has been extracted"); + for ( + let row = results.getNextRow(); + row != null; + row = results.getNextRow() + ) { + if (row.getResultByName("name") == "clockworker") { + found = true; + break; + } + } + }); + Assert.ok(found); + stmt.finalize(); + await asyncClose(adb); +}); + +add_task(async function test_clone_trivial_async() { + info("Open connection"); + let db = Services.storage.openDatabase(getTestDB()); + Assert.ok(db instanceof Ci.mozIStorageAsyncConnection); + info("AsyncClone connection"); + let clone = await asyncClone(db, true); + Assert.ok(clone instanceof Ci.mozIStorageAsyncConnection); + Assert.ok(clone instanceof Ci.mozIStorageConnection); + info("Close connection"); + await asyncClose(db); + info("Close clone"); + await asyncClose(clone); +}); + +add_task(async function test_clone_no_optional_param_async() { + "use strict"; + info("Testing async cloning"); + let adb1 = await openAsyncDatabase(getTestDB(), null); + Assert.ok(adb1 instanceof Ci.mozIStorageAsyncConnection); + Assert.ok(adb1 instanceof Ci.mozIStorageConnection); + + info("Cloning database"); + + let adb2 = await asyncClone(adb1); + info( + "Testing that the cloned db is a mozIStorageAsyncConnection " + + "and not a mozIStorageConnection" + ); + Assert.ok(adb2 instanceof Ci.mozIStorageAsyncConnection); + Assert.ok(adb2 instanceof Ci.mozIStorageConnection); + + info("Inserting data into source db"); + let stmt = adb1.createAsyncStatement( + "INSERT INTO test (name) VALUES (:name)" + ); + + stmt.params.name = "yoric"; + let result = await executeAsync(stmt); + info("Request complete"); + stmt.finalize(); + Assert.ok(Components.isSuccessCode(result)); + info("Extracting data from clone db"); + stmt = adb2.createAsyncStatement("SELECT * FROM test"); + let found = false; + await executeAsync(stmt, function (results) { + info("Data has been extracted"); + for ( + let row = results.getNextRow(); + row != null; + row = results.getNextRow() + ) { + if (row.getResultByName("name") == "yoric") { + found = true; + break; + } + } + }); + Assert.ok(found); + stmt.finalize(); + info("Closing databases"); + await asyncClose(adb2); + info("First db closed"); + + await asyncClose(adb1); + info("Second db closed"); +}); + +add_task(async function test_clone_readonly() { + let db1 = Services.storage.openUnsharedDatabase(getTestDB()); + let db2 = db1.clone(true); + Assert.ok(db2.connectionReady); + + // A write statement should fail here. + let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)"); + stmt.params.name = "reed"; + expectError(Cr.NS_ERROR_FILE_READ_ONLY, () => stmt.execute()); + stmt.finalize(); + + // And a read statement should succeed. + stmt = db2.createStatement("SELECT * FROM test"); + Assert.ok(stmt.executeStep()); + stmt.finalize(); + + db1.close(); + db2.close(); +}); + +add_task(async function test_clone_shared_readonly() { + let db1 = Services.storage.openDatabase(getTestDB()); + let db2 = db1.clone(true); + Assert.ok(db2.connectionReady); + + let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)"); + stmt.params.name = "parker"; + // TODO currently SQLite does not actually work correctly here. The behavior + // we want is commented out, and the current behavior is being tested + // for. Our IDL comments will have to be updated when this starts to + // work again. + stmt.execute(); + // expectError(Components.results.NS_ERROR_FILE_READ_ONLY, () => stmt.execute()); + stmt.finalize(); + + // And a read statement should succeed. + stmt = db2.createStatement("SELECT * FROM test"); + Assert.ok(stmt.executeStep()); + stmt.finalize(); + + db1.close(); + db2.close(); +}); + +add_task(async function test_close_clone_fails() { + let calls = ["openDatabase", "openUnsharedDatabase"]; + calls.forEach(function (methodName) { + let db = Services.storage[methodName](getTestDB()); + db.close(); + expectError(Cr.NS_ERROR_NOT_INITIALIZED, () => db.clone()); + }); +}); + +add_task(async function test_clone_copies_functions() { + const FUNC_NAME = "test_func"; + let calls = ["openDatabase", "openUnsharedDatabase"]; + let functionMethods = ["createFunction"]; + calls.forEach(function (methodName) { + [true, false].forEach(function (readOnly) { + functionMethods.forEach(function (functionMethod) { + let db1 = Services.storage[methodName](getTestDB()); + // Create a function for db1. + db1[functionMethod](FUNC_NAME, 1, { + onFunctionCall: () => 0, + onStep: () => 0, + onFinal: () => 0, + }); + + // Clone it, and make sure the function exists still. + let db2 = db1.clone(readOnly); + // Note: this would fail if the function did not exist. + let stmt = db2.createStatement( + "SELECT " + FUNC_NAME + "(id) FROM test" + ); + stmt.finalize(); + db1.close(); + db2.close(); + }); + }); + }); +}); + +add_task(async function test_clone_copies_overridden_functions() { + const FUNC_NAME = "lower"; + function test_func() { + this.called = false; + } + test_func.prototype = { + onFunctionCall() { + this.called = true; + }, + onStep() { + this.called = true; + }, + onFinal: () => 0, + }; + + let calls = ["openDatabase", "openUnsharedDatabase"]; + let functionMethods = ["createFunction"]; + calls.forEach(function (methodName) { + [true, false].forEach(function (readOnly) { + functionMethods.forEach(function (functionMethod) { + let db1 = Services.storage[methodName](getTestDB()); + // Create a function for db1. + let func = new test_func(); + db1[functionMethod](FUNC_NAME, 1, func); + Assert.ok(!func.called); + + // Clone it, and make sure the function gets called. + let db2 = db1.clone(readOnly); + let stmt = db2.createStatement( + "SELECT " + FUNC_NAME + "(id) FROM test" + ); + stmt.executeStep(); + Assert.ok(func.called); + stmt.finalize(); + db1.close(); + db2.close(); + }); + }); + }); +}); + +add_task(async function test_clone_copies_pragmas() { + const PRAGMAS = [ + { name: "cache_size", value: 500, copied: true }, + { name: "temp_store", value: 2, copied: true }, + { name: "foreign_keys", value: 1, copied: true }, + { name: "journal_size_limit", value: 524288, copied: true }, + { name: "synchronous", value: 2, copied: true }, + { name: "wal_autocheckpoint", value: 16, copied: true }, + { name: "busy_timeout", value: 50, copied: true }, + { name: "ignore_check_constraints", value: 1, copied: false }, + ]; + + let db1 = Services.storage.openUnsharedDatabase(getTestDB()); + + // Sanity check initial values are different from enforced ones. + PRAGMAS.forEach(function (pragma) { + let stmt = db1.createStatement("PRAGMA " + pragma.name); + Assert.ok(stmt.executeStep()); + Assert.notEqual(pragma.value, stmt.getInt32(0)); + stmt.finalize(); + }); + // Execute pragmas. + PRAGMAS.forEach(function (pragma) { + db1.executeSimpleSQL("PRAGMA " + pragma.name + " = " + pragma.value); + }); + + let db2 = db1.clone(); + Assert.ok(db2.connectionReady); + + // Check cloned connection inherited pragma values. + PRAGMAS.forEach(function (pragma) { + let stmt = db2.createStatement("PRAGMA " + pragma.name); + Assert.ok(stmt.executeStep()); + let validate = pragma.copied ? "equal" : "notEqual"; + Assert[validate](pragma.value, stmt.getInt32(0)); + stmt.finalize(); + }); + + db1.close(); + db2.close(); +}); + +add_task(async function test_readonly_clone_copies_pragmas() { + const PRAGMAS = [ + { name: "cache_size", value: 500, copied: true }, + { name: "temp_store", value: 2, copied: true }, + { name: "foreign_keys", value: 1, copied: false }, + { name: "journal_size_limit", value: 524288, copied: false }, + { name: "synchronous", value: 2, copied: false }, + { name: "wal_autocheckpoint", value: 16, copied: false }, + { name: "busy_timeout", value: 50, copied: false }, + { name: "ignore_check_constraints", value: 1, copied: false }, + ]; + + let db1 = Services.storage.openUnsharedDatabase(getTestDB()); + + // Sanity check initial values are different from enforced ones. + PRAGMAS.forEach(function (pragma) { + let stmt = db1.createStatement("PRAGMA " + pragma.name); + Assert.ok(stmt.executeStep()); + Assert.notEqual(pragma.value, stmt.getInt32(0)); + stmt.finalize(); + }); + // Execute pragmas. + PRAGMAS.forEach(function (pragma) { + db1.executeSimpleSQL("PRAGMA " + pragma.name + " = " + pragma.value); + }); + + let db2 = db1.clone(true); + Assert.ok(db2.connectionReady); + + // Check cloned connection inherited pragma values. + PRAGMAS.forEach(function (pragma) { + let stmt = db2.createStatement("PRAGMA " + pragma.name); + Assert.ok(stmt.executeStep()); + let validate = pragma.copied ? "equal" : "notEqual"; + Assert[validate](pragma.value, stmt.getInt32(0)); + stmt.finalize(); + }); + + db1.close(); + db2.close(); +}); + +add_task(async function test_clone_attach_database() { + let db1 = Services.storage.openUnsharedDatabase(getTestDB()); + + let c = 0; + function attachDB(conn, name) { + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("test_storage_" + ++c + ".sqlite"); + let db = Services.storage.openUnsharedDatabase(file); + conn.executeSimpleSQL( + `ATTACH DATABASE '${db.databaseFile.path}' AS ${name}` + ); + db.executeSimpleSQL(`CREATE TABLE test_${name}(name TEXT);`); + db.close(); + } + attachDB(db1, "attached_1"); + attachDB(db1, "attached_2"); + db1.executeSimpleSQL(` + CREATE TEMP TRIGGER test_temp_afterinsert_trigger + AFTER DELETE ON test_attached_1 FOR EACH ROW + BEGIN + INSERT INTO test(name) VALUES(OLD.name); + END`); + + // These should not throw. + let stmt = db1.createStatement("SELECT * FROM attached_1.sqlite_master"); + stmt.finalize(); + stmt = db1.createStatement("SELECT * FROM attached_2.sqlite_master"); + stmt.finalize(); + db1.executeSimpleSQL("INSERT INTO test_attached_1(name) VALUES('asuth')"); + db1.executeSimpleSQL("DELETE FROM test_attached_1"); + Assert.ok(fetchAllNames(db1).includes("asuth")); + + // R/W clone. + let db2 = db1.clone(); + Assert.ok(db2.connectionReady); + + // These should not throw. + stmt = db2.createStatement("SELECT * FROM attached_1.sqlite_master"); + stmt.finalize(); + stmt = db2.createStatement("SELECT * FROM attached_2.sqlite_master"); + stmt.finalize(); + db2.executeSimpleSQL("INSERT INTO test_attached_1(name) VALUES('past')"); + db2.executeSimpleSQL("DELETE FROM test_attached_1"); + let newNames = fetchAllNames(db2); + Assert.ok(newNames.includes("past")); + Assert.deepEqual(fetchAllNames(db1), newNames); + + // R/O clone. + let db3 = db1.clone(true); + Assert.ok(db3.connectionReady); + + // These should not throw. + stmt = db3.createStatement("SELECT * FROM attached_1.sqlite_master"); + stmt.finalize(); + stmt = db3.createStatement("SELECT * FROM attached_2.sqlite_master"); + stmt.finalize(); + + db1.close(); + db2.close(); + db3.close(); +}); + +add_task(async function test_async_clone_with_temp_trigger_and_table() { + info("Open connection"); + let db = Services.storage.openDatabase(getTestDB()); + Assert.ok(db instanceof Ci.mozIStorageAsyncConnection); + + info("Set up tables on original connection"); + let createQueries = [ + `CREATE TEMP TABLE test_temp(name TEXT)`, + `CREATE INDEX test_temp_idx ON test_temp(name)`, + `CREATE TEMP TRIGGER test_temp_afterdelete_trigger + AFTER DELETE ON test_temp FOR EACH ROW + BEGIN + INSERT INTO test(name) VALUES(OLD.name); + END`, + ]; + for (let query of createQueries) { + let stmt = db.createAsyncStatement(query); + await executeAsync(stmt); + stmt.finalize(); + } + + info("Create read-write clone with temp tables"); + let readWriteClone = await asyncClone(db, false); + Assert.ok(readWriteClone instanceof Ci.mozIStorageAsyncConnection); + + info("Insert into temp table on read-write clone"); + let insertStmt = readWriteClone.createAsyncStatement(` + INSERT INTO test_temp(name) VALUES('mak'), ('standard8'), ('markh')`); + await executeAsync(insertStmt); + insertStmt.finalize(); + + info("Fire temp trigger on read-write clone"); + let deleteStmt = readWriteClone.createAsyncStatement(` + DELETE FROM test_temp`); + await executeAsync(deleteStmt); + deleteStmt.finalize(); + + info("Read from original connection"); + let names = fetchAllNames(db); + Assert.ok(names.includes("mak")); + Assert.ok(names.includes("standard8")); + Assert.ok(names.includes("markh")); + + info("Create read-only clone"); + let readOnlyClone = await asyncClone(db, true); + Assert.ok(readOnlyClone instanceof Ci.mozIStorageAsyncConnection); + + info("Read-only clone shouldn't have temp entities"); + let badStmt = readOnlyClone.createAsyncStatement(`SELECT 1 FROM test_temp`); + await Assert.rejects(executeAsync(badStmt), Ci.mozIStorageError); + badStmt.finalize(); + + info("Clean up"); + for (let conn of [db, readWriteClone, readOnlyClone]) { + await asyncClose(conn); + } +}); + +add_task(async function test_sync_clone_in_transaction() { + info("Open connection"); + let db = Services.storage.openDatabase(getTestDB()); + Assert.ok(db instanceof Ci.mozIStorageAsyncConnection); + + info("Begin transaction on main connection"); + db.beginTransaction(); + + info("Create temp table and trigger in transaction"); + let createQueries = [ + `CREATE TEMP TABLE test_temp(name TEXT)`, + `CREATE TEMP TRIGGER test_temp_afterdelete_trigger + AFTER DELETE ON test_temp FOR EACH ROW + BEGIN + INSERT INTO test(name) VALUES(OLD.name); + END`, + ]; + for (let query of createQueries) { + db.executeSimpleSQL(query); + } + + info("Clone main connection while transaction is in progress"); + let clone = db.clone(/* aReadOnly */ false); + + // Dropping the table also drops `test_temp_afterdelete_trigger`. + info("Drop temp table on main connection"); + db.executeSimpleSQL(`DROP TABLE test_temp`); + + info("Commit transaction"); + db.commitTransaction(); + + info("Clone connection should still have temp entities"); + let readTempStmt = clone.createStatement(`SELECT 1 FROM test_temp`); + readTempStmt.execute(); + readTempStmt.finalize(); + + info("Clean up"); + + db.close(); + clone.close(); +}); + +add_task(async function test_sync_clone_with_function() { + info("Open connection"); + let db = Services.storage.openDatabase(getTestDB()); + Assert.ok(db instanceof Ci.mozIStorageAsyncConnection); + + info("Create SQL function"); + function storeLastInsertedNameFunc() { + this.name = null; + } + storeLastInsertedNameFunc.prototype = { + onFunctionCall(args) { + this.name = args.getUTF8String(0); + }, + }; + let func = new storeLastInsertedNameFunc(); + db.createFunction("store_last_inserted_name", 1, func); + + info("Create temp trigger on main connection"); + db.executeSimpleSQL(` + CREATE TEMP TRIGGER test_afterinsert_trigger + AFTER INSERT ON test FOR EACH ROW + BEGIN + SELECT store_last_inserted_name(NEW.name); + END`); + + info("Clone main connection"); + let clone = db.clone(/* aReadOnly */ false); + + info("Write to clone"); + clone.executeSimpleSQL(`INSERT INTO test(name) VALUES('kit')`); + + Assert.equal(func.name, "kit"); + + info("Clean up"); + db.close(); + clone.close(); +}); + +add_task(async function test_defaultTransactionType() { + info("Open connection"); + let db = Services.storage.openDatabase(getTestDB()); + Assert.ok(db instanceof Ci.mozIStorageAsyncConnection); + + info("Verify default transaction type"); + Assert.equal( + db.defaultTransactionType, + Ci.mozIStorageConnection.TRANSACTION_DEFERRED + ); + + info("Test other transaction types"); + for (let type of [ + Ci.mozIStorageConnection.TRANSACTION_IMMEDIATE, + Ci.mozIStorageConnection.TRANSACTION_EXCLUSIVE, + ]) { + db.defaultTransactionType = type; + Assert.equal(db.defaultTransactionType, type); + } + + info("Should reject unknown transaction types"); + Assert.throws( + () => + (db.defaultTransactionType = + Ci.mozIStorageConnection.TRANSACTION_DEFAULT), + /NS_ERROR_ILLEGAL_VALUE/ + ); + + db.defaultTransactionType = Ci.mozIStorageConnection.TRANSACTION_IMMEDIATE; + + info("Clone should inherit default transaction type"); + let clone = await asyncClone(db, true); + Assert.ok(clone instanceof Ci.mozIStorageAsyncConnection); + Assert.equal( + clone.defaultTransactionType, + Ci.mozIStorageConnection.TRANSACTION_IMMEDIATE + ); + + info("Begin immediate transaction on main connection"); + db.beginTransaction(); + + info("Queue immediate transaction on clone"); + let stmts = [ + clone.createAsyncStatement(`BEGIN IMMEDIATE TRANSACTION`), + clone.createAsyncStatement(`DELETE FROM test WHERE name = 'new'`), + clone.createAsyncStatement(`COMMIT`), + ]; + let promiseStmtsRan = stmts.map(stmt => executeAsync(stmt)); + + info("Commit immediate transaction on main connection"); + db.executeSimpleSQL(`INSERT INTO test(name) VALUES('new')`); + db.commitTransaction(); + + info("Wait for transaction to succeed on clone"); + await Promise.all(promiseStmtsRan); + + info("Clean up"); + for (let stmt of stmts) { + stmt.finalize(); + } + await asyncClose(clone); + await asyncClose(db); +}); + +add_task(async function test_variableLimit() { + info("Open connection"); + let db = Services.storage.openDatabase(getTestDB()); + Assert.equal(db.variableLimit, 32766, "Should return default limit"); + await asyncClose(db); +}); + +add_task(async function test_getInterface() { + let db = getOpenedDatabase(); + let target = db + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEventTarget); + // Just check that target is non-null. Other tests will ensure that it has + // the correct value. + Assert.ok(target != null); + + await asyncClose(db); + gDBConn = null; +}); diff --git a/storage/test/unit/test_storage_fulltextindex.js b/storage/test/unit/test_storage_fulltextindex.js new file mode 100644 index 0000000000..bcc9491a93 --- /dev/null +++ b/storage/test/unit/test_storage_fulltextindex.js @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests support for the fts3 (full-text index) module. + +// Example statements in these tests are taken from the Full Text Index page +// on the SQLite wiki: http://www.sqlite.org/cvstrac/wiki?p=FullTextIndex + +function test_table_creation() { + var msc = getOpenedUnsharedDatabase(); + + msc.executeSimpleSQL( + "CREATE VIRTUAL TABLE recipe USING fts3(name, ingredients)" + ); + + Assert.ok(msc.tableExists("recipe")); +} + +function test_insertion() { + var msc = getOpenedUnsharedDatabase(); + + msc.executeSimpleSQL( + "INSERT INTO recipe (name, ingredients) VALUES " + + "('broccoli stew', 'broccoli peppers cheese tomatoes')" + ); + msc.executeSimpleSQL( + "INSERT INTO recipe (name, ingredients) VALUES " + + "('pumpkin stew', 'pumpkin onions garlic celery')" + ); + msc.executeSimpleSQL( + "INSERT INTO recipe (name, ingredients) VALUES " + + "('broccoli pie', 'broccoli cheese onions flour')" + ); + msc.executeSimpleSQL( + "INSERT INTO recipe (name, ingredients) VALUES " + + "('pumpkin pie', 'pumpkin sugar flour butter')" + ); + + var stmt = msc.createStatement("SELECT COUNT(*) FROM recipe"); + stmt.executeStep(); + + Assert.equal(stmt.getInt32(0), 4); + + stmt.reset(); + stmt.finalize(); +} + +function test_selection() { + var msc = getOpenedUnsharedDatabase(); + + var stmt = msc.createStatement( + "SELECT rowid, name, ingredients FROM recipe WHERE name MATCH 'pie'" + ); + + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.getInt32(0), 3); + Assert.equal(stmt.getString(1), "broccoli pie"); + Assert.equal(stmt.getString(2), "broccoli cheese onions flour"); + + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.getInt32(0), 4); + Assert.equal(stmt.getString(1), "pumpkin pie"); + Assert.equal(stmt.getString(2), "pumpkin sugar flour butter"); + + Assert.ok(!stmt.executeStep()); + + stmt.reset(); + stmt.finalize(); +} + +var tests = [test_table_creation, test_insertion, test_selection]; + +function run_test() { + // It's extra important to start from scratch, since these tests won't work + // with an existing shared cache connection, so we do it even though the last + // test probably did it already. + cleanup(); + + try { + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + } finally { + // It's extra important to clean up afterwards, since later tests that use + // a shared cache connection will not be able to read the database we create, + // so we do this in a finally block to ensure it happens even if some of our + // tests fail. + cleanup(); + } +} diff --git a/storage/test/unit/test_storage_function.js b/storage/test/unit/test_storage_function.js new file mode 100644 index 0000000000..1b11aa4a52 --- /dev/null +++ b/storage/test/unit/test_storage_function.js @@ -0,0 +1,92 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the custom functions + +var testNums = [1, 2, 3, 4]; + +function setup() { + getOpenedDatabase().createTable("function_tests", "id INTEGER PRIMARY KEY"); + + var stmt = createStatement("INSERT INTO function_tests (id) VALUES(?1)"); + for (let i = 0; i < testNums.length; ++i) { + stmt.bindByIndex(0, testNums[i]); + stmt.execute(); + } + stmt.reset(); + stmt.finalize(); +} + +var testSquareFunction = { + calls: 0, + + onFunctionCall(val) { + ++this.calls; + return val.getInt32(0) * val.getInt32(0); + }, +}; + +function test_function_registration() { + var msc = getOpenedDatabase(); + msc.createFunction("test_square", 1, testSquareFunction); +} + +function test_function_no_double_registration() { + var msc = getOpenedDatabase(); + try { + msc.createFunction("test_square", 2, testSquareFunction); + do_throw("We shouldn't get here!"); + } catch (e) { + Assert.equal(Cr.NS_ERROR_FAILURE, e.result); + } +} + +function test_function_removal() { + var msc = getOpenedDatabase(); + msc.removeFunction("test_square"); + // Should be Ok now + msc.createFunction("test_square", 1, testSquareFunction); +} + +function test_function_aliases() { + var msc = getOpenedDatabase(); + msc.createFunction("test_square2", 1, testSquareFunction); +} + +function test_function_call() { + var stmt = createStatement("SELECT test_square(id) FROM function_tests"); + while (stmt.executeStep()) { + // Do nothing. + } + Assert.equal(testNums.length, testSquareFunction.calls); + testSquareFunction.calls = 0; + stmt.finalize(); +} + +function test_function_result() { + var stmt = createStatement("SELECT test_square(42) FROM function_tests"); + stmt.executeStep(); + Assert.equal(42 * 42, stmt.getInt32(0)); + testSquareFunction.calls = 0; + stmt.finalize(); +} + +var tests = [ + test_function_registration, + test_function_no_double_registration, + test_function_removal, + test_function_aliases, + test_function_call, + test_function_result, +]; + +function run_test() { + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_storage_progresshandler.js b/storage/test/unit/test_storage_progresshandler.js new file mode 100644 index 0000000000..e2e01eb2ff --- /dev/null +++ b/storage/test/unit/test_storage_progresshandler.js @@ -0,0 +1,112 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the custom progress handlers + +function setup() { + var msc = getOpenedDatabase(); + msc.createTable("handler_tests", "id INTEGER PRIMARY KEY, num INTEGER"); + msc.beginTransaction(); + + var stmt = createStatement( + "INSERT INTO handler_tests (id, num) VALUES(?1, ?2)" + ); + for (let i = 0; i < 100; ++i) { + stmt.bindByIndex(0, i); + stmt.bindByIndex(1, Math.floor(Math.random() * 1000)); + stmt.execute(); + } + stmt.reset(); + msc.commitTransaction(); + stmt.finalize(); +} + +var testProgressHandler = { + calls: 0, + abort: false, + + onProgress(comm) { + ++this.calls; + return this.abort; + }, +}; + +function test_handler_registration() { + var msc = getOpenedDatabase(); + msc.setProgressHandler(10, testProgressHandler); +} + +function test_handler_return() { + var msc = getOpenedDatabase(); + var oldH = msc.setProgressHandler(5, testProgressHandler); + Assert.ok(oldH instanceof Ci.mozIStorageProgressHandler); +} + +function test_handler_removal() { + var msc = getOpenedDatabase(); + msc.removeProgressHandler(); + var oldH = msc.removeProgressHandler(); + Assert.equal(oldH, null); +} + +function test_handler_call() { + var msc = getOpenedDatabase(); + msc.setProgressHandler(50, testProgressHandler); + // Some long-executing request + var stmt = createStatement( + "SELECT SUM(t1.num * t2.num) FROM handler_tests AS t1, handler_tests AS t2" + ); + while (stmt.executeStep()) { + // Do nothing. + } + Assert.ok(testProgressHandler.calls > 0); + stmt.finalize(); +} + +function test_handler_abort() { + var msc = getOpenedDatabase(); + testProgressHandler.abort = true; + msc.setProgressHandler(50, testProgressHandler); + // Some long-executing request + var stmt = createStatement( + "SELECT SUM(t1.num * t2.num) FROM handler_tests AS t1, handler_tests AS t2" + ); + + const SQLITE_INTERRUPT = 9; + try { + while (stmt.executeStep()) { + // Do nothing. + } + do_throw("We shouldn't get here!"); + } catch (e) { + Assert.equal(Cr.NS_ERROR_ABORT, e.result); + Assert.equal(SQLITE_INTERRUPT, msc.lastError); + } + try { + stmt.finalize(); + do_throw("We shouldn't get here!"); + } catch (e) { + // finalize should return the error code since we encountered an error + Assert.equal(Cr.NS_ERROR_ABORT, e.result); + Assert.equal(SQLITE_INTERRUPT, msc.lastError); + } +} + +var tests = [ + test_handler_registration, + test_handler_return, + test_handler_removal, + test_handler_call, + test_handler_abort, +]; + +function run_test() { + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_storage_service.js b/storage/test/unit/test_storage_service.js new file mode 100644 index 0000000000..68fcb15423 --- /dev/null +++ b/storage/test/unit/test_storage_service.js @@ -0,0 +1,311 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the functions of mozIStorageService except for +// openSpecialDatabase, which is tested by test_storage_service_special.js and +// openUnsharedDatabase, which is tested by test_storage_service_unshared.js. + +const BACKUP_FILE_NAME = "test_storage.sqlite.backup"; + +function test_openDatabase_null_file() { + try { + Services.storage.openDatabase(null); + do_throw("We should not get here!"); + } catch (e) { + print(e); + print("e.result is " + e.result); + Assert.equal(Cr.NS_ERROR_INVALID_ARG, e.result); + } +} + +function test_openDatabase_file_DNE() { + // the file should be created after calling + + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(OPEN_HISTOGRAM); + + var db = getTestDB(); + Assert.ok(!db.exists()); + Services.storage.openDatabase(db); + Assert.ok(db.exists()); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + db.leafName, + TELEMETRY_VALUES.success, + 1 + ); +} + +function test_openDatabase_file_exists() { + // it should already exist from our last test + + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(OPEN_HISTOGRAM); + + var db = getTestDB(); + Assert.ok(db.exists()); + Services.storage.openDatabase(db); + Assert.ok(db.exists()); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + db.leafName, + TELEMETRY_VALUES.success, + 1 + ); +} + +function test_corrupt_db_throws_with_openDatabase() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(OPEN_HISTOGRAM); + + let db = getCorruptDB(); + + try { + getDatabase(db); + do_throw("should not be here"); + } catch (e) { + Assert.equal(Cr.NS_ERROR_FILE_CORRUPTED, e.result); + } + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + db.leafName, + TELEMETRY_VALUES.corrupt, + 1 + ); +} + +function test_fake_db_throws_with_openDatabase() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(OPEN_HISTOGRAM); + + let db = getFakeDB(); + + try { + getDatabase(db); + do_throw("should not be here"); + } catch (e) { + Assert.equal(Cr.NS_ERROR_FILE_CORRUPTED, e.result); + } + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + db.leafName, + TELEMETRY_VALUES.corrupt, + 1 + ); +} + +function test_backup_not_new_filename() { + const fname = getTestDB().leafName; + + var backup = Services.storage.backupDatabaseFile(getTestDB(), fname); + Assert.notEqual(fname, backup.leafName); + + backup.remove(false); +} + +function test_backup_new_filename() { + var backup = Services.storage.backupDatabaseFile( + getTestDB(), + BACKUP_FILE_NAME + ); + Assert.equal(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); + Assert.ok(parentDir.exists()); + + var backup = Services.storage.backupDatabaseFile( + getTestDB(), + BACKUP_FILE_NAME, + parentDir + ); + Assert.equal(BACKUP_FILE_NAME, backup.leafName); + Assert.ok(parentDir.equals(backup.parent)); + + parentDir.remove(true); +} + +function test_openDatabase_directory() { + let dir = getTestDB().parent; + dir.append("test_storage_temp"); + if (dir.exists()) { + dir.remove(true); + } + dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + Assert.ok(dir.exists()); + + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(OPEN_HISTOGRAM); + + try { + getDatabase(dir); + do_throw("should not be here"); + } catch (e) { + Assert.equal(Cr.NS_ERROR_FILE_ACCESS_DENIED, e.result); + } + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + dir.leafName, + TELEMETRY_VALUES.access, + 1 + ); + + dir.remove(true); +} + +function test_read_gooddb() { + let file = do_get_file("goodDB.sqlite"); + let db = getDatabase(file); + + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM); + + db.executeSimpleSQL("SELECT * FROM Foo;"); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + file.leafName, + TELEMETRY_VALUES.success, + 1 + ); + + histogram.clear(); + + let stmt = db.createStatement("SELECT id from Foo"); + + while (true) { + if (!stmt.executeStep()) { + break; + } + } + + stmt.finalize(); + + // A single statement should count as a single access. + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + file.leafName, + TELEMETRY_VALUES.success, + 1 + ); + + histogram.clear(); + + Assert.throws( + () => db.executeSimpleSQL("INSERT INTO Foo (rowid) VALUES ('test');"), + /NS_ERROR_FAILURE/, + "Executing sql should fail." + ); + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + file.leafName, + TELEMETRY_VALUES.misuse, + 1 + ); +} + +function test_read_baddb() { + let file = do_get_file("baddataDB.sqlite"); + let db = getDatabase(file); + + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM); + + Assert.throws( + () => db.executeSimpleSQL("SELECT * FROM Foo"), + /NS_ERROR_FILE_CORRUPTED/, + "Executing sql should fail." + ); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + file.leafName, + TELEMETRY_VALUES.corrupt, + 1 + ); + + histogram.clear(); + + let stmt = db.createStatement("SELECT * FROM Foo"); + Assert.throws( + () => stmt.executeStep(), + /NS_ERROR_FILE_CORRUPTED/, + "Executing a statement should fail." + ); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + file.leafName, + TELEMETRY_VALUES.corrupt, + 1 + ); +} + +function test_busy_telemetry() { + // Thunderbird doesn't have one or more of the probes used in this test. + // Ensure the data is collected anyway. + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + let file = do_get_file("goodDB.sqlite"); + let conn1 = Services.storage.openUnsharedDatabase(file); + let conn2 = Services.storage.openUnsharedDatabase(file); + + conn1.beginTransaction(); + conn1.executeSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"); + + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM); + Assert.throws( + () => + conn2.executeSimpleSQL("CREATE TABLE test_busy (id INTEGER PRIMARY KEY)"), + /NS_ERROR_STORAGE_BUSY/, + "Nested transaction on second connection should fail" + ); + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + file.leafName, + TELEMETRY_VALUES.busy, + 1 + ); + + conn1.rollbackTransaction(); +} + +var tests = [ + test_openDatabase_null_file, + test_openDatabase_file_DNE, + test_openDatabase_file_exists, + test_corrupt_db_throws_with_openDatabase, + test_fake_db_throws_with_openDatabase, + test_backup_not_new_filename, + test_backup_new_filename, + test_backup_new_folder, + test_openDatabase_directory, + test_read_gooddb, + test_read_baddb, + test_busy_telemetry, +]; + +function run_test() { + // Thunderbird doesn't have one or more of the probes used in this test. + // Ensure the data is collected anyway. + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_storage_service_special.js b/storage/test/unit/test_storage_service_special.js new file mode 100644 index 0000000000..50cde042af --- /dev/null +++ b/storage/test/unit/test_storage_service_special.js @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the openSpecialDatabase function of mozIStorageService. + +add_task(async function test_invalid_storage_key() { + try { + Services.storage.openSpecialDatabase("abcd"); + do_throw("We should not get here!"); + } catch (e) { + print(e); + print("e.result is " + e.result); + Assert.equal(Cr.NS_ERROR_INVALID_ARG, e.result); + } +}); + +add_task(async function test_memdb_close_clone_fails() { + for (const name of [null, "foo"]) { + const db = Services.storage.openSpecialDatabase("memory", name); + db.close(); + expectError(Cr.NS_ERROR_NOT_INITIALIZED, () => db.clone()); + } +}); + +add_task(async function test_memdb_no_file_on_disk() { + for (const name of [null, "foo"]) { + const db = Services.storage.openSpecialDatabase("memory", name); + db.close(); + for (const dirKey of ["CurWorkD", "ProfD"]) { + const dir = Services.dirsvc.get(dirKey, Ci.nsIFile); + const file = dir.clone(); + file.append(!name ? ":memory:" : "file:foo?mode=memory&cache=shared"); + Assert.ok(!file.exists()); + } + } +}); + +add_task(async function test_memdb_sharing() { + for (const name of [null, "foo"]) { + const db = Services.storage.openSpecialDatabase("memory", name); + db.executeSimpleSQL("CREATE TABLE test(name TEXT)"); + const db2 = Services.storage.openSpecialDatabase("memory", name); + Assert.ok(!!name == db2.tableExists("test")); + db.close(); + db2.close(); + } +}); diff --git a/storage/test/unit/test_storage_service_unshared.js b/storage/test/unit/test_storage_service_unshared.js new file mode 100644 index 0000000000..da1865dfa6 --- /dev/null +++ b/storage/test/unit/test_storage_service_unshared.js @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the openUnsharedDatabase function of mozIStorageService. + +function test_openUnsharedDatabase_null_file() { + try { + Services.storage.openUnsharedDatabase(null); + do_throw("We should not get here!"); + } catch (e) { + print(e); + print("e.result is " + e.result); + Assert.equal(Cr.NS_ERROR_INVALID_ARG, e.result); + } +} + +function test_openUnsharedDatabase_file_DNE() { + // the file should be created after calling + var db = getTestDB(); + Assert.ok(!db.exists()); + Services.storage.openUnsharedDatabase(db); + Assert.ok(db.exists()); +} + +function test_openUnsharedDatabase_file_exists() { + // it should already exist from our last test + var db = getTestDB(); + Assert.ok(db.exists()); + Services.storage.openUnsharedDatabase(db); + Assert.ok(db.exists()); +} + +var tests = [ + test_openUnsharedDatabase_null_file, + test_openUnsharedDatabase_file_DNE, + test_openUnsharedDatabase_file_exists, +]; + +function run_test() { + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_storage_statement.js b/storage/test/unit/test_storage_statement.js new file mode 100644 index 0000000000..3daf3e7812 --- /dev/null +++ b/storage/test/unit/test_storage_statement.js @@ -0,0 +1,183 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the functions of mozIStorageStatement + +function setup() { + getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, name TEXT"); +} + +function test_parameterCount_none() { + var stmt = createStatement("SELECT * FROM test"); + Assert.equal(0, stmt.parameterCount); + stmt.reset(); + stmt.finalize(); +} + +function test_parameterCount_one() { + var stmt = createStatement("SELECT * FROM test WHERE id = ?1"); + Assert.equal(1, stmt.parameterCount); + stmt.reset(); + stmt.finalize(); +} + +function test_getParameterName() { + var stmt = createStatement("SELECT * FROM test WHERE id = :id"); + Assert.equal(":id", stmt.getParameterName(0)); + stmt.reset(); + stmt.finalize(); +} + +function test_getParameterIndex_different() { + var stmt = createStatement( + "SELECT * FROM test WHERE id = :id OR name = :name" + ); + Assert.equal(0, stmt.getParameterIndex("id")); + Assert.equal(1, stmt.getParameterIndex("name")); + stmt.reset(); + stmt.finalize(); +} + +function test_getParameterIndex_same() { + var stmt = createStatement( + "SELECT * FROM test WHERE id = :test OR name = :test" + ); + Assert.equal(0, stmt.getParameterIndex("test")); + stmt.reset(); + stmt.finalize(); +} + +function test_columnCount() { + var stmt = createStatement("SELECT * FROM test WHERE id = ?1 OR name = ?2"); + Assert.equal(2, stmt.columnCount); + stmt.reset(); + stmt.finalize(); +} + +function test_getColumnName() { + var stmt = createStatement("SELECT name, id FROM test"); + Assert.equal("id", stmt.getColumnName(1)); + Assert.equal("name", stmt.getColumnName(0)); + stmt.reset(); + stmt.finalize(); +} + +function test_getColumnIndex_same_case() { + var stmt = createStatement("SELECT name, id FROM test"); + Assert.equal(0, stmt.getColumnIndex("name")); + Assert.equal(1, stmt.getColumnIndex("id")); + stmt.reset(); + stmt.finalize(); +} + +function test_getColumnIndex_different_case() { + var stmt = createStatement("SELECT name, id FROM test"); + try { + Assert.equal(0, stmt.getColumnIndex("NaMe")); + do_throw("should not get here"); + } catch (e) { + Assert.equal(Cr.NS_ERROR_INVALID_ARG, e.result); + } + try { + Assert.equal(1, stmt.getColumnIndex("Id")); + do_throw("should not get here"); + } catch (e) { + Assert.equal(Cr.NS_ERROR_INVALID_ARG, e.result); + } + stmt.reset(); + stmt.finalize(); +} + +function test_state_ready() { + var stmt = createStatement("SELECT name, id FROM test"); + Assert.equal(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_READY, stmt.state); + stmt.reset(); + stmt.finalize(); +} + +function test_state_executing() { + var stmt = createStatement("INSERT INTO test (name) VALUES ('foo')"); + stmt.execute(); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("SELECT name, id FROM test"); + stmt.executeStep(); + Assert.equal( + Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_EXECUTING, + stmt.state + ); + stmt.executeStep(); + Assert.equal( + Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_EXECUTING, + stmt.state + ); + stmt.reset(); + Assert.equal(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_READY, stmt.state); + stmt.finalize(); +} + +function test_state_after_finalize() { + var stmt = createStatement("SELECT name, id FROM test"); + stmt.executeStep(); + stmt.finalize(); + Assert.equal( + Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_INVALID, + stmt.state + ); +} + +function test_failed_execute() { + var stmt = createStatement("INSERT INTO test (name) VALUES ('foo')"); + stmt.execute(); + stmt.finalize(); + var id = getOpenedDatabase().lastInsertRowID; + stmt = createStatement("INSERT INTO test(id, name) VALUES(:id, 'bar')"); + stmt.params.id = id; + try { + // Should throw a constraint error + stmt.execute(); + do_throw("Should have seen a constraint error"); + } catch (e) { + Assert.equal(getOpenedDatabase().lastError, Ci.mozIStorageError.CONSTRAINT); + } + Assert.equal(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_READY, stmt.state); + // Should succeed without needing to reset the statement manually + stmt.finalize(); +} + +function test_bind_undefined() { + var stmt = createStatement("INSERT INTO test (name) VALUES ('foo')"); + + expectError(Cr.NS_ERROR_ILLEGAL_VALUE, () => stmt.bindParameters(undefined)); + + stmt.finalize(); +} + +var tests = [ + test_parameterCount_none, + test_parameterCount_one, + test_getParameterName, + test_getParameterIndex_different, + test_getParameterIndex_same, + test_columnCount, + test_getColumnName, + test_getColumnIndex_same_case, + test_getColumnIndex_different_case, + test_state_ready, + test_state_executing, + test_state_after_finalize, + test_failed_execute, + test_bind_undefined, +]; + +function run_test() { + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_storage_value_array.js b/storage/test/unit/test_storage_value_array.js new file mode 100644 index 0000000000..6a8f1fb6a2 --- /dev/null +++ b/storage/test/unit/test_storage_value_array.js @@ -0,0 +1,194 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the functions of mozIStorageValueArray + +add_task(async function setup() { + getOpenedDatabase().createTable( + "test", + "id INTEGER PRIMARY KEY, name TEXT," + + "number REAL, nuller NULL, blobber BLOB" + ); + + var stmt = createStatement( + "INSERT INTO test (name, number, blobber) VALUES (?1, ?2, ?3)" + ); + stmt.bindByIndex(0, "foo"); + stmt.bindByIndex(1, 2.34); + stmt.bindBlobByIndex(2, [], 0); + stmt.execute(); + + stmt.bindByIndex(0, ""); + stmt.bindByIndex(1, 1.23); + stmt.bindBlobByIndex(2, [1, 2], 2); + stmt.execute(); + + stmt.reset(); + stmt.finalize(); + + registerCleanupFunction(cleanup); +}); + +add_task(async function test_getIsNull_for_null() { + var stmt = createStatement("SELECT nuller, blobber FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + Assert.ok(stmt.executeStep()); + + Assert.ok(stmt.getIsNull(0)); // null field + Assert.ok(stmt.getIsNull(1)); // data is null if size is 0 + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_getIsNull_for_non_null() { + var stmt = createStatement("SELECT name, blobber FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + Assert.ok(stmt.executeStep()); + + Assert.ok(!stmt.getIsNull(0)); + Assert.ok(!stmt.getIsNull(1)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_value_type_null() { + var stmt = createStatement("SELECT nuller FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + Assert.ok(stmt.executeStep()); + + Assert.equal( + Ci.mozIStorageValueArray.VALUE_TYPE_NULL, + stmt.getTypeOfIndex(0) + ); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_value_type_integer() { + var stmt = createStatement("SELECT id FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + Assert.ok(stmt.executeStep()); + + Assert.equal( + Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER, + stmt.getTypeOfIndex(0) + ); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_value_type_float() { + var stmt = createStatement("SELECT number FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + Assert.ok(stmt.executeStep()); + + Assert.equal( + Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT, + stmt.getTypeOfIndex(0) + ); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_value_type_text() { + var stmt = createStatement("SELECT name FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + Assert.ok(stmt.executeStep()); + + Assert.equal( + Ci.mozIStorageValueArray.VALUE_TYPE_TEXT, + stmt.getTypeOfIndex(0) + ); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_value_type_blob() { + var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + Assert.ok(stmt.executeStep()); + + Assert.equal( + Ci.mozIStorageValueArray.VALUE_TYPE_BLOB, + stmt.getTypeOfIndex(0) + ); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_numEntries_one() { + var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + Assert.ok(stmt.executeStep()); + + Assert.equal(1, stmt.numEntries); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_numEntries_all() { + var stmt = createStatement("SELECT * FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + Assert.ok(stmt.executeStep()); + + Assert.equal(5, stmt.numEntries); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_getInt() { + var stmt = createStatement("SELECT id FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + Assert.ok(stmt.executeStep()); + + Assert.equal(2, stmt.getInt32(0)); + Assert.equal(2, stmt.getInt64(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_getDouble() { + var stmt = createStatement("SELECT number FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + Assert.ok(stmt.executeStep()); + + Assert.equal(1.23, stmt.getDouble(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_getUTF8String() { + var stmt = createStatement("SELECT name FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + Assert.ok(stmt.executeStep()); + + Assert.equal("foo", stmt.getUTF8String(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_getString() { + var stmt = createStatement("SELECT name FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + Assert.ok(stmt.executeStep()); + + Assert.equal("", stmt.getString(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_getBlob() { + var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + Assert.ok(stmt.executeStep()); + + var count = { value: 0 }; + var arr = { value: null }; + stmt.getBlob(0, count, arr); + Assert.equal(2, count.value); + Assert.equal(1, arr.value[0]); + Assert.equal(2, arr.value[1]); + stmt.reset(); + stmt.finalize(); +}); diff --git a/storage/test/unit/test_unicode.js b/storage/test/unit/test_unicode.js new file mode 100644 index 0000000000..0785a54a42 --- /dev/null +++ b/storage/test/unit/test_unicode.js @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the unicode functions that we have added + +const LATIN1_AE = "\xc6"; // "Æ" +const LATIN1_ae = "\xe6"; // "æ" + +add_task(async function setup() { + getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, name TEXT"); + + var stmt = createStatement("INSERT INTO test (name, id) VALUES (?1, ?2)"); + stmt.bindByIndex(0, LATIN1_AE); + stmt.bindByIndex(1, 1); + stmt.execute(); + stmt.bindByIndex(0, "A"); + stmt.bindByIndex(1, 2); + stmt.execute(); + stmt.bindByIndex(0, "b"); + stmt.bindByIndex(1, 3); + stmt.execute(); + stmt.bindByIndex(0, LATIN1_ae); + stmt.bindByIndex(1, 4); + stmt.execute(); + stmt.finalize(); + + registerCleanupFunction(cleanup); +}); + +add_task(async function test_upper_ascii() { + var stmt = createStatement( + "SELECT name, id FROM test WHERE name = upper('a')" + ); + Assert.ok(stmt.executeStep()); + Assert.equal("A", stmt.getString(0)); + Assert.equal(2, stmt.getInt32(1)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_upper_non_ascii() { + var stmt = createStatement( + "SELECT name, id FROM test WHERE name = upper(?1)" + ); + stmt.bindByIndex(0, LATIN1_ae); + Assert.ok(stmt.executeStep()); + Assert.equal(LATIN1_AE, stmt.getString(0)); + Assert.equal(1, stmt.getInt32(1)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_lower_ascii() { + var stmt = createStatement( + "SELECT name, id FROM test WHERE name = lower('B')" + ); + Assert.ok(stmt.executeStep()); + Assert.equal("b", stmt.getString(0)); + Assert.equal(3, stmt.getInt32(1)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_lower_non_ascii() { + var stmt = createStatement( + "SELECT name, id FROM test WHERE name = lower(?1)" + ); + stmt.bindByIndex(0, LATIN1_AE); + Assert.ok(stmt.executeStep()); + Assert.equal(LATIN1_ae, stmt.getString(0)); + Assert.equal(4, stmt.getInt32(1)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_like_search_different() { + var stmt = createStatement("SELECT COUNT(*) FROM test WHERE name LIKE ?1"); + stmt.bindByIndex(0, LATIN1_AE); + Assert.ok(stmt.executeStep()); + Assert.equal(2, stmt.getInt32(0)); + stmt.finalize(); +}); + +add_task(async function test_like_search_same() { + var stmt = createStatement("SELECT COUNT(*) FROM test WHERE name LIKE ?1"); + stmt.bindByIndex(0, LATIN1_ae); + Assert.ok(stmt.executeStep()); + Assert.equal(2, stmt.getInt32(0)); + stmt.finalize(); +}); diff --git a/storage/test/unit/test_vacuum.js b/storage/test/unit/test_vacuum.js new file mode 100644 index 0000000000..a4cfdf714f --- /dev/null +++ b/storage/test/unit/test_vacuum.js @@ -0,0 +1,372 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This file tests the Vacuum Manager and asyncVacuum(). + +const { VacuumParticipant } = ChromeUtils.importESModule( + "resource://testing-common/VacuumParticipant.sys.mjs" +); + +/** + * Sends a fake idle-daily notification to the VACUUM Manager. + */ +function synthesize_idle_daily() { + Cc["@mozilla.org/storage/vacuum;1"] + .getService(Ci.nsIObserver) + .observe(null, "idle-daily", null); +} + +/** + * Returns a new nsIFile reference for a profile database. + * @param filename for the database, excluded the .sqlite extension. + */ +function new_db_file(name = "testVacuum") { + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append(name + ".sqlite"); + return file; +} + +function reset_vacuum_date(name = "testVacuum") { + let date = parseInt(Date.now() / 1000 - 31 * 86400); + // Set last VACUUM to a date in the past. + Services.prefs.setIntPref(`storage.vacuum.last.${name}.sqlite`, date); + return date; +} + +function get_vacuum_date(name = "testVacuum") { + return Services.prefs.getIntPref(`storage.vacuum.last.${name}.sqlite`, 0); +} + +add_setup(async function () { + // turn on Cu.isInAutomation + Services.prefs.setBoolPref( + "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer", + true + ); +}); + +add_task(async function test_common_vacuum() { + let last_vacuum_date = reset_vacuum_date(); + info("Test that a VACUUM correctly happens and all notifications are fired."); + let promiseTestVacuumBegin = TestUtils.topicObserved("test-begin-vacuum"); + let promiseTestVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success"); + let promiseVacuumBegin = TestUtils.topicObserved("vacuum-begin"); + let promiseVacuumEnd = TestUtils.topicObserved("vacuum-end"); + + let participant = new VacuumParticipant( + Services.storage.openDatabase(new_db_file()) + ); + await participant.promiseRegistered(); + synthesize_idle_daily(); + // Wait for notifications. + await Promise.all([ + promiseTestVacuumBegin, + promiseTestVacuumEnd, + promiseVacuumBegin, + promiseVacuumEnd, + ]); + Assert.greater(get_vacuum_date(), last_vacuum_date); + await participant.dispose(); +}); + +add_task(async function test_skipped_if_recent_vacuum() { + info("Test that a VACUUM is skipped if it was run recently."); + Services.prefs.setIntPref( + "storage.vacuum.last.testVacuum.sqlite", + parseInt(Date.now() / 1000) + ); + // Wait for VACUUM skipped notification. + let promiseSkipped = TestUtils.topicObserved("vacuum-skip"); + + let participant = new VacuumParticipant( + Services.storage.openDatabase(new_db_file()) + ); + await participant.promiseRegistered(); + synthesize_idle_daily(); + + // Check that VACUUM has been skipped. + await promiseSkipped; + + await participant.dispose(); +}); + +add_task(async function test_page_size_change() { + info("Test that a VACUUM changes page_size"); + reset_vacuum_date(); + + let conn = Services.storage.openDatabase(new_db_file()); + info("Check initial page size."); + let stmt = conn.createStatement("PRAGMA page_size"); + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.row.page_size, conn.defaultPageSize); + stmt.finalize(); + await populateFreeList(conn); + + let participant = new VacuumParticipant(conn, { expectedPageSize: 1024 }); + await participant.promiseRegistered(); + let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success"); + synthesize_idle_daily(); + await promiseVacuumEnd; + + info("Check that page size was updated."); + stmt = conn.createStatement("PRAGMA page_size"); + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.row.page_size, 1024); + stmt.finalize(); + + await participant.dispose(); +}); + +add_task(async function test_skipped_optout_vacuum() { + info("Test that a VACUUM is skipped if the participant wants to opt-out."); + reset_vacuum_date(); + + let participant = new VacuumParticipant( + Services.storage.openDatabase(new_db_file()), + { grant: false } + ); + await participant.promiseRegistered(); + // Wait for VACUUM skipped notification. + let promiseSkipped = TestUtils.topicObserved("vacuum-skip"); + + synthesize_idle_daily(); + + // Check that VACUUM has been skipped. + await promiseSkipped; + + await participant.dispose(); +}); + +add_task(async function test_memory_database_crash() { + info("Test that we don't crash trying to vacuum a memory database"); + reset_vacuum_date(); + + let participant = new VacuumParticipant( + Services.storage.openSpecialDatabase("memory") + ); + await participant.promiseRegistered(); + // Wait for VACUUM skipped notification. + let promiseSkipped = TestUtils.topicObserved("vacuum-skip"); + + synthesize_idle_daily(); + + // Check that VACUUM has been skipped. + await promiseSkipped; + + await participant.dispose(); +}); + +add_task(async function test_async_connection() { + info("Test we can vacuum an async connection"); + reset_vacuum_date(); + + let conn = await openAsyncDatabase(new_db_file()); + await populateFreeList(conn); + let participant = new VacuumParticipant(conn); + await participant.promiseRegistered(); + + let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success"); + synthesize_idle_daily(); + await promiseVacuumEnd; + + await participant.dispose(); +}); + +add_task(async function test_change_to_incremental_vacuum() { + info("Test we can change to incremental vacuum"); + reset_vacuum_date(); + + let conn = Services.storage.openDatabase(new_db_file()); + info("Check initial vacuum."); + let stmt = conn.createStatement("PRAGMA auto_vacuum"); + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.row.auto_vacuum, 0); + stmt.finalize(); + await populateFreeList(conn); + + let participant = new VacuumParticipant(conn, { useIncrementalVacuum: true }); + await participant.promiseRegistered(); + let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success"); + synthesize_idle_daily(); + await promiseVacuumEnd; + + info("Check that auto_vacuum was updated."); + stmt = conn.createStatement("PRAGMA auto_vacuum"); + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.row.auto_vacuum, 2); + stmt.finalize(); + + await participant.dispose(); +}); + +add_task(async function test_change_from_incremental_vacuum() { + info("Test we can change from incremental vacuum"); + reset_vacuum_date(); + + let conn = Services.storage.openDatabase(new_db_file()); + conn.executeSimpleSQL("PRAGMA auto_vacuum = 2"); + info("Check initial vacuum."); + let stmt = conn.createStatement("PRAGMA auto_vacuum"); + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.row.auto_vacuum, 2); + stmt.finalize(); + await populateFreeList(conn); + + let participant = new VacuumParticipant(conn, { + useIncrementalVacuum: false, + }); + await participant.promiseRegistered(); + let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success"); + synthesize_idle_daily(); + await promiseVacuumEnd; + + info("Check that auto_vacuum was updated."); + stmt = conn.createStatement("PRAGMA auto_vacuum"); + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.row.auto_vacuum, 0); + stmt.finalize(); + + await participant.dispose(); +}); + +add_task(async function test_attached_vacuum() { + info("Test attached database is not a problem"); + reset_vacuum_date(); + + let conn = Services.storage.openDatabase(new_db_file()); + let conn2 = Services.storage.openDatabase(new_db_file("attached")); + + info("Attach " + conn2.databaseFile.path); + conn.executeSimpleSQL( + `ATTACH DATABASE '${conn2.databaseFile.path}' AS attached` + ); + await asyncClose(conn2); + let stmt = conn.createStatement("PRAGMA database_list"); + let schemas = []; + while (stmt.executeStep()) { + schemas.push(stmt.row.name); + } + Assert.deepEqual(schemas, ["main", "attached"]); + stmt.finalize(); + + await populateFreeList(conn); + await populateFreeList(conn, "attached"); + + let participant = new VacuumParticipant(conn); + await participant.promiseRegistered(); + let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success"); + synthesize_idle_daily(); + await promiseVacuumEnd; + + await participant.dispose(); +}); + +add_task(async function test_vacuum_fail() { + info("Test a failed vacuum"); + reset_vacuum_date(); + + let conn = Services.storage.openDatabase(new_db_file()); + // Cannot vacuum in a transaction. + conn.beginTransaction(); + await populateFreeList(conn); + + let participant = new VacuumParticipant(conn); + await participant.promiseRegistered(); + let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-failure"); + synthesize_idle_daily(); + await promiseVacuumEnd; + + conn.commitTransaction(); + await participant.dispose(); +}); + +add_task(async function test_async_vacuum() { + // Since previous tests already go through most cases, this only checks + // the basics of directly calling asyncVacuum(). + info("Test synchronous connection"); + let conn = Services.storage.openDatabase(new_db_file()); + await populateFreeList(conn); + let rv = await new Promise(resolve => { + conn.asyncVacuum(status => { + resolve(status); + }); + }); + Assert.ok(Components.isSuccessCode(rv)); + await asyncClose(conn); + + info("Test asynchronous connection"); + conn = await openAsyncDatabase(new_db_file()); + await populateFreeList(conn); + rv = await new Promise(resolve => { + conn.asyncVacuum(status => { + resolve(status); + }); + }); + Assert.ok(Components.isSuccessCode(rv)); + await asyncClose(conn); +}); + +// Chunked growth is disabled on Android, so this test is pointless there. +add_task( + { skip_if: () => AppConstants.platform == "android" }, + async function test_vacuum_growth() { + // Tests vacuum doesn't nullify chunked growth. + let conn = Services.storage.openDatabase(new_db_file("incremental")); + conn.executeSimpleSQL("PRAGMA auto_vacuum = INCREMENTAL"); + conn.setGrowthIncrement(2 * conn.defaultPageSize, ""); + await populateFreeList(conn); + let stmt = conn.createStatement("PRAGMA freelist_count"); + let count = 0; + Assert.ok(stmt.executeStep()); + count = stmt.row.freelist_count; + stmt.reset(); + Assert.greater(count, 2, "There's more than 2 page in freelist"); + + let rv = await new Promise(resolve => { + conn.asyncVacuum(status => { + resolve(status); + }, true); + }); + Assert.ok(Components.isSuccessCode(rv)); + + Assert.ok(stmt.executeStep()); + Assert.equal( + stmt.row.freelist_count, + 2, + "chunked growth space was preserved" + ); + stmt.reset(); + + // A full vacuuum should not be executed if there's less free pages than + // chunked growth. + rv = await new Promise(resolve => { + conn.asyncVacuum(status => { + resolve(status); + }); + }); + Assert.ok(Components.isSuccessCode(rv)); + + Assert.ok(stmt.executeStep()); + Assert.equal( + stmt.row.freelist_count, + 2, + "chunked growth space was preserved" + ); + stmt.finalize(); + + await asyncClose(conn); + } +); + +async function populateFreeList(conn, schema = "main") { + await executeSimpleSQLAsync(conn, `CREATE TABLE ${schema}.test (id TEXT)`); + await executeSimpleSQLAsync( + conn, + `INSERT INTO ${schema}.test + VALUES ${Array.from({ length: 3000 }, () => Math.random()).map( + v => "('" + v + "')" + )}` + ); + await executeSimpleSQLAsync(conn, `DROP TABLE ${schema}.test`); +} diff --git a/storage/test/unit/xpcshell.ini b/storage/test/unit/xpcshell.ini new file mode 100644 index 0000000000..f4fef8e23c --- /dev/null +++ b/storage/test/unit/xpcshell.ini @@ -0,0 +1,54 @@ +[DEFAULT] +head = head_storage.js +support-files = + baddataDB.sqlite + corruptDB.sqlite + fakeDB.sqlite + goodDB.sqlite + locale_collation.txt + VacuumParticipant.sys.mjs + +[test_bug-365166.js] +[test_bug-393952.js] +[test_bug-429521.js] +[test_bug-444233.js] +[test_cache_size.js] +[test_chunk_growth.js] +# Bug 676981: test fails consistently on Android +fail-if = os == "android" +[test_connection_asyncClose.js] +[test_connection_executeAsync.js] +[test_connection_executeSimpleSQLAsync.js] +[test_connection_failsafe_close.js] +# The failsafe close mechanism asserts when performing SpinningSynchronousClose +# on debug builds, so we can only test on non-debug builds. +skip-if = debug +[test_connection_interrupt.js] +[test_default_journal_size_limit.js] +[test_js_helpers.js] +[test_levenshtein.js] +[test_like.js] +[test_like_escape.js] +[test_locale_collation.js] +[test_minimizeMemory.js] +[test_page_size_is_32k.js] +[test_persist_journal.js] +skip-if = release_or_beta +[test_readonly-immutable-nolock_vfs.js] +[test_retry_on_busy.js] +[test_sqlite_secure_delete.js] +[test_statement_executeAsync.js] +[test_statement_wrapper_automatically.js] +[test_storage_connection.js] +# Bug 676981: test fails consistently on Android +fail-if = os == "android" +[test_storage_fulltextindex.js] +[test_storage_function.js] +[test_storage_progresshandler.js] +[test_storage_service.js] +[test_storage_service_special.js] +[test_storage_service_unshared.js] +[test_storage_statement.js] +[test_storage_value_array.js] +[test_unicode.js] +[test_vacuum.js] |