diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /storage/test | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'storage/test')
53 files changed, 8750 insertions, 0 deletions
diff --git a/storage/test/moz.build b/storage/test/moz.build new file mode 100644 index 000000000..cf8b49c50 --- /dev/null +++ b/storage/test/moz.build @@ -0,0 +1,37 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini'] + +GeckoCppUnitTests([ + 'test_AsXXX_helpers', + 'test_async_callbacks_with_spun_event_loops', + 'test_asyncStatementExecution_transaction', + 'test_binding_params', + 'test_file_perms', + 'test_mutex', + 'test_service_init_background_thread', + 'test_statement_scoper', + 'test_StatementCache', + 'test_transaction_helper', + 'test_true_async', + 'test_unlock_notify', +]) + +if CONFIG['MOZ_DEBUG'] and CONFIG['OS_ARCH'] not in ('WINNT', 'Darwin'): + # FIXME bug 523392: test_deadlock_detector doesn't like Windows + # FIXME bug 523378: also fails on OS X + GeckoCppUnitTests([ + 'test_deadlock_detector', + ]) + +LOCAL_INCLUDES += [ + '..', +] + +USE_LIBS += [ + 'sqlite', +] diff --git a/storage/test/storage_test_harness.h b/storage/test/storage_test_harness.h new file mode 100644 index 000000000..043652b1c --- /dev/null +++ b/storage/test/storage_test_harness.h @@ -0,0 +1,389 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "TestHarness.h" + +#include "nsMemory.h" +#include "prthread.h" +#include "nsThreadUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "mozilla/ReentrantMonitor.h" + +#include "mozIStorageService.h" +#include "mozIStorageConnection.h" +#include "mozIStorageStatementCallback.h" +#include "mozIStorageCompletionCallback.h" +#include "mozIStorageBindingParamsArray.h" +#include "mozIStorageBindingParams.h" +#include "mozIStorageAsyncStatement.h" +#include "mozIStorageStatement.h" +#include "mozIStoragePendingStatement.h" +#include "mozIStorageError.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIEventTarget.h" + +#include "sqlite3.h" + +static int gTotalTests = 0; +static int gPassedTests = 0; + +#define do_check_true(aCondition) \ + PR_BEGIN_MACRO \ + gTotalTests++; \ + if (aCondition) { \ + gPassedTests++; \ + } else { \ + fail("%s | Expected true, got false at line %d", __FILE__, __LINE__); \ + } \ + PR_END_MACRO + +#define do_check_false(aCondition) \ + PR_BEGIN_MACRO \ + gTotalTests++; \ + if (!aCondition) { \ + gPassedTests++; \ + } else { \ + fail("%s | Expected false, got true at line %d", __FILE__, __LINE__); \ + } \ + PR_END_MACRO + +#define do_check_success(aResult) \ + do_check_true(NS_SUCCEEDED(aResult)) + +#ifdef LINUX +// XXX Linux opt builds on tinderbox are orange due to linking with stdlib. +// This is sad and annoying, but it's a workaround that works. +#define do_check_eq(aExpected, aActual) \ + do_check_true(aExpected == aActual) +#else +#include <sstream> +// Print nsresult as uint32_t +std::ostream& operator<<(std::ostream& aStream, const nsresult aInput) +{ + return aStream << static_cast<uint32_t>(aInput); +} +#define do_check_eq(aExpected, aActual) \ + PR_BEGIN_MACRO \ + gTotalTests++; \ + if (aExpected == aActual) { \ + gPassedTests++; \ + } else { \ + std::ostringstream temp; \ + temp << __FILE__ << " | Expected '" << aExpected << "', got '"; \ + temp << aActual <<"' at line " << __LINE__; \ + fail(temp.str().c_str()); \ + } \ + PR_END_MACRO +#endif + +#define do_check_ok(aInvoc) do_check_true((aInvoc) == SQLITE_OK) + +already_AddRefed<mozIStorageService> +getService() +{ + nsCOMPtr<mozIStorageService> ss = + do_GetService("@mozilla.org/storage/service;1"); + do_check_true(ss); + return ss.forget(); +} + +already_AddRefed<mozIStorageConnection> +getMemoryDatabase() +{ + nsCOMPtr<mozIStorageService> ss = getService(); + nsCOMPtr<mozIStorageConnection> conn; + nsresult rv = ss->OpenSpecialDatabase("memory", getter_AddRefs(conn)); + do_check_success(rv); + return conn.forget(); +} + +already_AddRefed<mozIStorageConnection> +getDatabase() +{ + nsCOMPtr<nsIFile> dbFile; + (void)NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(dbFile)); + NS_ASSERTION(dbFile, "The directory doesn't exists?!"); + + nsresult rv = dbFile->Append(NS_LITERAL_STRING("storage_test_db.sqlite")); + do_check_success(rv); + + nsCOMPtr<mozIStorageService> ss = getService(); + nsCOMPtr<mozIStorageConnection> conn; + rv = ss->OpenDatabase(dbFile, getter_AddRefs(conn)); + do_check_success(rv); + return conn.forget(); +} + + +class AsyncStatementSpinner : public mozIStorageStatementCallback + , public mozIStorageCompletionCallback +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGESTATEMENTCALLBACK + NS_DECL_MOZISTORAGECOMPLETIONCALLBACK + + AsyncStatementSpinner(); + + void SpinUntilCompleted(); + + uint16_t completionReason; + +protected: + virtual ~AsyncStatementSpinner() {} + volatile bool mCompleted; +}; + +NS_IMPL_ISUPPORTS(AsyncStatementSpinner, + mozIStorageStatementCallback, + mozIStorageCompletionCallback) + +AsyncStatementSpinner::AsyncStatementSpinner() +: completionReason(0) +, mCompleted(false) +{ +} + +NS_IMETHODIMP +AsyncStatementSpinner::HandleResult(mozIStorageResultSet *aResultSet) +{ + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatementSpinner::HandleError(mozIStorageError *aError) +{ + int32_t result; + nsresult rv = aError->GetResult(&result); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString message; + rv = aError->GetMessage(message); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString warnMsg; + warnMsg.AppendLiteral("An error occurred while executing an async statement: "); + warnMsg.AppendInt(result); + warnMsg.Append(' '); + warnMsg.Append(message); + NS_WARNING(warnMsg.get()); + + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatementSpinner::HandleCompletion(uint16_t aReason) +{ + completionReason = aReason; + mCompleted = true; + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatementSpinner::Complete(nsresult, nsISupports*) +{ + mCompleted = true; + return NS_OK; +} + +void AsyncStatementSpinner::SpinUntilCompleted() +{ + nsCOMPtr<nsIThread> thread(::do_GetCurrentThread()); + nsresult rv = NS_OK; + bool processed = true; + while (!mCompleted && NS_SUCCEEDED(rv)) { + rv = thread->ProcessNextEvent(true, &processed); + } +} + +#define NS_DECL_ASYNCSTATEMENTSPINNER \ + NS_IMETHOD HandleResult(mozIStorageResultSet *aResultSet) override; + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * Execute an async statement, blocking the main thread until we get the + * callback completion notification. + */ +void +blocking_async_execute(mozIStorageBaseStatement *stmt) +{ + RefPtr<AsyncStatementSpinner> spinner(new AsyncStatementSpinner()); + + nsCOMPtr<mozIStoragePendingStatement> pendy; + (void)stmt->ExecuteAsync(spinner, getter_AddRefs(pendy)); + spinner->SpinUntilCompleted(); +} + +/** + * Invoke AsyncClose on the given connection, blocking the main thread until we + * get the completion notification. + */ +void +blocking_async_close(mozIStorageConnection *db) +{ + RefPtr<AsyncStatementSpinner> spinner(new AsyncStatementSpinner()); + + db->AsyncClose(spinner); + spinner->SpinUntilCompleted(); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Mutex Watching + +/** + * Verify that mozIStorageAsyncStatement's life-cycle never triggers a mutex on + * the caller (generally main) thread. We do this by decorating the sqlite + * mutex logic with our own code that checks what thread it is being invoked on + * and sets a flag if it is invoked on the main thread. We are able to easily + * decorate the SQLite mutex logic because SQLite allows us to retrieve the + * current function pointers being used and then provide a new set. + */ + +sqlite3_mutex_methods orig_mutex_methods; +sqlite3_mutex_methods wrapped_mutex_methods; + +bool mutex_used_on_watched_thread = false; +PRThread *watched_thread = nullptr; +/** + * Ugly hack to let us figure out what a connection's async thread is. If we + * were MOZILLA_INTERNAL_API and linked as such we could just include + * mozStorageConnection.h and just ask Connection directly. But that turns out + * poorly. + * + * When the thread a mutex is invoked on isn't watched_thread we save it to this + * variable. + */ +PRThread *last_non_watched_thread = nullptr; + +/** + * Set a flag if the mutex is used on the thread we are watching, but always + * call the real mutex function. + */ +extern "C" void wrapped_MutexEnter(sqlite3_mutex *mutex) +{ + PRThread *curThread = ::PR_GetCurrentThread(); + if (curThread == watched_thread) + mutex_used_on_watched_thread = true; + else + last_non_watched_thread = curThread; + orig_mutex_methods.xMutexEnter(mutex); +} + +extern "C" int wrapped_MutexTry(sqlite3_mutex *mutex) +{ + if (::PR_GetCurrentThread() == watched_thread) + mutex_used_on_watched_thread = true; + return orig_mutex_methods.xMutexTry(mutex); +} + +void hook_sqlite_mutex() +{ + // We need to initialize and teardown SQLite to get it to set up the + // default mutex handlers for us so we can steal them and wrap them. + do_check_ok(sqlite3_initialize()); + do_check_ok(sqlite3_shutdown()); + do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &orig_mutex_methods)); + do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &wrapped_mutex_methods)); + wrapped_mutex_methods.xMutexEnter = wrapped_MutexEnter; + wrapped_mutex_methods.xMutexTry = wrapped_MutexTry; + do_check_ok(::sqlite3_config(SQLITE_CONFIG_MUTEX, &wrapped_mutex_methods)); +} + +/** + * Call to clear the watch state and to set the watching against this thread. + * + * Check |mutex_used_on_watched_thread| to see if the mutex has fired since + * this method was last called. Since we're talking about the current thread, + * there are no race issues to be concerned about + */ +void watch_for_mutex_use_on_this_thread() +{ + watched_thread = ::PR_GetCurrentThread(); + mutex_used_on_watched_thread = false; +} + + +//////////////////////////////////////////////////////////////////////////////// +//// Thread Wedgers + +/** + * A runnable that blocks until code on another thread invokes its unwedge + * method. By dispatching this to a thread you can ensure that no subsequent + * runnables dispatched to the thread will execute until you invoke unwedge. + * + * The wedger is self-dispatching, just construct it with its target. + */ +class ThreadWedger : public mozilla::Runnable +{ +public: + explicit ThreadWedger(nsIEventTarget *aTarget) + : mReentrantMonitor("thread wedger") + , unwedged(false) + { + aTarget->Dispatch(this, aTarget->NS_DISPATCH_NORMAL); + } + + NS_IMETHOD Run() override + { + mozilla::ReentrantMonitorAutoEnter automon(mReentrantMonitor); + + if (!unwedged) + automon.Wait(); + + return NS_OK; + } + + void unwedge() + { + mozilla::ReentrantMonitorAutoEnter automon(mReentrantMonitor); + unwedged = true; + automon.Notify(); + } + +private: + mozilla::ReentrantMonitor mReentrantMonitor; + bool unwedged; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * A horrible hack to figure out what the connection's async thread is. By + * creating a statement and async dispatching we can tell from the mutex who + * is the async thread, PRThread style. Then we map that to an nsIThread. + */ +already_AddRefed<nsIThread> +get_conn_async_thread(mozIStorageConnection *db) +{ + // Make sure we are tracking the current thread as the watched thread + watch_for_mutex_use_on_this_thread(); + + // - statement with nothing to bind + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement( + NS_LITERAL_CSTRING("SELECT 1"), + getter_AddRefs(stmt)); + blocking_async_execute(stmt); + stmt->Finalize(); + + nsCOMPtr<nsIThreadManager> threadMan = + do_GetService("@mozilla.org/thread-manager;1"); + nsCOMPtr<nsIThread> asyncThread; + threadMan->GetThreadFromPRThread(last_non_watched_thread, + getter_AddRefs(asyncThread)); + + // Additionally, check that the thread we get as the background thread is the + // same one as the one we report from getInterface. + nsCOMPtr<nsIEventTarget> target = do_GetInterface(db); + nsCOMPtr<nsIThread> allegedAsyncThread = do_QueryInterface(target); + PRThread *allegedPRThread; + (void)allegedAsyncThread->GetPRThread(&allegedPRThread); + do_check_eq(allegedPRThread, last_non_watched_thread); + return asyncThread.forget(); +} diff --git a/storage/test/storage_test_harness_tail.h b/storage/test/storage_test_harness_tail.h new file mode 100644 index 000000000..4ad25c719 --- /dev/null +++ b/storage/test/storage_test_harness_tail.h @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef TEST_NAME +#error "Must #define TEST_NAME before including storage_test_harness_tail.h" +#endif + +#ifndef TEST_FILE +#error "Must #define TEST_FILE before include storage_test_harness_tail.h" +#endif + +int +main(int aArgc, + char **aArgv) +{ + ScopedXPCOM xpcom(TEST_NAME); + + for (size_t i = 0; i < mozilla::ArrayLength(gTests); i++) + gTests[i](); + + if (gPassedTests == gTotalTests) + passed(TEST_FILE); + + (void)printf("%i of %i tests passed\n", gPassedTests, gTotalTests); +} diff --git a/storage/test/test_AsXXX_helpers.cpp b/storage/test/test_AsXXX_helpers.cpp new file mode 100644 index 000000000..4edf7b93a --- /dev/null +++ b/storage/test/test_AsXXX_helpers.cpp @@ -0,0 +1,127 @@ +/* + *Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +#include "storage_test_harness.h" +#include "mozIStorageRow.h" +#include "mozIStorageResultSet.h" + +/** + * This file tests AsXXX (AsInt32, AsInt64, ...) helpers. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Event Loop Spinning + +class Spinner : public AsyncStatementSpinner +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_ASYNCSTATEMENTSPINNER + Spinner() {} +protected: + virtual ~Spinner() {} +}; + +NS_IMPL_ISUPPORTS_INHERITED0(Spinner, + AsyncStatementSpinner) + +NS_IMETHODIMP +Spinner::HandleResult(mozIStorageResultSet *aResultSet) +{ + nsCOMPtr<mozIStorageRow> row; + do_check_true(NS_SUCCEEDED(aResultSet->GetNextRow(getter_AddRefs(row))) && row); + + do_check_eq(row->AsInt32(0), 0); + do_check_eq(row->AsInt64(0), 0); + do_check_eq(row->AsDouble(0), 0.0); + + uint32_t len = 100; + do_check_eq(row->AsSharedUTF8String(0, &len), (const char*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(row->AsSharedWString(0, &len), (const char16_t*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(row->AsSharedBlob(0, &len), (const uint8_t*)nullptr); + do_check_eq(len, 0); + + do_check_eq(row->IsNull(0), true); + return NS_OK; +} + +void +test_NULLFallback() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + nsCOMPtr<mozIStorageStatement> stmt; + (void)db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT NULL" + ), getter_AddRefs(stmt)); + + nsCOMPtr<mozIStorageValueArray> valueArray = do_QueryInterface(stmt); + do_check_true(valueArray); + + bool hasMore; + do_check_true(NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore); + + do_check_eq(stmt->AsInt32(0), 0); + do_check_eq(stmt->AsInt64(0), 0); + do_check_eq(stmt->AsDouble(0), 0.0); + uint32_t len = 100; + do_check_eq(stmt->AsSharedUTF8String(0, &len), (const char*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(stmt->AsSharedWString(0, &len), (const char16_t*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(stmt->AsSharedBlob(0, &len), (const uint8_t*)nullptr); + do_check_eq(len, 0); + do_check_eq(stmt->IsNull(0), true); + + do_check_eq(valueArray->AsInt32(0), 0); + do_check_eq(valueArray->AsInt64(0), 0); + do_check_eq(valueArray->AsDouble(0), 0.0); + len = 100; + do_check_eq(valueArray->AsSharedUTF8String(0, &len), (const char*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(valueArray->AsSharedWString(0, &len), (const char16_t*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(valueArray->AsSharedBlob(0, &len), (const uint8_t*)nullptr); + do_check_eq(len, 0); + do_check_eq(valueArray->IsNull(0), true); +} + +void +test_asyncNULLFallback() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + nsCOMPtr<mozIStorageAsyncStatement> stmt; + (void)db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "SELECT NULL" + ), getter_AddRefs(stmt)); + + nsCOMPtr<mozIStoragePendingStatement> pendingStmt; + do_check_true(NS_SUCCEEDED(stmt->ExecuteAsync(nullptr, getter_AddRefs(pendingStmt)))); + do_check_true(pendingStmt); + stmt->Finalize(); + RefPtr<Spinner> asyncSpin(new Spinner()); + db->AsyncClose(asyncSpin); + asyncSpin->SpinUntilCompleted(); + +} + +void (*gTests[])(void) = { + test_NULLFallback +, test_asyncNULLFallback +}; + +const char *file = __FILE__; +#define TEST_NAME "AsXXX helpers" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_StatementCache.cpp b/storage/test/test_StatementCache.cpp new file mode 100644 index 000000000..731a858de --- /dev/null +++ b/storage/test/test_StatementCache.cpp @@ -0,0 +1,163 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "mozilla/Attributes.h" +#include "mozilla/storage/StatementCache.h" +using namespace mozilla::storage; + +/** + * This file test our statement cache in StatementCache.h. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Helpers + +class SyncCache : public StatementCache<mozIStorageStatement> +{ +public: + explicit SyncCache(nsCOMPtr<mozIStorageConnection>& aConnection) + : StatementCache<mozIStorageStatement>(aConnection) + { + } +}; + +class AsyncCache : public StatementCache<mozIStorageAsyncStatement> +{ +public: + explicit AsyncCache(nsCOMPtr<mozIStorageConnection>& aConnection) + : StatementCache<mozIStorageAsyncStatement>(aConnection) + { + } +}; + +/** + * Wraps nsCString so we can not implement the same functions twice for each + * type. + */ +class StringWrapper : public nsCString +{ +public: + MOZ_IMPLICIT StringWrapper(const char* aOther) + { + this->Assign(aOther); + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Test Functions + +template<typename StringType> +void +test_GetCachedStatement() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + SyncCache cache(db); + + StringType sql = "SELECT * FROM sqlite_master"; + + // Make sure we get a statement back with the right state. + nsCOMPtr<mozIStorageStatement> stmt = cache.GetCachedStatement(sql); + do_check_true(stmt); + int32_t state; + do_check_success(stmt->GetState(&state)); + do_check_eq(mozIStorageBaseStatement::MOZ_STORAGE_STATEMENT_READY, state); + + // Check to make sure we get the same copy the second time we ask. + nsCOMPtr<mozIStorageStatement> stmt2 = cache.GetCachedStatement(sql); + do_check_true(stmt2); + do_check_eq(stmt.get(), stmt2.get()); +} + +template <typename StringType> +void +test_FinalizeStatements() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + SyncCache cache(db); + + StringType sql = "SELECT * FROM sqlite_master"; + + // Get a statement, and then tell the cache to finalize. + nsCOMPtr<mozIStorageStatement> stmt = cache.GetCachedStatement(sql); + do_check_true(stmt); + + cache.FinalizeStatements(); + + // We should be in an invalid state at this point. + int32_t state; + do_check_success(stmt->GetState(&state)); + do_check_eq(mozIStorageBaseStatement::MOZ_STORAGE_STATEMENT_INVALID, state); + + // Should be able to close the database now too. + do_check_success(db->Close()); +} + +template<typename StringType> +void +test_GetCachedAsyncStatement() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + AsyncCache cache(db); + + StringType sql = "SELECT * FROM sqlite_master"; + + // Make sure we get a statement back with the right state. + nsCOMPtr<mozIStorageAsyncStatement> stmt = cache.GetCachedStatement(sql); + do_check_true(stmt); + int32_t state; + do_check_success(stmt->GetState(&state)); + do_check_eq(mozIStorageBaseStatement::MOZ_STORAGE_STATEMENT_READY, state); + + // Check to make sure we get the same copy the second time we ask. + nsCOMPtr<mozIStorageAsyncStatement> stmt2 = cache.GetCachedStatement(sql); + do_check_true(stmt2); + do_check_eq(stmt.get(), stmt2.get()); +} + +template <typename StringType> +void +test_FinalizeAsyncStatements() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + AsyncCache cache(db); + + StringType sql = "SELECT * FROM sqlite_master"; + + // Get a statement, and then tell the cache to finalize. + nsCOMPtr<mozIStorageAsyncStatement> stmt = cache.GetCachedStatement(sql); + do_check_true(stmt); + + cache.FinalizeStatements(); + + // We should be in an invalid state at this point. + int32_t state; + do_check_success(stmt->GetState(&state)); + do_check_eq(mozIStorageBaseStatement::MOZ_STORAGE_STATEMENT_INVALID, state); + + // Should be able to close the database now too. + do_check_success(db->AsyncClose(nullptr)); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Test Harness Stuff + +void (*gTests[])(void) = { + test_GetCachedStatement<const char []>, + test_GetCachedStatement<StringWrapper>, + test_FinalizeStatements<const char []>, + test_FinalizeStatements<StringWrapper>, + test_GetCachedAsyncStatement<const char []>, + test_GetCachedAsyncStatement<StringWrapper>, + test_FinalizeAsyncStatements<const char []>, + test_FinalizeAsyncStatements<StringWrapper>, +}; + +const char *file = __FILE__; +#define TEST_NAME "StatementCache" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_asyncStatementExecution_transaction.cpp b/storage/test/test_asyncStatementExecution_transaction.cpp new file mode 100644 index 000000000..7e94bd80e --- /dev/null +++ b/storage/test/test_asyncStatementExecution_transaction.cpp @@ -0,0 +1,509 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +#include "storage_test_harness.h" + +#include "nsIEventTarget.h" +#include "mozStorageConnection.h" + +#include "sqlite3.h" + +using namespace mozilla; +using namespace mozilla::storage; + +//////////////////////////////////////////////////////////////////////////////// +//// Helpers + +/** + * Commit hook to detect transactions. + * + * @param aArg + * An integer pointer that will be incremented for each commit. + */ +int commit_hook(void *aArg) +{ + int *arg = static_cast<int *>(aArg); + (*arg)++; + return 0; +} + +/** + * Executes the passed-in statements and checks if a transaction is created. + * When done statements are finalized and database connection is closed. + * + * @param aDB + * The database connection. + * @param aStmts + * Vector of statements. + * @param aStmtsLen + * Number of statements. + * @param aTransactionExpected + * Whether a transaction is expected or not. + */ +void +check_transaction(mozIStorageConnection *aDB, + mozIStorageBaseStatement **aStmts, + uint32_t aStmtsLen, + bool aTransactionExpected) +{ + // -- install a transaction commit hook. + int commit = 0; + static_cast<Connection *>(aDB)->setCommitHook(commit_hook, &commit); + + RefPtr<AsyncStatementSpinner> asyncSpin(new AsyncStatementSpinner()); + nsCOMPtr<mozIStoragePendingStatement> asyncPend; + do_check_success(aDB->ExecuteAsync(aStmts, aStmtsLen, asyncSpin, + getter_AddRefs(asyncPend))); + do_check_true(asyncPend); + + // -- complete the execution + asyncSpin->SpinUntilCompleted(); + + // -- uninstall the transaction commit hook. + static_cast<Connection *>(aDB)->setCommitHook(nullptr); + + // -- check transaction + do_check_eq(aTransactionExpected, !!commit); + + // -- check that only one transaction was created. + if (aTransactionExpected) { + do_check_eq(1, commit); + } + + // -- cleanup + for (uint32_t i = 0; i < aStmtsLen; ++i) { + aStmts[i]->Finalize(); + } + blocking_async_close(aDB); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +/** + * Test that executing multiple readonly AsyncStatements doesn't create a + * transaction. + */ +void +test_MultipleAsyncReadStatements() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt1; + db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "SELECT * FROM sqlite_master" + ), getter_AddRefs(stmt1)); + + nsCOMPtr<mozIStorageAsyncStatement> stmt2; + db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "SELECT * FROM sqlite_master" + ), getter_AddRefs(stmt2)); + + mozIStorageBaseStatement *stmts[] = { + stmt1, + stmt2, + }; + + check_transaction(db, stmts, ArrayLength(stmts), false); +} + +/** + * Test that executing multiple readonly Statements doesn't create a + * transaction. + */ +void +test_MultipleReadStatements() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt1; + db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT * FROM sqlite_master" + ), getter_AddRefs(stmt1)); + + nsCOMPtr<mozIStorageStatement> stmt2; + db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT * FROM sqlite_master" + ), getter_AddRefs(stmt2)); + + mozIStorageBaseStatement *stmts[] = { + stmt1, + stmt2, + }; + + check_transaction(db, stmts, ArrayLength(stmts), false); +} + +/** + * Test that executing multiple AsyncStatements causing writes creates a + * transaction. + */ +void +test_MultipleAsyncReadWriteStatements() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt1; + db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "SELECT * FROM sqlite_master" + ), getter_AddRefs(stmt1)); + + nsCOMPtr<mozIStorageAsyncStatement> stmt2; + db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(stmt2)); + + mozIStorageBaseStatement *stmts[] = { + stmt1, + stmt2, + }; + + check_transaction(db, stmts, ArrayLength(stmts), true); +} + +/** + * Test that executing multiple Statements causing writes creates a transaction. + */ +void +test_MultipleReadWriteStatements() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt1; + db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT * FROM sqlite_master" + ), getter_AddRefs(stmt1)); + + nsCOMPtr<mozIStorageStatement> stmt2; + db->CreateStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(stmt2)); + + mozIStorageBaseStatement *stmts[] = { + stmt1, + stmt2, + }; + + check_transaction(db, stmts, ArrayLength(stmts), true); +} + +/** + * Test that executing multiple AsyncStatements causing writes creates a + * single transaction. + */ +void +test_MultipleAsyncWriteStatements() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt1; + db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test1 (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(stmt1)); + + nsCOMPtr<mozIStorageAsyncStatement> stmt2; + db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test2 (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(stmt2)); + + mozIStorageBaseStatement *stmts[] = { + stmt1, + stmt2, + }; + + check_transaction(db, stmts, ArrayLength(stmts), true); +} + +/** + * Test that executing multiple Statements causing writes creates a + * single transaction. + */ +void +test_MultipleWriteStatements() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt1; + db->CreateStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test1 (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(stmt1)); + + nsCOMPtr<mozIStorageStatement> stmt2; + db->CreateStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test2 (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(stmt2)); + + mozIStorageBaseStatement *stmts[] = { + stmt1, + stmt2, + }; + + check_transaction(db, stmts, ArrayLength(stmts), true); +} + +/** + * Test that executing a single read-only AsyncStatement doesn't create a + * transaction. + */ +void +test_SingleAsyncReadStatement() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "SELECT * FROM sqlite_master" + ), getter_AddRefs(stmt)); + + mozIStorageBaseStatement *stmts[] = { + stmt, + }; + + check_transaction(db, stmts, ArrayLength(stmts), false); +} + +/** + * Test that executing a single read-only Statement doesn't create a + * transaction. + */ +void +test_SingleReadStatement() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt; + db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT * FROM sqlite_master" + ), getter_AddRefs(stmt)); + + mozIStorageBaseStatement *stmts[] = { + stmt, + }; + + check_transaction(db, stmts, ArrayLength(stmts), false); +} + +/** + * Test that executing a single AsyncStatement causing writes creates a + * transaction. + */ +void +test_SingleAsyncWriteStatement() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(stmt)); + + mozIStorageBaseStatement *stmts[] = { + stmt, + }; + + check_transaction(db, stmts, ArrayLength(stmts), true); +} + +/** + * Test that executing a single Statement causing writes creates a transaction. + */ +void +test_SingleWriteStatement() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt; + db->CreateStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(stmt)); + + mozIStorageBaseStatement *stmts[] = { + stmt, + }; + + check_transaction(db, stmts, ArrayLength(stmts), true); +} + +/** + * Test that executing a single read-only AsyncStatement with multiple params + * doesn't create a transaction. + */ +void +test_MultipleParamsAsyncReadStatement() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "SELECT :param FROM sqlite_master" + ), getter_AddRefs(stmt)); + + // -- bind multiple BindingParams + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (int32_t i = 0; i < 2; i++) { + nsCOMPtr<mozIStorageBindingParams> params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName(NS_LITERAL_CSTRING("param"), 1); + paramsArray->AddParams(params); + } + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + + mozIStorageBaseStatement *stmts[] = { + stmt, + }; + + check_transaction(db, stmts, ArrayLength(stmts), false); +} + +/** + * Test that executing a single read-only Statement with multiple params + * doesn't create a transaction. + */ +void +test_MultipleParamsReadStatement() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt; + db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT :param FROM sqlite_master" + ), getter_AddRefs(stmt)); + + // -- bind multiple BindingParams + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (int32_t i = 0; i < 2; i++) { + nsCOMPtr<mozIStorageBindingParams> params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName(NS_LITERAL_CSTRING("param"), 1); + paramsArray->AddParams(params); + } + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + + mozIStorageBaseStatement *stmts[] = { + stmt, + }; + + check_transaction(db, stmts, ArrayLength(stmts), false); +} + +/** + * Test that executing a single write AsyncStatement with multiple params + * creates a transaction. + */ +void +test_MultipleParamsAsyncWriteStatement() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create a table for writes + nsCOMPtr<mozIStorageStatement> tableStmt; + db->CreateStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(tableStmt)); + tableStmt->Execute(); + tableStmt->Finalize(); + + // -- create statements and execute them + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "DELETE FROM test WHERE id = :param" + ), getter_AddRefs(stmt)); + + // -- bind multiple BindingParams + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (int32_t i = 0; i < 2; i++) { + nsCOMPtr<mozIStorageBindingParams> params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName(NS_LITERAL_CSTRING("param"), 1); + paramsArray->AddParams(params); + } + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + + mozIStorageBaseStatement *stmts[] = { + stmt, + }; + + check_transaction(db, stmts, ArrayLength(stmts), true); +} + +/** + * Test that executing a single write Statement with multiple params + * creates a transaction. + */ +void +test_MultipleParamsWriteStatement() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- create a table for writes + nsCOMPtr<mozIStorageStatement> tableStmt; + db->CreateStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(tableStmt)); + tableStmt->Execute(); + tableStmt->Finalize(); + + // -- create statements and execute them + nsCOMPtr<mozIStorageStatement> stmt; + db->CreateStatement(NS_LITERAL_CSTRING( + "DELETE FROM test WHERE id = :param" + ), getter_AddRefs(stmt)); + + // -- bind multiple BindingParams + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (int32_t i = 0; i < 2; i++) { + nsCOMPtr<mozIStorageBindingParams> params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName(NS_LITERAL_CSTRING("param"), 1); + paramsArray->AddParams(params); + } + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + + mozIStorageBaseStatement *stmts[] = { + stmt, + }; + + check_transaction(db, stmts, ArrayLength(stmts), true); +} + +void (*gTests[])(void) = { + test_MultipleAsyncReadStatements, + test_MultipleReadStatements, + test_MultipleAsyncReadWriteStatements, + test_MultipleReadWriteStatements, + test_MultipleAsyncWriteStatements, + test_MultipleWriteStatements, + test_SingleAsyncReadStatement, + test_SingleReadStatement, + test_SingleAsyncWriteStatement, + test_SingleWriteStatement, + test_MultipleParamsAsyncReadStatement, + test_MultipleParamsReadStatement, + test_MultipleParamsAsyncWriteStatement, + test_MultipleParamsWriteStatement, +}; + +const char *file = __FILE__; +#define TEST_NAME "async statement execution transaction" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_async_callbacks_with_spun_event_loops.cpp b/storage/test/test_async_callbacks_with_spun_event_loops.cpp new file mode 100644 index 000000000..75687c7f3 --- /dev/null +++ b/storage/test/test_async_callbacks_with_spun_event_loops.cpp @@ -0,0 +1,174 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +#include "storage_test_harness.h" +#include "prthread.h" +#include "nsIEventTarget.h" +#include "nsIInterfaceRequestorUtils.h" +#include "mozilla/Attributes.h" + +#include "sqlite3.h" + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * Spins the events loop for current thread until aCondition is true. + */ +void +spin_events_loop_until_true(const bool* const aCondition) +{ + nsCOMPtr<nsIThread> thread(::do_GetCurrentThread()); + nsresult rv = NS_OK; + bool processed = true; + while (!(*aCondition) && NS_SUCCEEDED(rv)) { + rv = thread->ProcessNextEvent(true, &processed); + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageStatementCallback implementation + +class UnownedCallback final : public mozIStorageStatementCallback +{ +public: + NS_DECL_ISUPPORTS + + // Whether the object has been destroyed. + static bool sAlive; + // Whether the first result was received. + static bool sResult; + // Whether an error was received. + static bool sError; + + explicit UnownedCallback(mozIStorageConnection* aDBConn) + : mDBConn(aDBConn) + , mCompleted(false) + { + sAlive = true; + sResult = false; + sError = false; + } + +private: + ~UnownedCallback() + { + sAlive = false; + blocking_async_close(mDBConn); + } + +public: + NS_IMETHOD HandleResult(mozIStorageResultSet* aResultSet) override + { + sResult = true; + spin_events_loop_until_true(&mCompleted); + if (!sAlive) { + NS_RUNTIMEABORT("The statement callback was destroyed prematurely."); + } + return NS_OK; + } + + NS_IMETHOD HandleError(mozIStorageError* aError) override + { + sError = true; + spin_events_loop_until_true(&mCompleted); + if (!sAlive) { + NS_RUNTIMEABORT("The statement callback was destroyed prematurely."); + } + return NS_OK; + } + + NS_IMETHOD HandleCompletion(uint16_t aReason) override + { + mCompleted = true; + return NS_OK; + } + +protected: + nsCOMPtr<mozIStorageConnection> mDBConn; + bool mCompleted; +}; + +NS_IMPL_ISUPPORTS(UnownedCallback, mozIStorageStatementCallback) + +bool UnownedCallback::sAlive = false; +bool UnownedCallback::sResult = false; +bool UnownedCallback::sError = false; + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +void +test_SpinEventsLoopInHandleResult() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create a test table and populate it. + nsCOMPtr<mozIStorageStatement> stmt; + db->CreateStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(stmt)); + stmt->Execute(); + stmt->Finalize(); + + db->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO test (id) VALUES (?)" + ), getter_AddRefs(stmt)); + for (int32_t i = 0; i < 30; ++i) { + stmt->BindInt32ByIndex(0, i); + stmt->Execute(); + stmt->Reset(); + } + stmt->Finalize(); + + db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT * FROM test" + ), getter_AddRefs(stmt)); + nsCOMPtr<mozIStoragePendingStatement> ps; + do_check_success(stmt->ExecuteAsync(new UnownedCallback(db), + getter_AddRefs(ps))); + stmt->Finalize(); + + spin_events_loop_until_true(&UnownedCallback::sResult); +} + +void +test_SpinEventsLoopInHandleError() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create a test table and populate it. + nsCOMPtr<mozIStorageStatement> stmt; + db->CreateStatement(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + ), getter_AddRefs(stmt)); + stmt->Execute(); + stmt->Finalize(); + + db->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO test (id) VALUES (1)" + ), getter_AddRefs(stmt)); + stmt->Execute(); + stmt->Finalize(); + + // This will cause a constraint error. + db->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO test (id) VALUES (1)" + ), getter_AddRefs(stmt)); + nsCOMPtr<mozIStoragePendingStatement> ps; + do_check_success(stmt->ExecuteAsync(new UnownedCallback(db), + getter_AddRefs(ps))); + stmt->Finalize(); + + spin_events_loop_until_true(&UnownedCallback::sError); +} + +void (*gTests[])(void) = { + test_SpinEventsLoopInHandleResult, + test_SpinEventsLoopInHandleError, +}; + +const char *file = __FILE__; +#define TEST_NAME "test async callbacks with spun event loops" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_binding_params.cpp b/storage/test/test_binding_params.cpp new file mode 100644 index 000000000..e5af6009d --- /dev/null +++ b/storage/test/test_binding_params.cpp @@ -0,0 +1,215 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "mozStorageHelper.h" + +using namespace mozilla; + +/** + * This file tests binding and reading out string parameters through the + * mozIStorageStatement API. + */ + +void +test_ASCIIString() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create table with a single string column. + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (str STRING)" + )); + + // Create statements to INSERT and SELECT the string. + nsCOMPtr<mozIStorageStatement> insert, select; + (void)db->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO test (str) VALUES (?1)" + ), getter_AddRefs(insert)); + (void)db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT str FROM test" + ), getter_AddRefs(select)); + + // Roundtrip a string through the table, and ensure it comes out as expected. + nsAutoCString inserted("I'm an ASCII string"); + { + mozStorageStatementScoper scoper(insert); + bool hasResult; + do_check_true(NS_SUCCEEDED(insert->BindUTF8StringByIndex(0, inserted))); + do_check_true(NS_SUCCEEDED(insert->ExecuteStep(&hasResult))); + do_check_false(hasResult); + } + + nsAutoCString result; + { + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetUTF8String(0, result))); + } + + do_check_true(result == inserted); + + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DELETE FROM test")); +} + +void +test_CString() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create table with a single string column. + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (str STRING)" + )); + + // Create statements to INSERT and SELECT the string. + nsCOMPtr<mozIStorageStatement> insert, select; + (void)db->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO test (str) VALUES (?1)" + ), getter_AddRefs(insert)); + (void)db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT str FROM test" + ), getter_AddRefs(select)); + + // Roundtrip a string through the table, and ensure it comes out as expected. + static const char sCharArray[] = + "I'm not a \xff\x00\xac\xde\xbb ASCII string!"; + nsAutoCString inserted(sCharArray, ArrayLength(sCharArray) - 1); + do_check_true(inserted.Length() == ArrayLength(sCharArray) - 1); + { + mozStorageStatementScoper scoper(insert); + bool hasResult; + do_check_true(NS_SUCCEEDED(insert->BindUTF8StringByIndex(0, inserted))); + do_check_true(NS_SUCCEEDED(insert->ExecuteStep(&hasResult))); + do_check_false(hasResult); + } + + { + nsAutoCString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetUTF8String(0, result))); + + do_check_true(result == inserted); + } + + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DELETE FROM test")); +} + +void +test_UTFStrings() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create table with a single string column. + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (str STRING)" + )); + + // Create statements to INSERT and SELECT the string. + nsCOMPtr<mozIStorageStatement> insert, select; + (void)db->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO test (str) VALUES (?1)" + ), getter_AddRefs(insert)); + (void)db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT str FROM test" + ), getter_AddRefs(select)); + + // Roundtrip a UTF8 string through the table, using UTF8 input and output. + static const char sCharArray[] = + "I'm a \xc3\xbb\xc3\xbc\xc3\xa2\xc3\xa4\xc3\xa7 UTF8 string!"; + nsAutoCString insertedUTF8(sCharArray, ArrayLength(sCharArray) - 1); + do_check_true(insertedUTF8.Length() == ArrayLength(sCharArray) - 1); + NS_ConvertUTF8toUTF16 insertedUTF16(insertedUTF8); + do_check_true(insertedUTF8 == NS_ConvertUTF16toUTF8(insertedUTF16)); + { + mozStorageStatementScoper scoper(insert); + bool hasResult; + do_check_true(NS_SUCCEEDED(insert->BindUTF8StringByIndex(0, insertedUTF8))); + do_check_true(NS_SUCCEEDED(insert->ExecuteStep(&hasResult))); + do_check_false(hasResult); + } + + { + nsAutoCString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetUTF8String(0, result))); + + do_check_true(result == insertedUTF8); + } + + // Use UTF8 input and UTF16 output. + { + nsAutoString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetString(0, result))); + + do_check_true(result == insertedUTF16); + } + + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DELETE FROM test")); + + // Roundtrip the same string using UTF16 input and UTF8 output. + { + mozStorageStatementScoper scoper(insert); + bool hasResult; + do_check_true(NS_SUCCEEDED(insert->BindStringByIndex(0, insertedUTF16))); + do_check_true(NS_SUCCEEDED(insert->ExecuteStep(&hasResult))); + do_check_false(hasResult); + } + + { + nsAutoCString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetUTF8String(0, result))); + + do_check_true(result == insertedUTF8); + } + + // Use UTF16 input and UTF16 output. + { + nsAutoString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetString(0, result))); + + do_check_true(result == insertedUTF16); + } + + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING("DELETE FROM test")); +} + +void (*gTests[])(void) = { + test_ASCIIString, + test_CString, + test_UTFStrings, +}; + +const char *file = __FILE__; +#define TEST_NAME "binding string params" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_deadlock_detector.cpp b/storage/test/test_deadlock_detector.cpp new file mode 100644 index 000000000..1cbd79467 --- /dev/null +++ b/storage/test/test_deadlock_detector.cpp @@ -0,0 +1,602 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: sw=4 ts=4 et : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Note: This file is a copy of xpcom/tests/TestDeadlockDetector.cpp, but all + * mutexes were turned into SQLiteMutexes. + */ + +#include "prenv.h" +#include "prerror.h" +#include "prio.h" +#include "prproces.h" + +#include "nsMemory.h" + +#include "mozilla/CondVar.h" +#include "mozilla/ReentrantMonitor.h" +#include "SQLiteMutex.h" + +#include "TestHarness.h" + +using namespace mozilla; + +/** + * Helper class to allocate a sqlite3_mutex for our SQLiteMutex. Also makes + * keeping the test files in sync easier. + */ +class TestMutex : public mozilla::storage::SQLiteMutex +{ +public: + explicit TestMutex(const char* aName) + : mozilla::storage::SQLiteMutex(aName) + , mInner(sqlite3_mutex_alloc(SQLITE_MUTEX_FAST)) + { + NS_ASSERTION(mInner, "could not allocate a sqlite3_mutex"); + initWithMutex(mInner); + } + + ~TestMutex() + { + sqlite3_mutex_free(mInner); + } + + void Lock() + { + lock(); + } + + void Unlock() + { + unlock(); + } +private: + sqlite3_mutex *mInner; +}; + +static PRThread* +spawn(void (*run)(void*), void* arg) +{ + return PR_CreateThread(PR_SYSTEM_THREAD, + run, + arg, + PR_PRIORITY_NORMAL, + PR_GLOBAL_THREAD, + PR_JOINABLE_THREAD, + 0); +} + +#define PASS() \ + do { \ + passed(__FUNCTION__); \ + return NS_OK; \ + } while (0) + +#define FAIL(why) \ + do { \ + fail("%s | %s - %s", __FILE__, __FUNCTION__, why); \ + return NS_ERROR_FAILURE; \ + } while (0) + +//----------------------------------------------------------------------------- + +static const char* sPathToThisBinary; +static const char* sAssertBehaviorEnv = "XPCOM_DEBUG_BREAK=abort"; + +class Subprocess +{ +public: + // not available until process finishes + int32_t mExitCode; + nsCString mStdout; + nsCString mStderr; + + explicit Subprocess(const char* aTestName) { + // set up stdio redirection + PRFileDesc* readStdin; PRFileDesc* writeStdin; + PRFileDesc* readStdout; PRFileDesc* writeStdout; + PRFileDesc* readStderr; PRFileDesc* writeStderr; + PRProcessAttr* pattr = PR_NewProcessAttr(); + + NS_ASSERTION(pattr, "couldn't allocate process attrs"); + + NS_ASSERTION(PR_SUCCESS == PR_CreatePipe(&readStdin, &writeStdin), + "couldn't create child stdin pipe"); + NS_ASSERTION(PR_SUCCESS == PR_SetFDInheritable(readStdin, true), + "couldn't set child stdin inheritable"); + PR_ProcessAttrSetStdioRedirect(pattr, PR_StandardInput, readStdin); + + NS_ASSERTION(PR_SUCCESS == PR_CreatePipe(&readStdout, &writeStdout), + "couldn't create child stdout pipe"); + NS_ASSERTION(PR_SUCCESS == PR_SetFDInheritable(writeStdout, true), + "couldn't set child stdout inheritable"); + PR_ProcessAttrSetStdioRedirect(pattr, PR_StandardOutput, writeStdout); + + NS_ASSERTION(PR_SUCCESS == PR_CreatePipe(&readStderr, &writeStderr), + "couldn't create child stderr pipe"); + NS_ASSERTION(PR_SUCCESS == PR_SetFDInheritable(writeStderr, true), + "couldn't set child stderr inheritable"); + PR_ProcessAttrSetStdioRedirect(pattr, PR_StandardError, writeStderr); + + // set up argv with test name to run + char* const newArgv[3] = { + strdup(sPathToThisBinary), + strdup(aTestName), + 0 + }; + + // make sure the child will abort if an assertion fails + NS_ASSERTION(PR_SUCCESS == PR_SetEnv(sAssertBehaviorEnv), + "couldn't set XPCOM_DEBUG_BREAK env var"); + + PRProcess* proc; + NS_ASSERTION(proc = PR_CreateProcess(sPathToThisBinary, + newArgv, + 0, // inherit environment + pattr), + "couldn't create process"); + PR_Close(readStdin); + PR_Close(writeStdout); + PR_Close(writeStderr); + + mProc = proc; + mStdinfd = writeStdin; + mStdoutfd = readStdout; + mStderrfd = readStderr; + + free(newArgv[0]); + free(newArgv[1]); + PR_DestroyProcessAttr(pattr); + } + + void RunToCompletion(uint32_t aWaitMs) + { + PR_Close(mStdinfd); + + PRPollDesc pollfds[2]; + int32_t nfds; + bool stdoutOpen = true, stderrOpen = true; + char buf[4096]; + + PRIntervalTime now = PR_IntervalNow(); + PRIntervalTime deadline = now + PR_MillisecondsToInterval(aWaitMs); + + while ((stdoutOpen || stderrOpen) && now < deadline) { + nfds = 0; + if (stdoutOpen) { + pollfds[nfds].fd = mStdoutfd; + pollfds[nfds].in_flags = PR_POLL_READ; + pollfds[nfds].out_flags = 0; + ++nfds; + } + if (stderrOpen) { + pollfds[nfds].fd = mStderrfd; + pollfds[nfds].in_flags = PR_POLL_READ; + pollfds[nfds].out_flags = 0; + ++nfds; + } + + int32_t rv = PR_Poll(pollfds, nfds, deadline - now); + NS_ASSERTION(0 <= rv, PR_ErrorToName(PR_GetError())); + + if (0 == rv) { // timeout + fputs("(timed out!)\n", stderr); + Finish(false); // abnormal + return; + } + + for (int32_t i = 0; i < nfds; ++i) { + if (!pollfds[i].out_flags) + continue; + + bool isStdout = mStdoutfd == pollfds[i].fd; + int32_t len = 0; + + if (PR_POLL_READ & pollfds[i].out_flags) { + len = PR_Read(pollfds[i].fd, buf, sizeof(buf) - 1); + NS_ASSERTION(0 <= len, PR_ErrorToName(PR_GetError())); + } + else if (!(PR_POLL_HUP & pollfds[i].out_flags)) { + NS_ERROR(PR_ErrorToName(PR_GetError())); + } + + if (0 < len) { + buf[len] = '\0'; + if (isStdout) + mStdout += buf; + else + mStderr += buf; + } + else if (isStdout) { + stdoutOpen = false; + } + else { + stderrOpen = false; + } + } + + now = PR_IntervalNow(); + } + + if (stdoutOpen) + fputs("(stdout still open!)\n", stderr); + if (stderrOpen) + fputs("(stderr still open!)\n", stderr); + if (now > deadline) + fputs("(timed out!)\n", stderr); + + Finish(!stdoutOpen && !stderrOpen && now <= deadline); + } + +private: + void Finish(bool normalExit) { + if (!normalExit) { + PR_KillProcess(mProc); + mExitCode = -1; + int32_t dummy; + PR_WaitProcess(mProc, &dummy); + } + else { + PR_WaitProcess(mProc, &mExitCode); // this had better not block ... + } + + PR_Close(mStdoutfd); + PR_Close(mStderrfd); + } + + PRProcess* mProc; + PRFileDesc* mStdinfd; // writeable + PRFileDesc* mStdoutfd; // readable + PRFileDesc* mStderrfd; // readable +}; + +//----------------------------------------------------------------------------- +// Harness for checking detector errors +bool +CheckForDeadlock(const char* test, const char* const* findTokens) +{ + Subprocess proc(test); + proc.RunToCompletion(5000); + + if (0 == proc.mExitCode) + return false; + + int32_t idx = 0; + for (const char* const* tp = findTokens; *tp; ++tp) { + const char* const token = *tp; +#ifdef MOZILLA_INTERNAL_API + idx = proc.mStderr.Find(token, false, idx); +#else + nsCString tokenCString(token); + idx = proc.mStderr.Find(tokenCString, idx); +#endif + if (-1 == idx) { + printf("(missed token '%s' in output)\n", token); + puts("----------------------------------\n"); + puts(proc.mStderr.get()); + puts("----------------------------------\n"); + return false; + } + idx += strlen(token); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Single-threaded sanity tests + +// Stupidest possible deadlock. +int +Sanity_Child() +{ + TestMutex m1("dd.sanity.m1"); + m1.Lock(); + m1.Lock(); + return 0; // not reached +} + +nsresult +Sanity() +{ + const char* const tokens[] = { + "###!!! ERROR: Potential deadlock detected", + "=== Cyclical dependency starts at\n--- Mutex : dd.sanity.m1", + "=== Cycle completed at\n--- Mutex : dd.sanity.m1", + "###!!! Deadlock may happen NOW!", // better catch these easy cases... + "###!!! ASSERTION: Potential deadlock detected", + 0 + }; + if (CheckForDeadlock("Sanity", tokens)) { + PASS(); + } else { + FAIL("deadlock not detected"); + } +} + +// Slightly less stupid deadlock. +int +Sanity2_Child() +{ + TestMutex m1("dd.sanity2.m1"); + TestMutex m2("dd.sanity2.m2"); + m1.Lock(); + m2.Lock(); + m1.Lock(); + return 0; // not reached +} + +nsresult +Sanity2() +{ + const char* const tokens[] = { + "###!!! ERROR: Potential deadlock detected", + "=== Cyclical dependency starts at\n--- Mutex : dd.sanity2.m1", + "--- Next dependency:\n--- Mutex : dd.sanity2.m2", + "=== Cycle completed at\n--- Mutex : dd.sanity2.m1", + "###!!! Deadlock may happen NOW!", // better catch these easy cases... + "###!!! ASSERTION: Potential deadlock detected", + 0 + }; + if (CheckForDeadlock("Sanity2", tokens)) { + PASS(); + } else { + FAIL("deadlock not detected"); + } +} + + +int +Sanity3_Child() +{ + TestMutex m1("dd.sanity3.m1"); + TestMutex m2("dd.sanity3.m2"); + TestMutex m3("dd.sanity3.m3"); + TestMutex m4("dd.sanity3.m4"); + + m1.Lock(); + m2.Lock(); + m3.Lock(); + m4.Lock(); + m4.Unlock(); + m3.Unlock(); + m2.Unlock(); + m1.Unlock(); + + m4.Lock(); + m1.Lock(); + return 0; +} + +nsresult +Sanity3() +{ + const char* const tokens[] = { + "###!!! ERROR: Potential deadlock detected", + "=== Cyclical dependency starts at\n--- Mutex : dd.sanity3.m1", + "--- Next dependency:\n--- Mutex : dd.sanity3.m2", + "--- Next dependency:\n--- Mutex : dd.sanity3.m3", + "--- Next dependency:\n--- Mutex : dd.sanity3.m4", + "=== Cycle completed at\n--- Mutex : dd.sanity3.m1", + "###!!! ASSERTION: Potential deadlock detected", + 0 + }; + if (CheckForDeadlock("Sanity3", tokens)) { + PASS(); + } else { + FAIL("deadlock not detected"); + } +} + + +int +Sanity4_Child() +{ + mozilla::ReentrantMonitor m1("dd.sanity4.m1"); + TestMutex m2("dd.sanity4.m2"); + m1.Enter(); + m2.Lock(); + m1.Enter(); + return 0; +} + +nsresult +Sanity4() +{ + const char* const tokens[] = { + "Re-entering ReentrantMonitor after acquiring other resources", + "###!!! ERROR: Potential deadlock detected", + "=== Cyclical dependency starts at\n--- ReentrantMonitor : dd.sanity4.m1", + "--- Next dependency:\n--- Mutex : dd.sanity4.m2", + "=== Cycle completed at\n--- ReentrantMonitor : dd.sanity4.m1", + "###!!! ASSERTION: Potential deadlock detected", + 0 + }; + if (CheckForDeadlock("Sanity4", tokens)) { + PASS(); + } else { + FAIL("deadlock not detected"); + } +} + +//----------------------------------------------------------------------------- +// Multithreaded tests + +TestMutex* ttM1; +TestMutex* ttM2; + +static void +TwoThreads_thread(void* arg) +{ + int32_t m1First = NS_PTR_TO_INT32(arg); + if (m1First) { + ttM1->Lock(); + ttM2->Lock(); + ttM2->Unlock(); + ttM1->Unlock(); + } + else { + ttM2->Lock(); + ttM1->Lock(); + ttM1->Unlock(); + ttM2->Unlock(); + } +} + +int +TwoThreads_Child() +{ + ttM1 = new TestMutex("dd.twothreads.m1"); + ttM2 = new TestMutex("dd.twothreads.m2"); + if (!ttM1 || !ttM2) + NS_RUNTIMEABORT("couldn't allocate mutexes"); + + PRThread* t1 = spawn(TwoThreads_thread, (void*) 0); + PR_JoinThread(t1); + + PRThread* t2 = spawn(TwoThreads_thread, (void*) 1); + PR_JoinThread(t2); + + return 0; +} + +nsresult +TwoThreads() +{ + const char* const tokens[] = { + "###!!! ERROR: Potential deadlock detected", + "=== Cyclical dependency starts at\n--- Mutex : dd.twothreads.m2", + "--- Next dependency:\n--- Mutex : dd.twothreads.m1", + "=== Cycle completed at\n--- Mutex : dd.twothreads.m2", + "###!!! ASSERTION: Potential deadlock detected", + 0 + }; + + if (CheckForDeadlock("TwoThreads", tokens)) { + PASS(); + } else { + FAIL("deadlock not detected"); + } +} + + +TestMutex* cndMs[4]; +const uint32_t K = 100000; + +static void +ContentionNoDeadlock_thread(void* arg) +{ + int32_t starti = NS_PTR_TO_INT32(arg); + + for (uint32_t k = 0; k < K; ++k) { + for (int32_t i = starti; i < (int32_t) ArrayLength(cndMs); ++i) + cndMs[i]->Lock(); + // comment out the next two lines for deadlocking fun! + for (int32_t i = ArrayLength(cndMs) - 1; i >= starti; --i) + cndMs[i]->Unlock(); + + starti = (starti + 1) % 3; + } +} + +int +ContentionNoDeadlock_Child() +{ + PRThread* threads[3]; + + for (uint32_t i = 0; i < ArrayLength(cndMs); ++i) + cndMs[i] = new TestMutex("dd.cnd.ms"); + + for (int32_t i = 0; i < (int32_t) ArrayLength(threads); ++i) + threads[i] = spawn(ContentionNoDeadlock_thread, NS_INT32_TO_PTR(i)); + + for (uint32_t i = 0; i < ArrayLength(threads); ++i) + PR_JoinThread(threads[i]); + + for (uint32_t i = 0; i < ArrayLength(cndMs); ++i) + delete cndMs[i]; + + return 0; +} + +nsresult +ContentionNoDeadlock() +{ + const char * func = __func__; + Subprocess proc(func); + proc.RunToCompletion(60000); + if (0 != proc.mExitCode) { + printf("(expected 0 == return code, got %d)\n", proc.mExitCode); + puts("(output)\n----------------------------------\n"); + puts(proc.mStdout.get()); + puts("----------------------------------\n"); + puts("(error output)\n----------------------------------\n"); + puts(proc.mStderr.get()); + puts("----------------------------------\n"); + + FAIL("deadlock"); + } + PASS(); +} + + + +//----------------------------------------------------------------------------- + +int +main(int argc, char** argv) +{ + if (1 < argc) { + // XXX can we run w/o scoped XPCOM? + const char* test = argv[1]; + ScopedXPCOM xpcom(test); + if (xpcom.failed()) + return 1; + + // running in a spawned process. call the specificed child function. + if (!strcmp("Sanity", test)) + return Sanity_Child(); + if (!strcmp("Sanity2", test)) + return Sanity2_Child(); + if (!strcmp("Sanity3", test)) + return Sanity3_Child(); + if (!strcmp("Sanity4", test)) + return Sanity4_Child(); + + if (!strcmp("TwoThreads", test)) + return TwoThreads_Child(); + if (!strcmp("ContentionNoDeadlock", test)) + return ContentionNoDeadlock_Child(); + + fail("%s | %s - unknown child test", __FILE__, __FUNCTION__); + return 1; + } + + ScopedXPCOM xpcom("Storage deadlock detector correctness (" __FILE__ ")"); + if (xpcom.failed()) + return 1; + + // in the first invocation of this process. we will be the "driver". + int rv = 0; + + sPathToThisBinary = argv[0]; + + if (NS_FAILED(Sanity())) + rv = 1; + if (NS_FAILED(Sanity2())) + rv = 1; + if (NS_FAILED(Sanity3())) + rv = 1; + if (NS_FAILED(Sanity4())) + rv = 1; + + if (NS_FAILED(TwoThreads())) + rv = 1; + if (NS_FAILED(ContentionNoDeadlock())) + rv = 1; + + return rv; +} diff --git a/storage/test/test_file_perms.cpp b/storage/test/test_file_perms.cpp new file mode 100644 index 000000000..1235fe09b --- /dev/null +++ b/storage/test/test_file_perms.cpp @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" +#include "nsIFile.h" +#include "prio.h" + +/** + * This file tests that the file permissions of the sqlite files match what + * we request they be + */ + +void +test_file_perms() +{ + nsCOMPtr<mozIStorageConnection> db(getDatabase()); + nsCOMPtr<nsIFile> dbFile; + do_check_success(db->GetDatabaseFile(getter_AddRefs(dbFile))); + + uint32_t perms = 0; + do_check_success(dbFile->GetPermissions(&perms)); + + // This reflexts the permissions defined by SQLITE_DEFAULT_FILE_PERMISSIONS in + // db/sqlite3/src/Makefile.in and must be kept in sync with that +#ifdef ANDROID + do_check_true(perms == (PR_IRUSR | PR_IWUSR)); +#elif defined(XP_WIN) + do_check_true(perms == (PR_IRUSR | PR_IWUSR | PR_IRGRP | PR_IWGRP | PR_IROTH | PR_IWOTH)); +#else + do_check_true(perms == (PR_IRUSR | PR_IWUSR | PR_IRGRP | PR_IROTH)); +#endif +} + +void (*gTests[])(void) = { + test_file_perms, +}; + +const char *file = __FILE__; +#define TEST_NAME "file perms" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_mutex.cpp b/storage/test/test_mutex.cpp new file mode 100644 index 000000000..8fd0eb3f3 --- /dev/null +++ b/storage/test/test_mutex.cpp @@ -0,0 +1,85 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "SQLiteMutex.h" + +using namespace mozilla; +using namespace mozilla::storage; + +/** + * This file test our sqlite3_mutex wrapper in SQLiteMutex.h. + */ + +void +test_AutoLock() +{ + int lockTypes[] = { + SQLITE_MUTEX_FAST, + SQLITE_MUTEX_RECURSIVE, + }; + for (size_t i = 0; i < ArrayLength(lockTypes); i++) { + // Get our test mutex (we have to allocate a SQLite mutex of the right type + // too!). + SQLiteMutex mutex("TestMutex"); + sqlite3_mutex *inner = sqlite3_mutex_alloc(lockTypes[i]); + do_check_true(inner); + mutex.initWithMutex(inner); + + // And test that our automatic locking wrapper works as expected. + mutex.assertNotCurrentThreadOwns(); + { + SQLiteMutexAutoLock lockedScope(mutex); + mutex.assertCurrentThreadOwns(); + } + mutex.assertNotCurrentThreadOwns(); + + // Free the wrapped mutex - we don't need it anymore. + sqlite3_mutex_free(inner); + } +} + +void +test_AutoUnlock() +{ + int lockTypes[] = { + SQLITE_MUTEX_FAST, + SQLITE_MUTEX_RECURSIVE, + }; + for (size_t i = 0; i < ArrayLength(lockTypes); i++) { + // Get our test mutex (we have to allocate a SQLite mutex of the right type + // too!). + SQLiteMutex mutex("TestMutex"); + sqlite3_mutex *inner = sqlite3_mutex_alloc(lockTypes[i]); + do_check_true(inner); + mutex.initWithMutex(inner); + + // And test that our automatic unlocking wrapper works as expected. + { + SQLiteMutexAutoLock lockedScope(mutex); + + { + SQLiteMutexAutoUnlock unlockedScope(mutex); + mutex.assertNotCurrentThreadOwns(); + } + mutex.assertCurrentThreadOwns(); + } + + // Free the wrapped mutex - we don't need it anymore. + sqlite3_mutex_free(inner); + } +} + +void (*gTests[])(void) = { + test_AutoLock, + test_AutoUnlock, +}; + +const char *file = __FILE__; +#define TEST_NAME "SQLiteMutex" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_service_init_background_thread.cpp b/storage/test/test_service_init_background_thread.cpp new file mode 100644 index 000000000..56e61a19b --- /dev/null +++ b/storage/test/test_service_init_background_thread.cpp @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim set:sw=2 ts=2 et lcs=trail\:.,tab\:>~ : */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "nsThreadUtils.h" + +/** + * This file tests that the storage service can be initialized off of the main + * thread without issue. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Helpers + +class ServiceInitializer : public mozilla::Runnable +{ +public: + NS_IMETHOD Run() override + { + // Use an explicit do_GetService instead of getService so that the check in + // getService doesn't blow up. + nsCOMPtr<mozIStorageService> service = do_GetService("@mozilla.org/storage/service;1"); + do_check_false(service); + return NS_OK; + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Test Functions + +void +test_service_initialization_on_background_thread() +{ + nsCOMPtr<nsIRunnable> event = new ServiceInitializer(); + do_check_true(event); + + nsCOMPtr<nsIThread> thread; + do_check_success(NS_NewThread(getter_AddRefs(thread))); + + do_check_success(thread->Dispatch(event, NS_DISPATCH_NORMAL)); + + // Shutting down the thread will spin the event loop until all work in its + // event queue is completed. This will act as our thread synchronization. + do_check_success(thread->Shutdown()); +} + +void (*gTests[])(void) = { + test_service_initialization_on_background_thread, +}; + +const char *file = __FILE__; +#define TEST_NAME "Background Thread Initialization" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_statement_scoper.cpp b/storage/test/test_statement_scoper.cpp new file mode 100644 index 000000000..784d8d772 --- /dev/null +++ b/storage/test/test_statement_scoper.cpp @@ -0,0 +1,104 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "mozStorageHelper.h" + +/** + * This file test our statement scoper in mozStorageHelper.h. + */ + +void +test_automatic_reset() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Need to create a table to populate sqlite_master with an entry. + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + )); + + nsCOMPtr<mozIStorageStatement> stmt; + (void)db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT * FROM sqlite_master" + ), getter_AddRefs(stmt)); + + // Reality check - make sure we start off in the right state. + int32_t state = -1; + (void)stmt->GetState(&state); + do_check_true(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_READY); + + // Start executing the statement, which will put it into an executing state. + { + mozStorageStatementScoper scoper(stmt); + bool hasMore; + do_check_true(NS_SUCCEEDED(stmt->ExecuteStep(&hasMore))); + + // Reality check that we are executing. + state = -1; + (void)stmt->GetState(&state); + do_check_true(state == + mozIStorageStatement::MOZ_STORAGE_STATEMENT_EXECUTING); + } + + // And we should be ready again. + state = -1; + (void)stmt->GetState(&state); + do_check_true(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_READY); +} + +void +test_Abandon() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Need to create a table to populate sqlite_master with an entry. + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + )); + + nsCOMPtr<mozIStorageStatement> stmt; + (void)db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT * FROM sqlite_master" + ), getter_AddRefs(stmt)); + + // Reality check - make sure we start off in the right state. + int32_t state = -1; + (void)stmt->GetState(&state); + do_check_true(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_READY); + + // Start executing the statement, which will put it into an executing state. + { + mozStorageStatementScoper scoper(stmt); + bool hasMore; + do_check_true(NS_SUCCEEDED(stmt->ExecuteStep(&hasMore))); + + // Reality check that we are executing. + state = -1; + (void)stmt->GetState(&state); + do_check_true(state == + mozIStorageStatement::MOZ_STORAGE_STATEMENT_EXECUTING); + + // And call Abandon. We should not reset now when we fall out of scope. + scoper.Abandon(); + } + + // And we should still be executing. + state = -1; + (void)stmt->GetState(&state); + do_check_true(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_EXECUTING); +} + +void (*gTests[])(void) = { + test_automatic_reset, + test_Abandon, +}; + +const char *file = __FILE__; +#define TEST_NAME "statement scoper" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_transaction_helper.cpp b/storage/test/test_transaction_helper.cpp new file mode 100644 index 000000000..4277c8f94 --- /dev/null +++ b/storage/test/test_transaction_helper.cpp @@ -0,0 +1,175 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "mozStorageHelper.h" +#include "mozStorageConnection.h" + +using namespace mozilla; +using namespace mozilla::storage; + +bool has_transaction(mozIStorageConnection* aDB) { + return !(static_cast<Connection *>(aDB)->getAutocommit()); +} + +/** + * This file test our Transaction helper in mozStorageHelper.h. + */ + +void +test_Commit() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create a table in a transaction, call Commit, and make sure that it does + // exists after the transaction falls out of scope. + { + mozStorageTransaction transaction(db, false); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + )); + (void)transaction.Commit(); + } + do_check_false(has_transaction(db)); + + bool exists = false; + (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); + do_check_true(exists); +} + +void +test_Rollback() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create a table in a transaction, call Rollback, and make sure that it does + // not exists after the transaction falls out of scope. + { + mozStorageTransaction transaction(db, true); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + )); + (void)transaction.Rollback(); + } + do_check_false(has_transaction(db)); + + bool exists = true; + (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); + do_check_false(exists); +} + +void +test_AutoCommit() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create a table in a transaction, and make sure that it exists after the + // transaction falls out of scope. This means the Commit was successful. + { + mozStorageTransaction transaction(db, true); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + )); + } + do_check_false(has_transaction(db)); + + bool exists = false; + (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); + do_check_true(exists); +} + +void +test_AutoRollback() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Create a table in a transaction, and make sure that it does not exists + // after the transaction falls out of scope. This means the Rollback was + // successful. + { + mozStorageTransaction transaction(db, false); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + )); + } + do_check_false(has_transaction(db)); + + bool exists = true; + (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); + do_check_false(exists); +} + +void +test_null_database_connection() +{ + // We permit the use of the Transaction helper when passing a null database + // in, so we need to make sure this still works without crashing. + mozStorageTransaction transaction(nullptr, false); + do_check_true(NS_SUCCEEDED(transaction.Commit())); + do_check_true(NS_SUCCEEDED(transaction.Rollback())); +} + +void +test_async_Commit() +{ + // note this will be active for any following test. + hook_sqlite_mutex(); + + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- wedge the thread + nsCOMPtr<nsIThread> target(get_conn_async_thread(db)); + do_check_true(target); + RefPtr<ThreadWedger> wedger (new ThreadWedger(target)); + + { + mozStorageTransaction transaction(db, false, + mozIStorageConnection::TRANSACTION_DEFERRED, + true); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + )); + (void)transaction.Commit(); + } + do_check_true(has_transaction(db)); + + // -- unwedge the async thread + wedger->unwedge(); + + // Ensure the transaction has done its job by enqueueing an async execution. + nsCOMPtr<mozIStorageAsyncStatement> stmt; + (void)db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "SELECT NULL" + ), getter_AddRefs(stmt)); + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(has_transaction(db)); + bool exists = false; + (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); + do_check_true(exists); + + blocking_async_close(db); +} + +void (*gTests[])(void) = { + test_Commit, + test_Rollback, + test_AutoCommit, + test_AutoRollback, + test_null_database_connection, + test_async_Commit, +}; + +const char *file = __FILE__; +#define TEST_NAME "transaction helper" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_true_async.cpp b/storage/test/test_true_async.cpp new file mode 100644 index 000000000..fea00392d --- /dev/null +++ b/storage/test/test_true_async.cpp @@ -0,0 +1,186 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +void +test_TrueAsyncStatement() +{ + // (only the first test needs to call this) + hook_sqlite_mutex(); + + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // Start watching for forbidden mutex usage. + watch_for_mutex_use_on_this_thread(); + + // - statement with nothing to bind + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement( + NS_LITERAL_CSTRING("CREATE TABLE test (id INTEGER PRIMARY KEY)"), + getter_AddRefs(stmt) + ); + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(mutex_used_on_watched_thread); + + // - statement with something to bind ordinally + db->CreateAsyncStatement( + NS_LITERAL_CSTRING("INSERT INTO test (id) VALUES (?)"), + getter_AddRefs(stmt) + ); + stmt->BindInt32ByIndex(0, 1); + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(mutex_used_on_watched_thread); + + // - statement with something to bind by name + db->CreateAsyncStatement( + NS_LITERAL_CSTRING("INSERT INTO test (id) VALUES (:id)"), + getter_AddRefs(stmt) + ); + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + nsCOMPtr<mozIStorageBindingParams> params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName(NS_LITERAL_CSTRING("id"), 2); + paramsArray->AddParams(params); + params = nullptr; + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(mutex_used_on_watched_thread); + + // - now, make sure creating a sync statement does trigger our guard. + // (If this doesn't happen, our test is bunk and it's important to know that.) + nsCOMPtr<mozIStorageStatement> syncStmt; + db->CreateStatement(NS_LITERAL_CSTRING("SELECT * FROM test"), + getter_AddRefs(syncStmt)); + syncStmt->Finalize(); + do_check_true(mutex_used_on_watched_thread); + + blocking_async_close(db); +} + +/** + * Test that cancellation before a statement is run successfully stops the + * statement from executing. + */ +void +test_AsyncCancellation() +{ + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + + // -- wedge the thread + nsCOMPtr<nsIThread> target(get_conn_async_thread(db)); + do_check_true(target); + RefPtr<ThreadWedger> wedger (new ThreadWedger(target)); + + // -- create statements and cancel them + // - async + nsCOMPtr<mozIStorageAsyncStatement> asyncStmt; + db->CreateAsyncStatement( + NS_LITERAL_CSTRING("CREATE TABLE asyncTable (id INTEGER PRIMARY KEY)"), + getter_AddRefs(asyncStmt) + ); + + RefPtr<AsyncStatementSpinner> asyncSpin(new AsyncStatementSpinner()); + nsCOMPtr<mozIStoragePendingStatement> asyncPend; + (void)asyncStmt->ExecuteAsync(asyncSpin, getter_AddRefs(asyncPend)); + do_check_true(asyncPend); + asyncPend->Cancel(); + + // - sync + nsCOMPtr<mozIStorageStatement> syncStmt; + db->CreateStatement( + NS_LITERAL_CSTRING("CREATE TABLE syncTable (id INTEGER PRIMARY KEY)"), + getter_AddRefs(syncStmt) + ); + + RefPtr<AsyncStatementSpinner> syncSpin(new AsyncStatementSpinner()); + nsCOMPtr<mozIStoragePendingStatement> syncPend; + (void)syncStmt->ExecuteAsync(syncSpin, getter_AddRefs(syncPend)); + do_check_true(syncPend); + syncPend->Cancel(); + + // -- unwedge the async thread + wedger->unwedge(); + + // -- verify that both statements report they were canceled + asyncSpin->SpinUntilCompleted(); + do_check_true(asyncSpin->completionReason == + mozIStorageStatementCallback::REASON_CANCELED); + + syncSpin->SpinUntilCompleted(); + do_check_true(syncSpin->completionReason == + mozIStorageStatementCallback::REASON_CANCELED); + + // -- verify that neither statement constructed their tables + nsresult rv; + bool exists; + rv = db->TableExists(NS_LITERAL_CSTRING("asyncTable"), &exists); + do_check_true(rv == NS_OK); + do_check_false(exists); + rv = db->TableExists(NS_LITERAL_CSTRING("syncTable"), &exists); + do_check_true(rv == NS_OK); + do_check_false(exists); + + // -- cleanup + asyncStmt->Finalize(); + syncStmt->Finalize(); + blocking_async_close(db); +} + +/** + * Test that the destructor for an asynchronous statement which has a + * sqlite3_stmt will dispatch that statement to the async thread for + * finalization rather than trying to finalize it on the main thread + * (and thereby running afoul of our mutex use detector). + */ +void test_AsyncDestructorFinalizesOnAsyncThread() +{ + // test_TrueAsyncStatement called hook_sqlite_mutex() for us + + nsCOMPtr<mozIStorageConnection> db(getMemoryDatabase()); + watch_for_mutex_use_on_this_thread(); + + // -- create an async statement + nsCOMPtr<mozIStorageAsyncStatement> stmt; + db->CreateAsyncStatement( + NS_LITERAL_CSTRING("CREATE TABLE test (id INTEGER PRIMARY KEY)"), + getter_AddRefs(stmt) + ); + + // -- execute it so it gets a sqlite3_stmt that needs to be finalized + blocking_async_execute(stmt); + do_check_false(mutex_used_on_watched_thread); + + // -- forget our reference + stmt = nullptr; + + // -- verify the mutex was not touched + do_check_false(mutex_used_on_watched_thread); + + // -- make sure the statement actually gets finalized / cleanup + // the close will assert if we failed to finalize! + blocking_async_close(db); +} + +void (*gTests[])(void) = { + // this test must be first because it hooks the mutex mechanics + test_TrueAsyncStatement, + test_AsyncCancellation, + test_AsyncDestructorFinalizesOnAsyncThread +}; + +const char *file = __FILE__; +#define TEST_NAME "true async statement" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/test_unlock_notify.cpp b/storage/test/test_unlock_notify.cpp new file mode 100644 index 000000000..0a7ca67f9 --- /dev/null +++ b/storage/test/test_unlock_notify.cpp @@ -0,0 +1,266 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim set:sw=2 ts=2 et lcs=trail\:.,tab\:>~ : */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "storage_test_harness.h" + +#include "mozilla/ReentrantMonitor.h" +#include "nsThreadUtils.h" +#include "mozIStorageStatement.h" + +/** + * This file tests that our implementation around sqlite3_unlock_notify works + * as expected. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Helpers + +enum State { + STARTING, + WRITE_LOCK, + READ_LOCK, + TEST_DONE +}; + +class DatabaseLocker : public mozilla::Runnable +{ +public: + explicit DatabaseLocker(const char* aSQL) + : monitor("DatabaseLocker::monitor") + , mSQL(aSQL) + , mState(STARTING) + { + } + + void RunInBackground() + { + (void)NS_NewNamedThread("DatabaseLocker", getter_AddRefs(mThread)); + do_check_true(mThread); + + do_check_success(mThread->Dispatch(this, NS_DISPATCH_NORMAL)); + } + + void Shutdown() + { + if (mThread) { + mThread->Shutdown(); + } + } + + NS_IMETHOD Run() override + { + mozilla::ReentrantMonitorAutoEnter lock(monitor); + + nsCOMPtr<mozIStorageConnection> db(getDatabase()); + + nsCString sql(mSQL); + nsCOMPtr<mozIStorageStatement> stmt; + do_check_success(db->CreateStatement(sql, getter_AddRefs(stmt))); + + bool hasResult; + do_check_success(stmt->ExecuteStep(&hasResult)); + + Notify(WRITE_LOCK); + WaitFor(TEST_DONE); + + return NS_OK; + } + + void WaitFor(State aState) + { + monitor.AssertCurrentThreadIn(); + while (mState != aState) { + do_check_success(monitor.Wait()); + } + } + + void Notify(State aState) + { + monitor.AssertCurrentThreadIn(); + mState = aState; + do_check_success(monitor.Notify()); + } + + mozilla::ReentrantMonitor monitor; + +protected: + nsCOMPtr<nsIThread> mThread; + const char *const mSQL; + State mState; +}; + +class DatabaseTester : public DatabaseLocker +{ +public: + DatabaseTester(mozIStorageConnection *aConnection, + const char* aSQL) + : DatabaseLocker(aSQL) + , mConnection(aConnection) + { + } + + NS_IMETHOD Run() override + { + mozilla::ReentrantMonitorAutoEnter lock(monitor); + WaitFor(READ_LOCK); + + nsCString sql(mSQL); + nsCOMPtr<mozIStorageStatement> stmt; + do_check_success(mConnection->CreateStatement(sql, getter_AddRefs(stmt))); + + bool hasResult; + nsresult rv = stmt->ExecuteStep(&hasResult); + do_check_eq(rv, NS_ERROR_FILE_IS_LOCKED); + + // Finalize our statement and null out our connection before notifying to + // ensure that we close on the proper thread. + rv = stmt->Finalize(); + do_check_eq(rv, NS_ERROR_FILE_IS_LOCKED); + mConnection = nullptr; + + Notify(TEST_DONE); + + return NS_OK; + } + +private: + nsCOMPtr<mozIStorageConnection> mConnection; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Test Functions + +void +setup() +{ + nsCOMPtr<mozIStorageConnection> db(getDatabase()); + + // Create and populate a dummy table. + nsresult rv = db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY, data STRING)" + )); + do_check_success(rv); + rv = db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO test (data) VALUES ('foo')" + )); + do_check_success(rv); + rv = db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO test (data) VALUES ('bar')" + )); + do_check_success(rv); + rv = db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE UNIQUE INDEX unique_data ON test (data)" + )); + do_check_success(rv); +} + +void +test_step_locked_does_not_block_main_thread() +{ + nsCOMPtr<mozIStorageConnection> db(getDatabase()); + + // Need to prepare our statement ahead of time so we make sure to only test + // step and not prepare. + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = db->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO test (data) VALUES ('test1')" + ), getter_AddRefs(stmt)); + do_check_success(rv); + + RefPtr<DatabaseLocker> locker(new DatabaseLocker("SELECT * FROM test")); + do_check_true(locker); + { + mozilla::ReentrantMonitorAutoEnter lock(locker->monitor); + locker->RunInBackground(); + + // Wait for the locker to notify us that it has locked the database properly. + locker->WaitFor(WRITE_LOCK); + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + do_check_eq(rv, NS_ERROR_FILE_IS_LOCKED); + + locker->Notify(TEST_DONE); + } + locker->Shutdown(); +} + +void +test_drop_index_does_not_loop() +{ + nsCOMPtr<mozIStorageConnection> db(getDatabase()); + + // Need to prepare our statement ahead of time so we make sure to only test + // step and not prepare. + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT * FROM test" + ), getter_AddRefs(stmt)); + do_check_success(rv); + + RefPtr<DatabaseTester> tester = + new DatabaseTester(db, "DROP INDEX unique_data"); + do_check_true(tester); + { + mozilla::ReentrantMonitorAutoEnter lock(tester->monitor); + tester->RunInBackground(); + + // Hold a read lock on the database, and then let the tester try to execute. + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + do_check_success(rv); + do_check_true(hasResult); + tester->Notify(READ_LOCK); + + // Make sure the tester finishes its test before we move on. + tester->WaitFor(TEST_DONE); + } + tester->Shutdown(); +} + +void +test_drop_table_does_not_loop() +{ + nsCOMPtr<mozIStorageConnection> db(getDatabase()); + + // Need to prepare our statement ahead of time so we make sure to only test + // step and not prepare. + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = db->CreateStatement(NS_LITERAL_CSTRING( + "SELECT * FROM test" + ), getter_AddRefs(stmt)); + do_check_success(rv); + + RefPtr<DatabaseTester> tester(new DatabaseTester(db, "DROP TABLE test")); + do_check_true(tester); + { + mozilla::ReentrantMonitorAutoEnter lock(tester->monitor); + tester->RunInBackground(); + + // Hold a read lock on the database, and then let the tester try to execute. + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + do_check_success(rv); + do_check_true(hasResult); + tester->Notify(READ_LOCK); + + // Make sure the tester finishes its test before we move on. + tester->WaitFor(TEST_DONE); + } + tester->Shutdown(); +} + +void (*gTests[])(void) = { + setup, + test_step_locked_does_not_block_main_thread, + test_drop_index_does_not_loop, + test_drop_table_does_not_loop, +}; + +const char *file = __FILE__; +#define TEST_NAME "sqlite3_unlock_notify" +#define TEST_FILE file +#include "storage_test_harness_tail.h" diff --git a/storage/test/unit/corruptDB.sqlite b/storage/test/unit/corruptDB.sqlite Binary files differnew file mode 100644 index 000000000..b234246ca --- /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 000000000..5f7498bfc --- /dev/null +++ b/storage/test/unit/fakeDB.sqlite @@ -0,0 +1 @@ +BACON diff --git a/storage/test/unit/head_storage.js b/storage/test/unit/head_storage.js new file mode 100644 index 000000000..40374afad --- /dev/null +++ b/storage/test/unit/head_storage.js @@ -0,0 +1,372 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); + + +do_get_profile(); +var dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + +var gDBConn = null; + +function getTestDB() +{ + var db = dirSvc.get("ProfD", Ci.nsIFile); + db.append("test_storage.sqlite"); + return db; +} + +/** + * Obtains a corrupt database to test against. + */ +function getCorruptDB() +{ + return do_get_file("corruptDB.sqlite"); +} + +/** + * Obtains a fake (non-SQLite format) database to test against. + */ +function getFakeDB() +{ + return do_get_file("fakeDB.sqlite"); +} + +/** + * Delete the test database file. + */ +function deleteTestDB() +{ + print("*** Storage Tests: Trying to remove file!"); + var dbFile = getTestDB(); + if (dbFile.exists()) + try { dbFile.remove(false); } catch (e) { /* stupid windows box */ } +} + +function cleanup() +{ + // close the connection + print("*** Storage Tests: Trying to close!"); + getOpenedDatabase().close(); + + // we need to null out the database variable to get a new connection the next + // time getOpenedDatabase is called + gDBConn = null; + + // removing test db + deleteTestDB(); +} + +/** + * Use asyncClose to cleanup a connection. Synchronous by means of internally + * spinning an event loop. + */ +function asyncCleanup() +{ + let closed = false; + + // close the connection + print("*** Storage Tests: Trying to asyncClose!"); + getOpenedDatabase().asyncClose(function () { closed = true; }); + + let curThread = Components.classes["@mozilla.org/thread-manager;1"] + .getService().currentThread; + while (!closed) + curThread.processNextEvent(true); + + // we need to null out the database variable to get a new connection the next + // time getOpenedDatabase is called + gDBConn = null; + + // removing test db + deleteTestDB(); +} + +function getService() +{ + return Cc["@mozilla.org/storage/service;1"].getService(Ci.mozIStorageService); +} + +/** + * Get a connection to the test database. Creates and caches the connection + * if necessary, otherwise reuses the existing cached connection. This + * connection shares its cache. + * + * @returns the mozIStorageConnection for the file. + */ +function getOpenedDatabase() +{ + if (!gDBConn) { + gDBConn = getService().openDatabase(getTestDB()); + } + return gDBConn; +} + +/** + * Get a connection to the test database. Creates and caches the connection + * if necessary, otherwise reuses the existing cached connection. This + * connection doesn't share its cache. + * + * @returns the mozIStorageConnection for the file. + */ +function getOpenedUnsharedDatabase() +{ + if (!gDBConn) { + gDBConn = getService().openUnsharedDatabase(getTestDB()); + } + return gDBConn; +} + +/** + * Obtains a specific database to use. + * + * @param aFile + * The nsIFile representing the db file to open. + * @returns the mozIStorageConnection for the file. + */ +function getDatabase(aFile) +{ + return getService().openDatabase(aFile); +} + +function createStatement(aSQL) +{ + return getOpenedDatabase().createStatement(aSQL); +} + +/** + * Creates an asynchronous SQL statement. + * + * @param aSQL + * The SQL to parse into a statement. + * @returns a mozIStorageAsyncStatement from aSQL. + */ +function createAsyncStatement(aSQL) +{ + return getOpenedDatabase().createAsyncStatement(aSQL); +} + +/** + * Invoke the given function and assert that it throws an exception expressing + * the provided error code in its 'result' attribute. JS function expressions + * can be used to do this concisely. + * + * Example: + * expectError(Cr.NS_ERROR_INVALID_ARG, () => explodingFunction()); + * + * @param aErrorCode + * The error code to expect from invocation of aFunction. + * @param aFunction + * The function to invoke and expect an XPCOM-style error from. + */ +function expectError(aErrorCode, aFunction) +{ + let exceptionCaught = false; + try { + aFunction(); + } + catch (e) { + if (e.result != aErrorCode) { + do_throw("Got an exception, but the result code was not the expected " + + "one. Expected " + aErrorCode + ", got " + e.result); + } + exceptionCaught = true; + } + if (!exceptionCaught) + do_throw(aFunction + " should have thrown an exception but did not!"); +} + +/** + * Run a query synchronously and verify that we get back the expected results. + * + * @param aSQLString + * The SQL string for the query. + * @param aBind + * The value to bind at index 0. + * @param aResults + * A list of the expected values returned in the sole result row. + * Express blobs as lists. + */ +function verifyQuery(aSQLString, aBind, aResults) +{ + let stmt = getOpenedDatabase().createStatement(aSQLString); + stmt.bindByIndex(0, aBind); + try { + do_check_true(stmt.executeStep()); + let nCols = stmt.numEntries; + if (aResults.length != nCols) + do_throw("Expected " + aResults.length + " columns in result but " + + "there are only " + aResults.length + "!"); + for (let iCol = 0; iCol < nCols; iCol++) { + let expectedVal = aResults[iCol]; + let valType = stmt.getTypeOfIndex(iCol); + if (expectedVal === null) { + do_check_eq(stmt.VALUE_TYPE_NULL, valType); + do_check_true(stmt.getIsNull(iCol)); + } + else if (typeof expectedVal == "number") { + if (Math.floor(expectedVal) == expectedVal) { + do_check_eq(stmt.VALUE_TYPE_INTEGER, valType); + do_check_eq(expectedVal, stmt.getInt32(iCol)); + } + else { + do_check_eq(stmt.VALUE_TYPE_FLOAT, valType); + do_check_eq(expectedVal, stmt.getDouble(iCol)); + } + } + else if (typeof expectedVal == "string") { + do_check_eq(stmt.VALUE_TYPE_TEXT, valType); + do_check_eq(expectedVal, stmt.getUTF8String(iCol)); + } + else { // blob + do_check_eq(stmt.VALUE_TYPE_BLOB, valType); + let count = { value: 0 }, blob = { value: null }; + stmt.getBlob(iCol, count, blob); + do_check_eq(count.value, expectedVal.length); + for (let i = 0; i < count.value; i++) { + do_check_eq(expectedVal[i], blob.value[i]); + } + } + } + } + finally { + stmt.finalize(); + } +} + +/** + * Return the number of rows in the able with the given name using a synchronous + * query. + * + * @param aTableName + * The name of the table. + * @return The number of rows. + */ +function getTableRowCount(aTableName) +{ + var currentRows = 0; + var countStmt = getOpenedDatabase().createStatement( + "SELECT COUNT(1) AS count FROM " + aTableName + ); + try { + do_check_true(countStmt.executeStep()); + currentRows = countStmt.row.count; + } + finally { + countStmt.finalize(); + } + return currentRows; +} + +// Promise-Returning Functions + +function asyncClone(db, readOnly) { + let deferred = Promise.defer(); + db.asyncClone(readOnly, function (status, db2) { + if (Components.isSuccessCode(status)) { + deferred.resolve(db2); + } else { + deferred.reject(status); + } + }); + return deferred.promise; +} + +function asyncClose(db) { + let deferred = Promise.defer(); + db.asyncClose(function (status) { + if (Components.isSuccessCode(status)) { + deferred.resolve(); + } else { + deferred.reject(status); + } + }); + return deferred.promise; +} + +function openAsyncDatabase(file, options) { + let deferred = Promise.defer(); + let properties; + if (options) { + properties = Cc["@mozilla.org/hash-property-bag;1"]. + createInstance(Ci.nsIWritablePropertyBag); + for (let k in options) { + properties.setProperty(k, options[k]); + } + } + getService().openAsyncDatabase(file, properties, function (status, db) { + if (Components.isSuccessCode(status)) { + deferred.resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection)); + } else { + deferred.reject(status); + } + }); + return deferred.promise; +} + +function executeAsync(statement, onResult) { + let deferred = Promise.defer(); + statement.executeAsync({ + handleError: function (error) { + deferred.reject(error); + }, + handleResult: function (result) { + if (onResult) { + onResult(result); + } + }, + handleCompletion: function (result) { + deferred.resolve(result); + } + }); + return deferred.promise; +} + +function executeMultipleStatementsAsync(db, statements, onResult) { + let deferred = Promise.defer(); + db.executeAsync(statements, statements.length, { + handleError: function (error) { + deferred.reject(error); + }, + handleResult: function (result) { + if (onResult) { + onResult(result); + } + }, + handleCompletion: function (result) { + deferred.resolve(result); + } + }); + return deferred.promise; +} + +function executeSimpleSQLAsync(db, query, onResult) { + let deferred = Promise.defer(); + db.executeSimpleSQLAsync(query, { + handleError(error) { + deferred.reject(error); + }, + handleResult(result) { + if (onResult) { + onResult(result); + } else { + do_throw("No results were expected"); + } + }, + handleCompletion(result) { + deferred.resolve(result); + } + }); + return deferred.promise; +} + +cleanup(); diff --git a/storage/test/unit/locale_collation.txt b/storage/test/unit/locale_collation.txt new file mode 100644 index 000000000..86f50579b --- /dev/null +++ b/storage/test/unit/locale_collation.txt @@ -0,0 +1,174 @@ + +! +" +# +$ +% +& +' +( +) +* ++ +, +- +. +/ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +: +; +< += +> +? +@ +A +B +C +D +E +F +G +H +I +J +K +L +M +N +O +P +Q +R +S +T +U +V +W +X +Y +Z +[ +\ +] +^ +_ +` +a +b +c +d +e +f +g +h +i +j +k +l +m +n +o +p +q +r +s +t +u +v +w +x +y +z +{ +| +} +~ +ludwig van beethoven +Ludwig van Beethoven +Ludwig van beethoven +Jane +jane +JANE +jAne +jaNe +janE +JAne +JaNe +JanE +JANe +JaNE +JAnE +jANE +Umberto Eco +Umberto eco +umberto eco +umberto Eco +UMBERTO ECO +ace +bash +*ace +!ace +%ace +~ace +#ace +cork +denizen +[denizen] +(denizen) +{denizen} +/denizen/ +#denizen# +$denizen$ +@denizen +elf +full +gnome +gnomic investigation of typological factors in the grammaticalization process of Malayo-Polynesian substaratum in the protoAltaic vocabulary core in the proto-layers of pre-historic Japanese +gnomic investigation of typological factors in the grammaticalization process of Malayo-Polynesian substaratum in the protoAltaic vocabulary core in the proto-layers of pre-historic Javanese +hint +Internationalization +Zinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalization +Zinternationalization internationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizatio +n +Zinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalization internationalizationinternationalizationinternationalizationinternationalizationinternationalization +ZinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizaTioninternationalizationinternationalizationinternationalizationinternationalization +ZinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizaTion +jostle +kin +Laymen +lumens +Lumens +motleycrew +motley crew +niven's creative talents +nivens creative talents +opie +posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the Rockies +posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the Rokkies +posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the rockies +quilt's +quilts +quilt +Rondo +street +tamale oxidization and iodization in rapid progress +tamale oxidization and iodization in rapid Progress +until +vera +Wobble +Xanadu's legenary imaginary floccinaucinihilipilification in localization of theoretical portions of glottochronological understanding of the phoneme +Xanadu's legenary imaginary floccinaucinihilipilification in localization of theoretical portions of glottochronological understanding of the phoname +yearn +zodiac +a +å diff --git a/storage/test/unit/test_bug-365166.js b/storage/test/unit/test_bug-365166.js new file mode 100644 index 000000000..ebcdc6900 --- /dev/null +++ b/storage/test/unit/test_bug-365166.js @@ -0,0 +1,26 @@ +// Testcase for bug 365166 - crash [@ strlen] calling +// mozIStorageStatement::getColumnName of a statement created with +// "PRAGMA user_version" or "PRAGMA schema_version" +function run_test() { + test('user'); + test('schema'); + + function test(param) + { + var colName = param + "_version"; + var sql = "PRAGMA " + colName; + + var file = getTestDB(); + var storageService = Components.classes["@mozilla.org/storage/service;1"]. + getService(Components.interfaces.mozIStorageService); + var conn = storageService.openDatabase(file); + var statement = conn.createStatement(sql); + try { + // This shouldn't crash: + do_check_eq(statement.getColumnName(0), colName); + } finally { + statement.reset(); + statement.finalize(); + } + } +} diff --git a/storage/test/unit/test_bug-393952.js b/storage/test/unit/test_bug-393952.js new file mode 100644 index 000000000..f3c61f62b --- /dev/null +++ b/storage/test/unit/test_bug-393952.js @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Testcase for bug 393952: crash when I try to VACUUM and one of the tables +// has a UNIQUE text column. StorageUnicodeFunctions::likeFunction() +// needs to handle null aArgv[0] and aArgv[1] + +function setup() +{ + getOpenedDatabase().createTable("t1", "x TEXT UNIQUE"); + + var stmt = createStatement("INSERT INTO t1 (x) VALUES ('a')"); + stmt.execute(); + stmt.reset(); + stmt.finalize(); +} + +function test_vacuum() +{ + var stmt = createStatement("VACUUM;"); + stmt.executeStep(); + stmt.reset(); + stmt.finalize(); +} + +var tests = [test_vacuum]; + +function run_test() +{ + setup(); + + for (var i = 0; i < tests.length; i++) + tests[i](); + + cleanup(); +} + diff --git a/storage/test/unit/test_bug-429521.js b/storage/test/unit/test_bug-429521.js new file mode 100644 index 000000000..a9eafc1c2 --- /dev/null +++ b/storage/test/unit/test_bug-429521.js @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function setup() { + getOpenedDatabase().createTable("t1", "x TEXT"); + + var stmt = createStatement("INSERT INTO t1 (x) VALUES ('/mozilla.org/20070129_1/Europe/Berlin')"); + stmt.execute(); + stmt.finalize(); +} + +function test_bug429521() { + var stmt = createStatement( + "SELECT DISTINCT(zone) FROM (" + + "SELECT x AS zone FROM t1 WHERE x LIKE '/mozilla.org%'" + + ");"); + + print("*** test_bug429521: started"); + + try { + while (stmt.executeStep()) { + print("*** test_bug429521: step() Read wrapper.row.zone"); + + // BUG: the print commands after the following statement + // are never executed. Script stops immediately. + var tzId = stmt.row.zone; + + print("*** test_bug429521: step() Read wrapper.row.zone finished"); + } + } catch (e) { + print("*** test_bug429521: " + e); + } + + print("*** test_bug429521: finished"); + + stmt.finalize(); +} + +function run_test() { + setup(); + + test_bug429521(); + + cleanup(); +} diff --git a/storage/test/unit/test_bug-444233.js b/storage/test/unit/test_bug-444233.js new file mode 100644 index 000000000..eb315f934 --- /dev/null +++ b/storage/test/unit/test_bug-444233.js @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function setup() { + // Create the table + getOpenedDatabase().createTable("test_bug444233", + "id INTEGER PRIMARY KEY, value TEXT"); + + // Insert dummy data, using wrapper methods + var stmt = createStatement("INSERT INTO test_bug444233 (value) VALUES (:value)"); + stmt.params.value = "value1"; + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO test_bug444233 (value) VALUES (:value)"); + stmt.params.value = "value2"; + stmt.execute(); + stmt.finalize(); +} + +function test_bug444233() { + print("*** test_bug444233: started"); + + // Check that there are 2 results + var stmt = createStatement("SELECT COUNT(*) AS number FROM test_bug444233"); + do_check_true(stmt.executeStep()); + do_check_eq(2, stmt.row.number); + stmt.reset(); + stmt.finalize(); + + print("*** test_bug444233: doing delete"); + + // Now try to delete using IN + // Cheating since we know ids are 1,2 + try { + var ids = [1, 2]; + stmt = createStatement("DELETE FROM test_bug444233 WHERE id IN (:ids)"); + stmt.params.ids = ids; + } catch (e) { + print("*** test_bug444233: successfully caught exception"); + } + stmt.finalize(); +} + +function run_test() { + setup(); + test_bug444233(); + cleanup(); +} + diff --git a/storage/test/unit/test_cache_size.js b/storage/test/unit/test_cache_size.js new file mode 100644 index 000000000..e0a5e8723 --- /dev/null +++ b/storage/test/unit/test_cache_size.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This file tests that dbs of various page sizes are using the right cache +// size (bug 703113). + +/** + * In order to change the cache size, we must open a DB, change the page + * size, create a table, close the DB, then re-open the DB. We then check + * the cache size after reopening. + * + * @param dbOpener + * function that opens the DB specified in file + * @param file + * file holding the database + * @param pageSize + * the DB's page size + * @param expectedCacheSize + * the expected cache size for the given page size + */ +function check_size(dbOpener, file, pageSize, expectedCacheSize) +{ + // Open the DB, immediately change its page size. + let db = dbOpener(file); + db.executeSimpleSQL("PRAGMA page_size = " + pageSize); + + // Check the page size change worked. + let stmt = db.createStatement("PRAGMA page_size"); + do_check_true(stmt.executeStep()); + do_check_eq(stmt.row.page_size, pageSize); + stmt.finalize(); + + // Create a simple table. + db.executeSimpleSQL("CREATE TABLE test ( id INTEGER PRIMARY KEY )"); + + // Close and re-open the DB. + db.close(); + db = dbOpener(file); + + // Check cache size is as expected. + stmt = db.createStatement("PRAGMA cache_size"); + do_check_true(stmt.executeStep()); + do_check_eq(stmt.row.cache_size, expectedCacheSize); + stmt.finalize(); +} + +function new_file(name) +{ + let file = dirSvc.get("ProfD", Ci.nsIFile); + file.append(name + ".sqlite"); + do_check_false(file.exists()); + return file; +} + +function run_test() +{ + const kExpectedCacheSize = -2048; // 2MiB + + let pageSizes = [ + 1024, + 4096, + 32768, + ]; + + for (let i = 0; i < pageSizes.length; i++) { + let pageSize = pageSizes[i]; + check_size(getDatabase, + new_file("shared" + pageSize), pageSize, kExpectedCacheSize); + check_size(getService().openUnsharedDatabase, + new_file("unshared" + pageSize), pageSize, kExpectedCacheSize); + } +} diff --git a/storage/test/unit/test_chunk_growth.js b/storage/test/unit/test_chunk_growth.js new file mode 100644 index 000000000..2cf91fe16 --- /dev/null +++ b/storage/test/unit/test_chunk_growth.js @@ -0,0 +1,52 @@ +// This file tests SQLITE_FCNTL_CHUNK_SIZE behaves as expected + +function run_sql(d, sql) { + var stmt = d.createStatement(sql); + stmt.execute(); + stmt.finalize(); +} + +function new_file(name) { + var file = dirSvc.get("ProfD", Ci.nsIFile); + file.append(name); + return file; +} + +function get_size(name) { + return new_file(name).fileSize; +} + +function run_test() { + const filename = "chunked.sqlite"; + const CHUNK_SIZE = 512 * 1024; + var d = getDatabase(new_file(filename)); + try { + d.setGrowthIncrement(CHUNK_SIZE, ""); + } catch (e) { + if (e.result != Cr.NS_ERROR_FILE_TOO_BIG) { + throw e; + } + print("Too little free space to set CHUNK_SIZE!"); + return; + } + run_sql(d, "CREATE TABLE bloat(data varchar)"); + + var orig_size = get_size(filename); + /* Dump in at least 32K worth of data. + * While writing ensure that the file size growth in chunksize set above. + */ + const str1024 = new Array(1024).join("T"); + for (var i = 0; i < 32; i++) { + run_sql(d, "INSERT INTO bloat VALUES('" + str1024 + "')"); + var size = get_size(filename); + // Must not grow in small increments. + do_check_true(size == orig_size || size >= CHUNK_SIZE); + } + /* In addition to growing in chunk-size increments, the db + * should shrink in chunk-size increments too. + */ + run_sql(d, "DELETE FROM bloat"); + run_sql(d, "VACUUM"); + do_check_true(get_size(filename) >= CHUNK_SIZE); +} + diff --git a/storage/test/unit/test_connection_asyncClose.js b/storage/test/unit/test_connection_asyncClose.js new file mode 100644 index 000000000..7704dcc81 --- /dev/null +++ b/storage/test/unit/test_connection_asyncClose.js @@ -0,0 +1,125 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Thorough branch coverage for asyncClose. + * + * Coverage of asyncClose by connection state at time of AsyncClose invocation: + * - (asyncThread && mDBConn) => AsyncCloseConnection used, actually closes + * - test_asyncClose_does_not_complete_before_statements + * - test_double_asyncClose_throws + * - test_asyncClose_does_not_throw_without_callback + * - (asyncThread && !mDBConn) => AsyncCloseConnection used, although no close + * is required. Note that this is only possible in the event that + * openAsyncDatabase was used and we failed to open the database. + * Additionally, the async connection will never be exposed to the caller and + * AsyncInitDatabase will be the one to (automatically) call AsyncClose. + * - test_asyncClose_failed_open + * - (!asyncThread && mDBConn) => Close() invoked, actually closes + * - test_asyncClose_on_sync_db + * - (!asyncThread && !mDBConn) => Close() invoked, no close needed, errors. + * This happens if the database has already been closed. + * - test_double_asyncClose_throws + */ + + +/** + * Sanity check that our close indeed happens after asynchronously executed + * statements scheduled during the same turn of the event loop. Note that we + * just care that the statement says it completed without error, we're not + * worried that the close will happen and then the statement will magically + * complete. + */ +add_task(function* test_asyncClose_does_not_complete_before_statements() { + let db = getService().openDatabase(getTestDB()); + let stmt = db.createStatement("SELECT * FROM sqlite_master"); + // Issue the executeAsync but don't yield for it... + let asyncStatementPromise = executeAsync(stmt); + stmt.finalize(); + + // Issue the close. (And now the order of yielding doesn't matter.) + // Branch coverage: (asyncThread && mDBConn) + yield asyncClose(db); + equal((yield asyncStatementPromise), + Ci.mozIStorageStatementCallback.REASON_FINISHED); +}); + +/** + * Open an async database (ensures the async thread is created) and then invoke + * AsyncClose() twice without yielding control flow. The first will initiate + * the actual async close after calling setClosedState which synchronously + * impacts what the second call will observe. The second call will then see the + * async thread is not available and fall back to invoking Close() which will + * notice the mDBConn is already gone. + */ +add_task(function* test_double_asyncClose_throws() { + let db = yield openAsyncDatabase(getTestDB()); + + // (Don't yield control flow yet, save the promise for after we make the + // second call.) + // Branch coverage: (asyncThread && mDBConn) + let realClosePromise = yield asyncClose(db); + try { + // Branch coverage: (!asyncThread && !mDBConn) + db.asyncClose(); + ok(false, "should have thrown"); + } catch (e) { + equal(e.result, Cr.NS_ERROR_NOT_INITIALIZED); + } + + yield realClosePromise; +}); + +/** + * Create a sync db connection and never take it asynchronous and then call + * asyncClose on it. This will bring the async thread to life to perform the + * shutdown to avoid blocking the main thread, although we won't be able to + * tell the difference between this happening and the method secretly shunting + * to close(). + */ +add_task(function* test_asyncClose_on_sync_db() { + let db = getService().openDatabase(getTestDB()); + + // Branch coverage: (!asyncThread && mDBConn) + yield asyncClose(db); + ok(true, 'closed sync connection asynchronously'); +}); + +/** + * Fail to asynchronously open a DB in order to get an async thread existing + * without having an open database and asyncClose invoked. As per the file + * doc-block, note that asyncClose will automatically be invoked by the + * AsyncInitDatabase when it fails to open the database. We will never be + * provided with a reference to the connection and so cannot call AsyncClose on + * it ourselves. + */ +add_task(function* test_asyncClose_failed_open() { + // This will fail and the promise will be rejected. + let openPromise = openAsyncDatabase(getFakeDB()); + yield openPromise.then( + () => { + ok(false, 'we should have failed to open the db; this test is broken!'); + }, + () => { + ok(true, 'correctly failed to open db; bg asyncClose should happen'); + } + ); + // (NB: we are unable to observe the thread shutdown, but since we never open + // a database, this test is not going to interfere with other tests so much.) +}); + +// THE TEST BELOW WANTS TO BE THE LAST TEST WE RUN. DO NOT MAKE IT SAD. +/** + * Verify that asyncClose without a callback does not explode. Without a + * callback the shutdown is not actually observable, so we run this test last + * in order to avoid weird overlaps. + */ +add_task(function* test_asyncClose_does_not_throw_without_callback() { + let db = yield openAsyncDatabase(getTestDB()); + // Branch coverage: (asyncThread && mDBConn) + db.asyncClose(); + ok(true, 'if we shutdown cleanly and do not crash, then we succeeded'); +}); +// OBEY SHOUTING UPPER-CASE COMMENTS. +// ADD TESTS ABOVE THE FORMER TEST, NOT BELOW IT. diff --git a/storage/test/unit/test_connection_executeAsync.js b/storage/test/unit/test_connection_executeAsync.js new file mode 100644 index 000000000..e56d98e55 --- /dev/null +++ b/storage/test/unit/test_connection_executeAsync.js @@ -0,0 +1,171 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This file tests the functionality of mozIStorageConnection::executeAsync for + * both mozIStorageStatement and mozIStorageAsyncStatement. + * + * A single database connection is used for the entirety of the test, which is + * a legacy thing, but we otherwise use the modern promise-based driver and + * async helpers. + */ + +const INTEGER = 1; +const TEXT = "this is test text"; +const REAL = 3.23; +const BLOB = [1, 2]; + +add_task(function* test_first_create_and_add() { + // synchronously open the database and let gDBConn hold onto it because we + // use this database + let db = getOpenedDatabase(); + // synchronously set up our table *that will be used for the rest of the file* + db.executeSimpleSQL( + "CREATE TABLE test (" + + "id INTEGER, " + + "string TEXT, " + + "number REAL, " + + "nuller NULL, " + + "blober BLOB" + + ")" + ); + + let stmts = []; + stmts[0] = db.createStatement( + "INSERT INTO test (id, string, number, nuller, blober) VALUES (?, ?, ?, ?, ?)" + ); + stmts[0].bindByIndex(0, INTEGER); + stmts[0].bindByIndex(1, TEXT); + stmts[0].bindByIndex(2, REAL); + stmts[0].bindByIndex(3, null); + stmts[0].bindBlobByIndex(4, BLOB, BLOB.length); + stmts[1] = getOpenedDatabase().createAsyncStatement( + "INSERT INTO test (string, number, nuller, blober) VALUES (?, ?, ?, ?)" + ); + stmts[1].bindByIndex(0, TEXT); + stmts[1].bindByIndex(1, REAL); + stmts[1].bindByIndex(2, null); + stmts[1].bindBlobByIndex(3, BLOB, BLOB.length); + + // asynchronously execute the statements + let execResult = yield executeMultipleStatementsAsync( + db, + stmts, + function(aResultSet) { + ok(false, 'we only did inserts so we should not have gotten results!'); + }); + equal(Ci.mozIStorageStatementCallback.REASON_FINISHED, execResult, + 'execution should have finished successfully.'); + + // Check that the result is in the table + let stmt = db.createStatement( + "SELECT string, number, nuller, blober FROM test WHERE id = ?" + ); + stmt.bindByIndex(0, INTEGER); + try { + do_check_true(stmt.executeStep()); + do_check_eq(TEXT, stmt.getString(0)); + do_check_eq(REAL, stmt.getDouble(1)); + do_check_true(stmt.getIsNull(2)); + let count = { value: 0 }; + let blob = { value: null }; + stmt.getBlob(3, count, blob); + do_check_eq(BLOB.length, count.value); + for (let i = 0; i < BLOB.length; i++) + do_check_eq(BLOB[i], blob.value[i]); + } + finally { + stmt.finalize(); + } + + // Make sure we have two rows in the table + stmt = db.createStatement( + "SELECT COUNT(1) FROM test" + ); + try { + do_check_true(stmt.executeStep()); + do_check_eq(2, stmt.getInt32(0)); + } + finally { + stmt.finalize(); + } + + stmts[0].finalize(); + stmts[1].finalize(); +}); + +add_task(function* test_last_multiple_bindings_on_statements() { + // This tests to make sure that we pass all the statements multiply bound + // parameters when we call executeAsync. + const AMOUNT_TO_ADD = 5; + const ITERATIONS = 5; + + let stmts = []; + let db = getOpenedDatabase(); + let sqlString = "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (:int, :text, :real, :null, :blob)"; + // We run the same statement twice, and should insert 2 * AMOUNT_TO_ADD. + for (let i = 0; i < ITERATIONS; i++) { + // alternate the type of statement we create + if (i % 2) + stmts[i] = db.createStatement(sqlString); + else + stmts[i] = db.createAsyncStatement(sqlString); + + let params = stmts[i].newBindingParamsArray(); + for (let j = 0; j < AMOUNT_TO_ADD; j++) { + let bp = params.newBindingParams(); + bp.bindByName("int", INTEGER); + bp.bindByName("text", TEXT); + bp.bindByName("real", REAL); + bp.bindByName("null", null); + bp.bindBlobByName("blob", BLOB, BLOB.length); + params.addParams(bp); + } + stmts[i].bindParameters(params); + } + + // Get our current number of rows in the table. + let currentRows = 0; + let countStmt = getOpenedDatabase().createStatement( + "SELECT COUNT(1) AS count FROM test" + ); + try { + do_check_true(countStmt.executeStep()); + currentRows = countStmt.row.count; + } + finally { + countStmt.reset(); + } + + // Execute asynchronously. + let execResult = yield executeMultipleStatementsAsync( + db, + stmts, + function(aResultSet) { + ok(false, 'we only did inserts so we should not have gotten results!'); + }); + equal(Ci.mozIStorageStatementCallback.REASON_FINISHED, execResult, + 'execution should have finished successfully.'); + + // Check to make sure we added all of our rows. + try { + do_check_true(countStmt.executeStep()); + do_check_eq(currentRows + (ITERATIONS * AMOUNT_TO_ADD), + countStmt.row.count); + } + finally { + countStmt.finalize(); + } + + stmts.forEach(stmt => stmt.finalize()); + + // we are the last test using this connection and since it has gone async + // we *must* call asyncClose on it. + yield asyncClose(db); + gDBConn = null; +}); + +// If you add a test down here you will need to move the asyncClose or clean +// things up a little more. diff --git a/storage/test/unit/test_connection_executeSimpleSQLAsync.js b/storage/test/unit/test_connection_executeSimpleSQLAsync.js new file mode 100644 index 000000000..142cc8e14 --- /dev/null +++ b/storage/test/unit/test_connection_executeSimpleSQLAsync.js @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This file tests the functionality of + * mozIStorageAsyncConnection::executeSimpleSQLAsync. + */ + +const INTEGER = 1; +const TEXT = "this is test text"; +const REAL = 3.23; + +add_task(function* test_create_and_add() { + let adb = yield openAsyncDatabase(getTestDB()); + + let completion = yield executeSimpleSQLAsync(adb, + "CREATE TABLE test (id INTEGER, string TEXT, number REAL)"); + + do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion); + + completion = yield executeSimpleSQLAsync(adb, + "INSERT INTO test (id, string, number) " + + "VALUES (" + INTEGER + ", \"" + TEXT + "\", " + REAL + ")"); + + do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion); + + let result = null; + + completion = yield executeSimpleSQLAsync(adb, + "SELECT string, number FROM test WHERE id = 1", + function (aResultSet) { + result = aResultSet.getNextRow(); + do_check_eq(2, result.numEntries); + do_check_eq(TEXT, result.getString(0)); + do_check_eq(REAL, result.getDouble(1)); + } + ); + + do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion); + do_check_neq(result, null); + result = null; + + yield executeSimpleSQLAsync(adb, "SELECT COUNT(0) FROM test", + function (aResultSet) { + result = aResultSet.getNextRow(); + do_check_eq(1, result.getInt32(0)); + }); + + do_check_neq(result, null); + + yield asyncClose(adb); +}); + + +add_task(function* test_asyncClose_does_not_complete_before_statement() { + let adb = yield openAsyncDatabase(getTestDB()); + let executed = false; + + let reason = yield executeSimpleSQLAsync(adb, "SELECT * FROM test", + function (aResultSet) { + let result = aResultSet.getNextRow(); + + do_check_neq(result, null); + do_check_eq(3, result.numEntries); + do_check_eq(INTEGER, result.getInt32(0)); + do_check_eq(TEXT, result.getString(1)); + do_check_eq(REAL, result.getDouble(2)); + executed = true; + } + ); + + do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, reason); + + // Ensure that the statement executed to completion. + do_check_true(executed); + + yield asyncClose(adb); +}); diff --git a/storage/test/unit/test_js_helpers.js b/storage/test/unit/test_js_helpers.js new file mode 100644 index 000000000..dde9fac20 --- /dev/null +++ b/storage/test/unit/test_js_helpers.js @@ -0,0 +1,125 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sw=2 ts=2 sts=2 et : */ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests that the JS language helpers in various ways. + */ + +// Test Functions + +function test_params_enumerate() +{ + let stmt = createStatement( + "SELECT * FROM test WHERE id IN (:a, :b, :c)" + ); + + // Make sure they are right. + let expected = ["a", "b", "c"]; + let index = 0; + for (let name in stmt.params) { + if (name == "QueryInterface") + continue; + do_check_eq(name, expected[index++]); + } +} + +function test_params_prototype() +{ + let stmt = createStatement( + "SELECT * FROM sqlite_master" + ); + + // Set a property on the prototype and make sure it exist (will not be a + // bindable parameter, however). + Object.getPrototypeOf(stmt.params).test = 2; + do_check_eq(stmt.params.test, 2); + stmt.finalize(); +} + +function test_row_prototype() +{ + let stmt = createStatement( + "SELECT * FROM sqlite_master" + ); + + do_check_true(stmt.executeStep()); + + // Set a property on the prototype and make sure it exists (will not be in the + // results, however). + Object.getPrototypeOf(stmt.row).test = 2; + do_check_eq(stmt.row.test, 2); + + // Clean up after ourselves. + delete Object.getPrototypeOf(stmt.row).test; + stmt.finalize(); +} + +function test_params_gets_sync() +{ + // Added for bug 562866. + /* + let stmt = createStatement( + "SELECT * FROM test WHERE id IN (:a, :b, :c)" + ); + + // Make sure we do not assert in getting the value. + let originalCount = Object.getOwnPropertyNames(stmt.params).length; + let expected = ["a", "b", "c"]; + for (let name of expected) { + stmt.params[name]; + } + + // Now make sure we didn't magically get any additional properties. + let finalCount = Object.getOwnPropertyNames(stmt.params).length; + do_check_eq(originalCount + expected.length, finalCount); + */ +} + +function test_params_gets_async() +{ + // Added for bug 562866. + /* + let stmt = createAsyncStatement( + "SELECT * FROM test WHERE id IN (:a, :b, :c)" + ); + + // Make sure we do not assert in getting the value. + let originalCount = Object.getOwnPropertyNames(stmt.params).length; + let expected = ["a", "b", "c"]; + for (let name of expected) { + stmt.params[name]; + } + + // Now make sure we didn't magically get any additional properties. + let finalCount = Object.getOwnPropertyNames(stmt.params).length; + do_check_eq(originalCount + expected.length, finalCount); + */ +} + +// Test Runner + +var tests = [ + test_params_enumerate, + test_params_prototype, + test_row_prototype, + test_params_gets_sync, + test_params_gets_async, +]; +function run_test() +{ + cleanup(); + + // Create our database. + getOpenedDatabase().executeSimpleSQL( + "CREATE TABLE test (" + + "id INTEGER PRIMARY KEY " + + ")" + ); + + // Run the tests. + tests.forEach(test => test()); +} diff --git a/storage/test/unit/test_levenshtein.js b/storage/test/unit/test_levenshtein.js new file mode 100644 index 000000000..ced141abd --- /dev/null +++ b/storage/test/unit/test_levenshtein.js @@ -0,0 +1,74 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the Levenshtein Distance function we've registered. + +function createUtf16Database() +{ + print("Creating the in-memory UTF-16-encoded database."); + let conn = getService().openSpecialDatabase("memory"); + conn.executeSimpleSQL("PRAGMA encoding = 'UTF-16'"); + + print("Make sure the encoding was set correctly and is now UTF-16."); + let stmt = conn.createStatement("PRAGMA encoding"); + do_check_true(stmt.executeStep()); + let enc = stmt.getString(0); + stmt.finalize(); + + // The value returned will actually be UTF-16le or UTF-16be. + do_check_true(enc === "UTF-16le" || enc === "UTF-16be"); + + return conn; +} + +function check_levenshtein(db, s, t, expectedDistance) +{ + var stmt = db.createStatement("SELECT levenshteinDistance(:s, :t) AS result"); + stmt.params.s = s; + stmt.params.t = t; + try { + do_check_true(stmt.executeStep()); + do_check_eq(expectedDistance, stmt.row.result); + } finally { + stmt.reset(); + stmt.finalize(); + } +} + +function testLevenshtein(db) +{ + // Basic tests. + check_levenshtein(db, "", "", 0); + check_levenshtein(db, "foo", "", 3); + check_levenshtein(db, "", "bar", 3); + check_levenshtein(db, "yellow", "hello", 2); + check_levenshtein(db, "gumbo", "gambol", 2); + check_levenshtein(db, "kitten", "sitten", 1); + check_levenshtein(db, "sitten", "sittin", 1); + check_levenshtein(db, "sittin", "sitting", 1); + check_levenshtein(db, "kitten", "sitting", 3); + check_levenshtein(db, "Saturday", "Sunday", 3); + check_levenshtein(db, "YHCQPGK", "LAHYQQKPGKA", 6); + + // Test SQL NULL handling. + check_levenshtein(db, "foo", null, null); + check_levenshtein(db, null, "bar", null); + check_levenshtein(db, null, null, null); + + // The levenshteinDistance function allocates temporary memory on the stack + // if it can. Test some strings long enough to force a heap allocation. + var dots1000 = Array(1001).join("."); + var dashes1000 = Array(1001).join("-"); + check_levenshtein(db, dots1000, dashes1000, 1000); +} + +function run_test() +{ + testLevenshtein(getOpenedDatabase()); + testLevenshtein(createUtf16Database()); +} + + + + diff --git a/storage/test/unit/test_like.js b/storage/test/unit/test_like.js new file mode 100644 index 000000000..b42f2eb27 --- /dev/null +++ b/storage/test/unit/test_like.js @@ -0,0 +1,202 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests our LIKE implementation since we override it for unicode + +function setup() +{ + getOpenedDatabase().createTable("t1", "x TEXT"); + + var stmt = createStatement("INSERT INTO t1 (x) VALUES ('a')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('ab')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('abc')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('abcd')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('acd')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('abd')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('bc')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('bcd')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('xyz')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('ABC')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('CDE')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('ABC abc xyz')"); + stmt.execute(); + stmt.finalize(); +} + +function test_count() +{ + var stmt = createStatement("SELECT count(*) FROM t1;"); + do_check_true(stmt.executeStep()); + do_check_eq(stmt.getInt32(0), 12); + stmt.reset(); + stmt.finalize(); +} + +function test_like_1() +{ + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, 'abc'); + var solutions = ["abc", "ABC"]; + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_false(stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_2() +{ + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, 'ABC'); + var solutions = ["abc", "ABC"]; + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_false(stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_3() +{ + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, 'aBc'); + var solutions = ["abc", "ABC"]; + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_false(stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_4() +{ + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, 'abc%'); + var solutions = ["abc", "abcd", "ABC", "ABC abc xyz"]; + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_false(stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_5() +{ + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, 'a_c'); + var solutions = ["abc", "ABC"]; + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_false(stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_6() +{ + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, 'ab%d'); + var solutions = ["abcd", "abd"]; + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_false(stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_7() +{ + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, 'a_c%'); + var solutions = ["abc", "abcd", "ABC", "ABC abc xyz"]; + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_false(stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_8() +{ + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, '%bcd'); + var solutions = ["abcd", "bcd"]; + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_true(stmt.executeStep()); + do_check_true(solutions.indexOf(stmt.getString(0)) != -1); + do_check_false(stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +var tests = [test_count, test_like_1, test_like_2, test_like_3, test_like_4, + test_like_5, test_like_6, test_like_7, test_like_8]; + +function run_test() +{ + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} + diff --git a/storage/test/unit/test_like_escape.js b/storage/test/unit/test_like_escape.js new file mode 100644 index 000000000..414f6237c --- /dev/null +++ b/storage/test/unit/test_like_escape.js @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const LATIN1_AE = "\xc6"; +const LATIN1_ae = "\xe6"; + +function setup() +{ + getOpenedDatabase().createTable("t1", "x TEXT"); + + var stmt = createStatement("INSERT INTO t1 (x) VALUES ('foo/bar_baz%20cheese')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES (?1)"); + // insert LATIN_ae, but search on LATIN_AE + stmt.bindByIndex(0, "foo%20" + LATIN1_ae + "/_bar"); + stmt.execute(); + stmt.finalize(); +} + +function test_escape_for_like_ascii() +{ + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?1 ESCAPE '/'"); + var paramForLike = stmt.escapeStringForLIKE("oo/bar_baz%20chees", '/'); + // verify that we escaped / _ and % + do_check_eq(paramForLike, "oo//bar/_baz/%20chees"); + // prepend and append with % for "contains" + stmt.bindByIndex(0, "%" + paramForLike + "%"); + stmt.executeStep(); + do_check_eq("foo/bar_baz%20cheese", stmt.getString(0)); + stmt.finalize(); +} + +function test_escape_for_like_non_ascii() +{ + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?1 ESCAPE '/'"); + var paramForLike = stmt.escapeStringForLIKE("oo%20" + LATIN1_AE + "/_ba", '/'); + // verify that we escaped / _ and % + do_check_eq(paramForLike, "oo/%20" + LATIN1_AE + "///_ba"); + // prepend and append with % for "contains" + stmt.bindByIndex(0, "%" + paramForLike + "%"); + stmt.executeStep(); + do_check_eq("foo%20" + LATIN1_ae + "/_bar", stmt.getString(0)); + stmt.finalize(); +} + +var tests = [test_escape_for_like_ascii, test_escape_for_like_non_ascii]; + +function run_test() +{ + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_locale_collation.js b/storage/test/unit/test_locale_collation.js new file mode 100644 index 000000000..12ba2b943 --- /dev/null +++ b/storage/test/unit/test_locale_collation.js @@ -0,0 +1,304 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Bug 499990 - Locale-aware collation + * + * Tests our custom, locale-aware collating sequences. + */ + +// The name of the file containing the strings we'll sort during this test. +// The file's data is taken from intl/locale/tests/sort/us-ascii_base.txt and +// and intl/locale/tests/sort/us-ascii_sort.txt. +const DATA_BASENAME = "locale_collation.txt"; + +// The test data from DATA_BASENAME is read into this array. +var gStrings; + +// A collation created from the application's locale. Used by localeCompare(). +var gLocaleCollation; + +// A connection to our in-memory UTF-16-encoded database. +var gUtf16Conn; + +// Helper Functions + +/** + * Since we create a UTF-16 database we have to clean it up, in addition to + * the normal cleanup of Storage tests. + */ +function cleanupLocaleTests() +{ + print("-- Cleaning up test_locale_collation.js suite."); + gUtf16Conn.close(); + cleanup(); +} + +/** + * Creates a test database similar to the default one created in + * head_storage.js, except that this one uses UTF-16 encoding. + * + * @return A connection to the database. + */ +function createUtf16Database() +{ + print("Creating the in-memory UTF-16-encoded database."); + let conn = getService().openSpecialDatabase("memory"); + conn.executeSimpleSQL("PRAGMA encoding = 'UTF-16'"); + + print("Make sure the encoding was set correctly and is now UTF-16."); + let stmt = conn.createStatement("PRAGMA encoding"); + do_check_true(stmt.executeStep()); + let enc = stmt.getString(0); + stmt.finalize(); + + // The value returned will actually be UTF-16le or UTF-16be. + do_check_true(enc === "UTF-16le" || enc === "UTF-16be"); + + return conn; +} + +/** + * Compares aActual to aExpected, ensuring that the numbers and orderings of + * the two arrays' elements are the same. + * + * @param aActual + * An array of strings retrieved from the database. + * @param aExpected + * An array of strings to which aActual should be equivalent. + */ +function ensureResultsAreCorrect(aActual, aExpected) +{ + print("Actual results: " + aActual); + print("Expected results: " + aExpected); + + do_check_eq(aActual.length, aExpected.length); + for (let i = 0; i < aActual.length; i++) + do_check_eq(aActual[i], aExpected[i]); +} + +/** + * Synchronously SELECTs all rows from the test table of the given database + * using the given collation. + * + * @param aCollation + * The name of one of our custom locale collations. The rows are + * ordered by this collation. + * @param aConn + * A connection to either the UTF-8 database or the UTF-16 database. + * @return The resulting strings in an array. + */ +function getResults(aCollation, aConn) +{ + let results = []; + let stmt = aConn.createStatement("SELECT t FROM test " + + "ORDER BY t COLLATE " + aCollation + " ASC"); + while (stmt.executeStep()) + results.push(stmt.row.t); + stmt.finalize(); + return results; +} + +/** + * Inserts strings into our test table of the given database in the order given. + * + * @param aStrings + * An array of strings. + * @param aConn + * A connection to either the UTF-8 database or the UTF-16 database. + */ +function initTableWithStrings(aStrings, aConn) +{ + print("Initializing test table."); + + aConn.executeSimpleSQL("DROP TABLE IF EXISTS test"); + aConn.createTable("test", "t TEXT"); + let stmt = aConn.createStatement("INSERT INTO test (t) VALUES (:t)"); + aStrings.forEach(function (str) { + stmt.params.t = str; + stmt.execute(); + stmt.reset(); + }); + stmt.finalize(); +} + +/** + * Returns a sorting function suitable for passing to Array.prototype.sort(). + * The returned function uses the application's locale to compare strings. + * + * @param aCollation + * The name of one of our custom locale collations. The sorting + * strength is computed from this value. + * @return A function to use as a sorting callback. + */ +function localeCompare(aCollation) +{ + var strength; + + switch (aCollation) { + case "locale": + strength = Ci.nsICollation.kCollationCaseInSensitive; + break; + case "locale_case_sensitive": + strength = Ci.nsICollation.kCollationAccentInsenstive; + break; + case "locale_accent_sensitive": + strength = Ci.nsICollation.kCollationCaseInsensitiveAscii; + break; + case "locale_case_accent_sensitive": + strength = Ci.nsICollation.kCollationCaseSensitive; + break; + default: + do_throw("Error in test: unknown collation '" + aCollation + "'"); + break; + } + return function (aStr1, aStr2) { + return gLocaleCollation.compareString(strength, aStr1, aStr2); + }; +} + +/** + * Reads in the test data from the file DATA_BASENAME and returns it as an array + * of strings. + * + * @return The test data as an array of strings. + */ +function readTestData() +{ + print("Reading in test data."); + + let file = do_get_file(DATA_BASENAME); + + let istream = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + istream.init(file, -1, -1, 0); + istream.QueryInterface(Components.interfaces.nsILineInputStream); + + let line = {}; + let lines = []; + while (istream.readLine(line)) { + lines.push(line.value); + } + istream.close(); + + return lines; +} + +/** + * Gets the results from the given database using the given collation and + * ensures that they match gStrings sorted by the same collation. + * + * @param aCollation + * The name of one of our custom locale collations. The rows from the + * database and the expected results are ordered by this collation. + * @param aConn + * A connection to either the UTF-8 database or the UTF-16 database. + */ +function runTest(aCollation, aConn) +{ + ensureResultsAreCorrect(getResults(aCollation, aConn), + gStrings.slice(0).sort(localeCompare(aCollation))); +} + +/** + * Gets the results from the UTF-8 database using the given collation and + * ensures that they match gStrings sorted by the same collation. + * + * @param aCollation + * The name of one of our custom locale collations. The rows from the + * database and the expected results are ordered by this collation. + */ +function runUtf8Test(aCollation) +{ + runTest(aCollation, getOpenedDatabase()); +} + +/** + * Gets the results from the UTF-16 database using the given collation and + * ensures that they match gStrings sorted by the same collation. + * + * @param aCollation + * The name of one of our custom locale collations. The rows from the + * database and the expected results are ordered by this collation. + */ +function runUtf16Test(aCollation) +{ + runTest(aCollation, gUtf16Conn); +} + +/** + * Sets up the test suite. + */ +function setup() +{ + print("-- Setting up the test_locale_collation.js suite."); + + gStrings = readTestData(); + + initTableWithStrings(gStrings, getOpenedDatabase()); + + gUtf16Conn = createUtf16Database(); + initTableWithStrings(gStrings, gUtf16Conn); + + let localeSvc = Cc["@mozilla.org/intl/nslocaleservice;1"]. + getService(Ci.nsILocaleService); + let collFact = Cc["@mozilla.org/intl/collation-factory;1"]. + createInstance(Ci.nsICollationFactory); + gLocaleCollation = collFact.CreateCollation(localeSvc.getApplicationLocale()); +} + +// Test Runs + +var gTests = [ + { + desc: "Case and accent sensitive UTF-8", + run: () => runUtf8Test("locale_case_accent_sensitive") + }, + + { + desc: "Case sensitive, accent insensitive UTF-8", + run: () => runUtf8Test("locale_case_sensitive") + }, + + { + desc: "Case insensitive, accent sensitive UTF-8", + run: () => runUtf8Test("locale_accent_sensitive") + }, + + { + desc: "Case and accent insensitive UTF-8", + run: () => runUtf8Test("locale") + }, + + { + desc: "Case and accent sensitive UTF-16", + run: () => runUtf16Test("locale_case_accent_sensitive") + }, + + { + desc: "Case sensitive, accent insensitive UTF-16", + run: () => runUtf16Test("locale_case_sensitive") + }, + + { + desc: "Case insensitive, accent sensitive UTF-16", + run: () => runUtf16Test("locale_accent_sensitive") + }, + + { + desc: "Case and accent insensitive UTF-16", + run: () => runUtf16Test("locale") + }, +]; + +function run_test() +{ + setup(); + gTests.forEach(function (test) { + print("-- Running test: " + test.desc); + test.run(); + }); +} diff --git a/storage/test/unit/test_page_size_is_32k.js b/storage/test/unit/test_page_size_is_32k.js new file mode 100644 index 000000000..a2548d1e6 --- /dev/null +++ b/storage/test/unit/test_page_size_is_32k.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This file tests that dbs are using 32k pagesize + +const kExpectedPageSize = 32768; // 32K +const kExpectedCacheSize = -2048; // 2MiB + +function check_size(db) +{ + var stmt = db.createStatement("PRAGMA page_size"); + stmt.executeStep(); + do_check_eq(stmt.getInt32(0), kExpectedPageSize); + stmt.finalize(); + stmt = db.createStatement("PRAGMA cache_size"); + stmt.executeStep(); + do_check_eq(stmt.getInt32(0), kExpectedCacheSize); + stmt.finalize(); +} + +function new_file(name) +{ + var file = dirSvc.get("ProfD", Ci.nsIFile); + file.append(name + ".sqlite"); + do_check_false(file.exists()); + return file; +} + +function run_test() +{ + check_size(getDatabase(new_file("shared32k"))); + check_size(getService().openUnsharedDatabase(new_file("unshared32k"))); +} + diff --git a/storage/test/unit/test_sqlite_secure_delete.js b/storage/test/unit/test_sqlite_secure_delete.js new file mode 100644 index 000000000..1eff34f70 --- /dev/null +++ b/storage/test/unit/test_sqlite_secure_delete.js @@ -0,0 +1,80 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + *vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This file tests to make sure that SQLite was compiled with + * SQLITE_SECURE_DELETE=1. + */ + +// Helper Methods + +/** + * Reads the contents of a file and returns it as a string. + * + * @param aFile + * The file to return from. + * @return the contents of the file in the form of a string. + */ +function getFileContents(aFile) +{ + let fstream = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + fstream.init(aFile, -1, 0, 0); + + let bstream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + bstream.setInputStream(fstream); + return bstream.readBytes(bstream.available()); +} + +// Tests + +add_test(function test_delete_removes_data() { + const TEST_STRING = "SomeRandomStringToFind"; + + let file = getTestDB(); + let db = getService().openDatabase(file); + + // Create the table and insert the data. + db.createTable("test", "data TEXT"); + let stmt = db.createStatement("INSERT INTO test VALUES(:data)"); + stmt.params.data = TEST_STRING; + try { + stmt.execute(); + } + finally { + stmt.finalize(); + } + + // Make sure this test is actually testing what it thinks by making sure the + // string shows up in the database. Because the previous statement was + // automatically wrapped in a transaction, the contents are already on disk. + let contents = getFileContents(file); + do_check_neq(-1, contents.indexOf(TEST_STRING)); + + // Delete the data, and then close the database. + stmt = db.createStatement("DELETE FROM test WHERE data = :data"); + stmt.params.data = TEST_STRING; + try { + stmt.execute(); + } + finally { + stmt.finalize(); + } + db.close(); + + // Check the file to see if the string can be found. + contents = getFileContents(file); + do_check_eq(-1, contents.indexOf(TEST_STRING)); + + run_next_test(); +}); + +function run_test() +{ + cleanup(); + run_next_test(); +} diff --git a/storage/test/unit/test_statement_executeAsync.js b/storage/test/unit/test_statement_executeAsync.js new file mode 100644 index 000000000..edcecb999 --- /dev/null +++ b/storage/test/unit/test_statement_executeAsync.js @@ -0,0 +1,998 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This file tests the functionality of mozIStorageBaseStatement::executeAsync + * for both mozIStorageStatement and mozIStorageAsyncStatement. + */ + +const INTEGER = 1; +const TEXT = "this is test text"; +const REAL = 3.23; +const BLOB = [1, 2]; + +/** + * Execute the given statement asynchronously, spinning an event loop until the + * async statement completes. + * + * @param aStmt + * The statement to execute. + * @param [aOptions={}] + * @param [aOptions.error=false] + * If true we should expect an error whose code we do not care about. If + * a numeric value, that's the error code we expect and require. If we + * are expecting an error, we expect a completion reason of REASON_ERROR. + * Otherwise we expect no error notification and a completion reason of + * REASON_FINISHED. + * @param [aOptions.cancel] + * If true we cancel the pending statement and additionally return the + * pending statement in case you want to further manipulate it. + * @param [aOptions.returnPending=false] + * If true we keep the pending statement around and return it to you. We + * normally avoid doing this to try and minimize the amount of time a + * reference is held to the returned pending statement. + * @param [aResults] + * If omitted, we assume no results rows are expected. If it is a + * number, we assume it is the number of results rows expected. If it is + * a function, we assume it is a function that takes the 1) result row + * number, 2) result tuple, 3) call stack for the original call to + * execAsync as arguments. If it is a list, we currently assume it is a + * list of functions where each function is intended to evaluate the + * result row at that ordinal position and takes the result tuple and + * the call stack for the original call. + */ +function execAsync(aStmt, aOptions, aResults) +{ + let caller = Components.stack.caller; + if (aOptions == null) + aOptions = {}; + + let resultsExpected; + let resultsChecker; + if (aResults == null) { + resultsExpected = 0; + } + else if (typeof aResults == "number") { + resultsExpected = aResults; + } + else if (typeof aResults == "function") { + resultsChecker = aResults; + } + else { // array + resultsExpected = aResults.length; + resultsChecker = function (aResultNum, aTup, aCaller) { + aResults[aResultNum](aTup, aCaller); + }; + } + let resultsSeen = 0; + + let errorCodeExpected = false; + let reasonExpected = Ci.mozIStorageStatementCallback.REASON_FINISHED; + let altReasonExpected = null; + if ("error" in aOptions) { + errorCodeExpected = aOptions.error; + if (errorCodeExpected) + reasonExpected = Ci.mozIStorageStatementCallback.REASON_ERROR; + } + let errorCodeSeen = false; + + if ("cancel" in aOptions && aOptions.cancel) + altReasonExpected = Ci.mozIStorageStatementCallback.REASON_CANCELED; + + let completed = false; + + let listener = { + handleResult(aResultSet) + { + let row, resultsSeenThisCall = 0; + while ((row = aResultSet.getNextRow()) != null) { + if (resultsChecker) + resultsChecker(resultsSeen, row, caller); + resultsSeen++; + resultsSeenThisCall++; + } + + if (!resultsSeenThisCall) + do_throw("handleResult invoked with 0 result rows!"); + }, + handleError(aError) + { + if (errorCodeSeen != false) + do_throw("handleError called when we already had an error!"); + errorCodeSeen = aError.result; + }, + handleCompletion(aReason) + { + if (completed) // paranoia check + do_throw("Received a second handleCompletion notification!", caller); + + if (resultsSeen != resultsExpected) + do_throw("Expected " + resultsExpected + " rows of results but " + + "got " + resultsSeen + " rows!", caller); + + if (errorCodeExpected == true && errorCodeSeen == false) + do_throw("Expected an error, but did not see one.", caller); + else if (errorCodeExpected != errorCodeSeen) + do_throw("Expected error code " + errorCodeExpected + " but got " + + errorCodeSeen, caller); + + if (aReason != reasonExpected && aReason != altReasonExpected) + do_throw("Expected reason " + reasonExpected + + (altReasonExpected ? (" or " + altReasonExpected) : "") + + " but got " + aReason, caller); + + completed = true; + } + }; + + let pending; + // Only get a pending reference if we're supposed to do. + // (note: This does not stop XPConnect from holding onto one currently.) + if (("cancel" in aOptions && aOptions.cancel) || + ("returnPending" in aOptions && aOptions.returnPending)) { + pending = aStmt.executeAsync(listener); + } + else { + aStmt.executeAsync(listener); + } + + if ("cancel" in aOptions && aOptions.cancel) + pending.cancel(); + + let curThread = Components.classes["@mozilla.org/thread-manager;1"] + .getService().currentThread; + while (!completed && !_quit) + curThread.processNextEvent(true); + + return pending; +} + +/** + * Make sure that illegal SQL generates the expected runtime error and does not + * result in any crashes. Async-only since the synchronous case generates the + * error synchronously (and is tested elsewhere). + */ +function test_illegal_sql_async_deferred() +{ + // gibberish + let stmt = makeTestStatement("I AM A ROBOT. DO AS I SAY."); + execAsync(stmt, {error: Ci.mozIStorageError.ERROR}); + stmt.finalize(); + + // legal SQL syntax, but with semantics issues. + stmt = makeTestStatement("SELECT destination FROM funkytown"); + execAsync(stmt, {error: Ci.mozIStorageError.ERROR}); + stmt.finalize(); + + run_next_test(); +} +test_illegal_sql_async_deferred.asyncOnly = true; + +function test_create_table() +{ + // Ensure our table doesn't exist + do_check_false(getOpenedDatabase().tableExists("test")); + + var stmt = makeTestStatement( + "CREATE TABLE test (" + + "id INTEGER, " + + "string TEXT, " + + "number REAL, " + + "nuller NULL, " + + "blober BLOB" + + ")" + ); + execAsync(stmt); + stmt.finalize(); + + // Check that the table has been created + do_check_true(getOpenedDatabase().tableExists("test")); + + // Verify that it's created correctly (this will throw if it wasn't) + let checkStmt = getOpenedDatabase().createStatement( + "SELECT id, string, number, nuller, blober FROM test" + ); + checkStmt.finalize(); + run_next_test(); +} + +function test_add_data() +{ + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (?, ?, ?, ?, ?)" + ); + stmt.bindBlobByIndex(4, BLOB, BLOB.length); + stmt.bindByIndex(3, null); + stmt.bindByIndex(2, REAL); + stmt.bindByIndex(1, TEXT); + stmt.bindByIndex(0, INTEGER); + + execAsync(stmt); + stmt.finalize(); + + // Check that the result is in the table + verifyQuery("SELECT string, number, nuller, blober FROM test WHERE id = ?", + INTEGER, + [TEXT, REAL, null, BLOB]); + run_next_test(); +} + +function test_get_data() +{ + var stmt = makeTestStatement( + "SELECT string, number, nuller, blober, id FROM test WHERE id = ?" + ); + stmt.bindByIndex(0, INTEGER); + execAsync(stmt, {}, [ + function (tuple) { + do_check_neq(null, tuple); + + // Check that it's what we expect + do_check_false(tuple.getIsNull(0)); + do_check_eq(tuple.getResultByName("string"), tuple.getResultByIndex(0)); + do_check_eq(TEXT, tuple.getResultByName("string")); + do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_TEXT, + tuple.getTypeOfIndex(0)); + + do_check_false(tuple.getIsNull(1)); + do_check_eq(tuple.getResultByName("number"), tuple.getResultByIndex(1)); + do_check_eq(REAL, tuple.getResultByName("number")); + do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT, + tuple.getTypeOfIndex(1)); + + do_check_true(tuple.getIsNull(2)); + do_check_eq(tuple.getResultByName("nuller"), tuple.getResultByIndex(2)); + do_check_eq(null, tuple.getResultByName("nuller")); + do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_NULL, + tuple.getTypeOfIndex(2)); + + do_check_false(tuple.getIsNull(3)); + var blobByName = tuple.getResultByName("blober"); + do_check_eq(BLOB.length, blobByName.length); + var blobByIndex = tuple.getResultByIndex(3); + do_check_eq(BLOB.length, blobByIndex.length); + for (let i = 0; i < BLOB.length; i++) { + do_check_eq(BLOB[i], blobByName[i]); + do_check_eq(BLOB[i], blobByIndex[i]); + } + var count = { value: 0 }; + var blob = { value: null }; + tuple.getBlob(3, count, blob); + do_check_eq(BLOB.length, count.value); + for (let i = 0; i < BLOB.length; i++) + do_check_eq(BLOB[i], blob.value[i]); + do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_BLOB, + tuple.getTypeOfIndex(3)); + + do_check_false(tuple.getIsNull(4)); + do_check_eq(tuple.getResultByName("id"), tuple.getResultByIndex(4)); + do_check_eq(INTEGER, tuple.getResultByName("id")); + do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER, + tuple.getTypeOfIndex(4)); + }]); + stmt.finalize(); + run_next_test(); +} + +function test_tuple_out_of_bounds() +{ + var stmt = makeTestStatement( + "SELECT string FROM test" + ); + execAsync(stmt, {}, [ + function (tuple) { + do_check_neq(null, tuple); + + // Check all out of bounds - should throw + var methods = [ + "getTypeOfIndex", + "getInt32", + "getInt64", + "getDouble", + "getUTF8String", + "getString", + "getIsNull", + ]; + for (var i in methods) { + try { + tuple[methods[i]](tuple.numEntries); + do_throw("did not throw :("); + } + catch (e) { + do_check_eq(Cr.NS_ERROR_ILLEGAL_VALUE, e.result); + } + } + + // getBlob requires more args... + try { + var blob = { value: null }; + var size = { value: 0 }; + tuple.getBlob(tuple.numEntries, blob, size); + do_throw("did not throw :("); + } + catch (e) { + do_check_eq(Cr.NS_ERROR_ILLEGAL_VALUE, e.result); + } + }]); + stmt.finalize(); + run_next_test(); +} + +function test_no_listener_works_on_success() +{ + var stmt = makeTestStatement( + "DELETE FROM test WHERE id = ?" + ); + stmt.bindByIndex(0, 0); + stmt.executeAsync(); + stmt.finalize(); + + // Run the next test. + run_next_test(); +} + +function test_no_listener_works_on_results() +{ + var stmt = makeTestStatement( + "SELECT ?" + ); + stmt.bindByIndex(0, 1); + stmt.executeAsync(); + stmt.finalize(); + + // Run the next test. + run_next_test(); +} + +function test_no_listener_works_on_error() +{ + // commit without a transaction will trigger an error + var stmt = makeTestStatement( + "COMMIT" + ); + stmt.executeAsync(); + stmt.finalize(); + + // Run the next test. + run_next_test(); +} + +function test_partial_listener_works() +{ + var stmt = makeTestStatement( + "DELETE FROM test WHERE id = ?" + ); + stmt.bindByIndex(0, 0); + stmt.executeAsync({ + handleResult(aResultSet) {} + }); + stmt.executeAsync({ + handleError(aError) {} + }); + stmt.executeAsync({ + handleCompletion(aReason) {} + }); + stmt.finalize(); + + // Run the next test. + run_next_test(); +} + +/** + * Dubious cancellation test that depends on system loading may or may not + * succeed in canceling things. It does at least test if calling cancel blows + * up. test_AsyncCancellation in test_true_async.cpp is our test that canceling + * actually works correctly. + */ +function test_immediate_cancellation() +{ + var stmt = makeTestStatement( + "DELETE FROM test WHERE id = ?" + ); + stmt.bindByIndex(0, 0); + execAsync(stmt, {cancel: true}); + stmt.finalize(); + run_next_test(); +} + +/** + * Test that calling cancel twice throws the second time. + */ +function test_double_cancellation() +{ + var stmt = makeTestStatement( + "DELETE FROM test WHERE id = ?" + ); + stmt.bindByIndex(0, 0); + let pendingStatement = execAsync(stmt, {cancel: true}); + // And cancel again - expect an exception + expectError(Cr.NS_ERROR_UNEXPECTED, + () => pendingStatement.cancel()); + + stmt.finalize(); + run_next_test(); +} + +/** + * Verify that nothing untoward happens if we try and cancel something after it + * has fully run to completion. + */ +function test_cancellation_after_execution() +{ + var stmt = makeTestStatement( + "DELETE FROM test WHERE id = ?" + ); + stmt.bindByIndex(0, 0); + let pendingStatement = execAsync(stmt, {returnPending: true}); + // (the statement has fully executed at this point) + // canceling after the statement has run to completion should not throw! + pendingStatement.cancel(); + + stmt.finalize(); + run_next_test(); +} + +/** + * Verifies that a single statement can be executed more than once. Might once + * have been intended to also ensure that callback notifications were not + * incorrectly interleaved, but that part was brittle (it's totally fine for + * handleResult to get called multiple times) and not comprehensive. + */ +function test_double_execute() +{ + var stmt = makeTestStatement( + "SELECT 1" + ); + execAsync(stmt, null, 1); + execAsync(stmt, null, 1); + stmt.finalize(); + run_next_test(); +} + +function test_finalized_statement_does_not_crash() +{ + var stmt = makeTestStatement( + "SELECT * FROM TEST" + ); + stmt.finalize(); + // we are concerned about a crash here; an error is fine. + try { + stmt.executeAsync(); + } catch (ex) { + // Do nothing. + } + + // Run the next test. + run_next_test(); +} + +/** + * Bind by mozIStorageBindingParams on the mozIStorageBaseStatement by index. + */ +function test_bind_direct_binding_params_by_index() +{ + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (?, ?, ?, ?, ?)" + ); + let insertId = nextUniqueId++; + stmt.bindByIndex(0, insertId); + stmt.bindByIndex(1, TEXT); + stmt.bindByIndex(2, REAL); + stmt.bindByIndex(3, null); + stmt.bindBlobByIndex(4, BLOB, BLOB.length); + execAsync(stmt); + stmt.finalize(); + verifyQuery("SELECT string, number, nuller, blober FROM test WHERE id = ?", + insertId, + [TEXT, REAL, null, BLOB]); + run_next_test(); +} + +/** + * Bind by mozIStorageBindingParams on the mozIStorageBaseStatement by name. + */ +function test_bind_direct_binding_params_by_name() +{ + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (:int, :text, :real, :null, :blob)" + ); + let insertId = nextUniqueId++; + stmt.bindByName("int", insertId); + stmt.bindByName("text", TEXT); + stmt.bindByName("real", REAL); + stmt.bindByName("null", null); + stmt.bindBlobByName("blob", BLOB, BLOB.length); + execAsync(stmt); + stmt.finalize(); + verifyQuery("SELECT string, number, nuller, blober FROM test WHERE id = ?", + insertId, + [TEXT, REAL, null, BLOB]); + run_next_test(); +} + +function test_bind_js_params_helper_by_index() +{ + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (?, ?, ?, ?, NULL)" + ); + let insertId = nextUniqueId++; + // we cannot bind blobs this way; no blober + stmt.params[3] = null; + stmt.params[2] = REAL; + stmt.params[1] = TEXT; + stmt.params[0] = insertId; + execAsync(stmt); + stmt.finalize(); + verifyQuery("SELECT string, number, nuller FROM test WHERE id = ?", insertId, + [TEXT, REAL, null]); + run_next_test(); +} + +function test_bind_js_params_helper_by_name() +{ + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (:int, :text, :real, :null, NULL)" + ); + let insertId = nextUniqueId++; + // we cannot bind blobs this way; no blober + stmt.params.null = null; + stmt.params.real = REAL; + stmt.params.text = TEXT; + stmt.params.int = insertId; + execAsync(stmt); + stmt.finalize(); + verifyQuery("SELECT string, number, nuller FROM test WHERE id = ?", insertId, + [TEXT, REAL, null]); + run_next_test(); +} + +function test_bind_multiple_rows_by_index() +{ + const AMOUNT_TO_ADD = 5; + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (?, ?, ?, ?, ?)" + ); + var array = stmt.newBindingParamsArray(); + for (let i = 0; i < AMOUNT_TO_ADD; i++) { + let bp = array.newBindingParams(); + bp.bindByIndex(0, INTEGER); + bp.bindByIndex(1, TEXT); + bp.bindByIndex(2, REAL); + bp.bindByIndex(3, null); + bp.bindBlobByIndex(4, BLOB, BLOB.length); + array.addParams(bp); + do_check_eq(array.length, i + 1); + } + stmt.bindParameters(array); + + let rowCount = getTableRowCount("test"); + execAsync(stmt); + do_check_eq(rowCount + AMOUNT_TO_ADD, getTableRowCount("test")); + stmt.finalize(); + run_next_test(); +} + +function test_bind_multiple_rows_by_name() +{ + const AMOUNT_TO_ADD = 5; + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (:int, :text, :real, :null, :blob)" + ); + var array = stmt.newBindingParamsArray(); + for (let i = 0; i < AMOUNT_TO_ADD; i++) { + let bp = array.newBindingParams(); + bp.bindByName("int", INTEGER); + bp.bindByName("text", TEXT); + bp.bindByName("real", REAL); + bp.bindByName("null", null); + bp.bindBlobByName("blob", BLOB, BLOB.length); + array.addParams(bp); + do_check_eq(array.length, i + 1); + } + stmt.bindParameters(array); + + let rowCount = getTableRowCount("test"); + execAsync(stmt); + do_check_eq(rowCount + AMOUNT_TO_ADD, getTableRowCount("test")); + stmt.finalize(); + run_next_test(); +} + +/** + * Verify that a mozIStorageStatement instance throws immediately when we + * try and bind to an illegal index. + */ +function test_bind_out_of_bounds_sync_immediate() +{ + let stmt = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (?)" + ); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + + // Check variant binding. + expectError(Cr.NS_ERROR_INVALID_ARG, + () => bp.bindByIndex(1, INTEGER)); + // Check blob binding. + expectError(Cr.NS_ERROR_INVALID_ARG, + () => bp.bindBlobByIndex(1, BLOB, BLOB.length)); + + stmt.finalize(); + run_next_test(); +} +test_bind_out_of_bounds_sync_immediate.syncOnly = true; + +/** + * Verify that a mozIStorageAsyncStatement reports an error asynchronously when + * we bind to an illegal index. + */ +function test_bind_out_of_bounds_async_deferred() +{ + let stmt = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (?)" + ); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + + // There is no difference between variant and blob binding for async purposes. + bp.bindByIndex(1, INTEGER); + array.addParams(bp); + stmt.bindParameters(array); + execAsync(stmt, {error: Ci.mozIStorageError.RANGE}); + + stmt.finalize(); + run_next_test(); +} +test_bind_out_of_bounds_async_deferred.asyncOnly = true; + +function test_bind_no_such_name_sync_immediate() +{ + let stmt = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (:foo)" + ); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + + // Check variant binding. + expectError(Cr.NS_ERROR_INVALID_ARG, + () => bp.bindByName("doesnotexist", INTEGER)); + // Check blob binding. + expectError(Cr.NS_ERROR_INVALID_ARG, + () => bp.bindBlobByName("doesnotexist", BLOB, BLOB.length)); + + stmt.finalize(); + run_next_test(); +} +test_bind_no_such_name_sync_immediate.syncOnly = true; + +function test_bind_no_such_name_async_deferred() +{ + let stmt = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (:foo)" + ); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + + bp.bindByName("doesnotexist", INTEGER); + array.addParams(bp); + stmt.bindParameters(array); + execAsync(stmt, {error: Ci.mozIStorageError.RANGE}); + + stmt.finalize(); + run_next_test(); +} +test_bind_no_such_name_async_deferred.asyncOnly = true; + +function test_bind_bogus_type_by_index() +{ + // We try to bind a JS Object here that should fail to bind. + let stmt = makeTestStatement( + "INSERT INTO test (blober) " + + "VALUES (?)" + ); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + Assert.throws(() => bp.bindByIndex(0, run_test), /NS_ERROR_UNEXPECTED/); + + stmt.finalize(); + run_next_test(); +} + +function test_bind_bogus_type_by_name() +{ + // We try to bind a JS Object here that should fail to bind. + let stmt = makeTestStatement( + "INSERT INTO test (blober) " + + "VALUES (:blob)" + ); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + Assert.throws(() => bp.bindByName("blob", run_test), /NS_ERROR_UNEXPECTED/); + + stmt.finalize(); + run_next_test(); +} + +function test_bind_params_already_locked() +{ + let stmt = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (:int)" + ); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + bp.bindByName("int", INTEGER); + array.addParams(bp); + + // We should get an error after we call addParams and try to bind again. + expectError(Cr.NS_ERROR_UNEXPECTED, + () => bp.bindByName("int", INTEGER)); + + stmt.finalize(); + run_next_test(); +} + +function test_bind_params_array_already_locked() +{ + let stmt = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (:int)" + ); + + let array = stmt.newBindingParamsArray(); + let bp1 = array.newBindingParams(); + bp1.bindByName("int", INTEGER); + array.addParams(bp1); + let bp2 = array.newBindingParams(); + stmt.bindParameters(array); + bp2.bindByName("int", INTEGER); + + // We should get an error after we have bound the array to the statement. + expectError(Cr.NS_ERROR_UNEXPECTED, + () => array.addParams(bp2)); + + stmt.finalize(); + run_next_test(); +} + +function test_no_binding_params_from_locked_array() +{ + let stmt = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (:int)" + ); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + bp.bindByName("int", INTEGER); + array.addParams(bp); + stmt.bindParameters(array); + + // We should not be able to get a new BindingParams object after we have bound + // to the statement. + expectError(Cr.NS_ERROR_UNEXPECTED, + () => array.newBindingParams()); + + stmt.finalize(); + run_next_test(); +} + +function test_not_right_owning_array() +{ + let stmt = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (:int)" + ); + + let array1 = stmt.newBindingParamsArray(); + let array2 = stmt.newBindingParamsArray(); + let bp = array1.newBindingParams(); + bp.bindByName("int", INTEGER); + + // We should not be able to add bp to array2 since it was created from array1. + expectError(Cr.NS_ERROR_UNEXPECTED, + () => array2.addParams(bp)); + + stmt.finalize(); + run_next_test(); +} + +function test_not_right_owning_statement() +{ + let stmt1 = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (:int)" + ); + let stmt2 = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (:int)" + ); + + let array1 = stmt1.newBindingParamsArray(); + let array2 = stmt2.newBindingParamsArray(); + let bp = array1.newBindingParams(); + bp.bindByName("int", INTEGER); + array1.addParams(bp); + + // We should not be able to bind array1 since it was created from stmt1. + expectError(Cr.NS_ERROR_UNEXPECTED, + () => stmt2.bindParameters(array1)); + + stmt1.finalize(); + stmt2.finalize(); + run_next_test(); +} + +function test_bind_empty_array() +{ + let stmt = makeTestStatement( + "INSERT INTO test (id) " + + "VALUES (:int)" + ); + + let paramsArray = stmt.newBindingParamsArray(); + + // We should not be able to bind this array to the statement because it is + // empty. + expectError(Cr.NS_ERROR_UNEXPECTED, + () => stmt.bindParameters(paramsArray)); + + stmt.finalize(); + run_next_test(); +} + +function test_multiple_results() +{ + let expectedResults = getTableRowCount("test"); + // Sanity check - we should have more than one result, but let's be sure. + do_check_true(expectedResults > 1); + + // Now check that we get back two rows of data from our async query. + let stmt = makeTestStatement("SELECT * FROM test"); + execAsync(stmt, {}, expectedResults); + + stmt.finalize(); + run_next_test(); +} + +// Test Runner + +const TEST_PASS_SYNC = 0; +const TEST_PASS_ASYNC = 1; +/** + * We run 2 passes against the test. One where makeTestStatement generates + * synchronous (mozIStorageStatement) statements and one where it generates + * asynchronous (mozIStorageAsyncStatement) statements. + * + * Because of differences in the ability to know the number of parameters before + * dispatching, some tests are sync/async specific. These functions are marked + * with 'syncOnly' or 'asyncOnly' attributes and run_next_test knows what to do. + */ +var testPass = TEST_PASS_SYNC; + +/** + * Create a statement of the type under test per testPass. + * + * @param aSQL + * The SQL string from which to build a statement. + * @return a statement of the type under test per testPass. + */ +function makeTestStatement(aSQL) { + if (testPass == TEST_PASS_SYNC) { + return getOpenedDatabase().createStatement(aSQL); + } + return getOpenedDatabase().createAsyncStatement(aSQL); +} + +var tests = [ + test_illegal_sql_async_deferred, + test_create_table, + test_add_data, + test_get_data, + test_tuple_out_of_bounds, + test_no_listener_works_on_success, + test_no_listener_works_on_results, + test_no_listener_works_on_error, + test_partial_listener_works, + test_immediate_cancellation, + test_double_cancellation, + test_cancellation_after_execution, + test_double_execute, + test_finalized_statement_does_not_crash, + test_bind_direct_binding_params_by_index, + test_bind_direct_binding_params_by_name, + test_bind_js_params_helper_by_index, + test_bind_js_params_helper_by_name, + test_bind_multiple_rows_by_index, + test_bind_multiple_rows_by_name, + test_bind_out_of_bounds_sync_immediate, + test_bind_out_of_bounds_async_deferred, + test_bind_no_such_name_sync_immediate, + test_bind_no_such_name_async_deferred, + test_bind_bogus_type_by_index, + test_bind_bogus_type_by_name, + test_bind_params_already_locked, + test_bind_params_array_already_locked, + test_bind_empty_array, + test_no_binding_params_from_locked_array, + test_not_right_owning_array, + test_not_right_owning_statement, + test_multiple_results, +]; +var index = 0; + +const STARTING_UNIQUE_ID = 2; +var nextUniqueId = STARTING_UNIQUE_ID; + +function run_next_test() +{ + function _run_next_test() { + // use a loop so we can skip tests... + while (index < tests.length) { + let test = tests[index++]; + // skip tests not appropriate to the current test pass + if ((testPass == TEST_PASS_SYNC && ("asyncOnly" in test)) || + (testPass == TEST_PASS_ASYNC && ("syncOnly" in test))) + continue; + + // Asynchronous tests means that exceptions don't kill the test. + try { + print("****** Running the next test: " + test.name); + test(); + return; + } + catch (e) { + do_throw(e); + } + } + + // if we only completed the first pass, move to the next pass + if (testPass == TEST_PASS_SYNC) { + print("********* Beginning mozIStorageAsyncStatement pass."); + testPass++; + index = 0; + // a new pass demands a new database + asyncCleanup(); + nextUniqueId = STARTING_UNIQUE_ID; + _run_next_test(); + return; + } + + // we did some async stuff; we need to clean up. + asyncCleanup(); + do_test_finished(); + } + + // Don't actually schedule another test if we're quitting. + if (!_quit) { + // For saner stacks, we execute this code RSN. + do_execute_soon(_run_next_test); + } +} + +function run_test() +{ + cleanup(); + + do_test_pending(); + run_next_test(); +} diff --git a/storage/test/unit/test_statement_wrapper_automatically.js b/storage/test/unit/test_statement_wrapper_automatically.js new file mode 100644 index 000000000..58b27dd2d --- /dev/null +++ b/storage/test/unit/test_statement_wrapper_automatically.js @@ -0,0 +1,167 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + vim:set ts=2 sw=2 sts=2 et: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the functions of mozIStorageStatementWrapper + +function setup() +{ + getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, val NONE," + + "alt_val NONE"); +} + +/** + * A convenience wrapper for do_check_eq. Calls do_check_eq on aActualVal + * and aReturnedVal, with one caveat. + * + * Date objects are converted before parameter binding to PRTime's (microsecs + * since epoch). They are not reconverted when retrieved from the database. + * This function abstracts away this reconversion so that you can pass in, + * for example: + * + * checkVal(new Date(), aReturnedVal) // this + * checkVal(new Date().valueOf() * 1000.0, aReturnedVal) // instead of this + * + * Should any other types require conversion in the future, their conversions + * may also be abstracted away here. + * + * @param aActualVal + * the value inserted into the database + * @param aReturnedVal + * the value retrieved from the database + */ +function checkVal(aActualVal, aReturnedVal) +{ + if (aActualVal instanceof Date) aActualVal = aActualVal.valueOf() * 1000.0; + do_check_eq(aActualVal, aReturnedVal); +} + +/** + * Removes all rows from our test table. + */ +function clearTable() +{ + var stmt = createStatement("DELETE FROM test"); + stmt.execute(); + stmt.finalize(); + ensureNumRows(0); +} + +/** + * Ensures that the number of rows in our test table is equal to aNumRows. + * Calls do_check_eq on aNumRows and the value retrieved by SELECT'ing COUNT(*). + * + * @param aNumRows + * the number of rows our test table should contain + */ +function ensureNumRows(aNumRows) +{ + var stmt = createStatement("SELECT COUNT(*) AS number FROM test"); + do_check_true(stmt.step()); + do_check_eq(aNumRows, stmt.row.number); + stmt.reset(); + stmt.finalize(); +} + +/** + * Inserts aVal into our test table and checks that insertion was successful by + * retrieving the newly inserted value from the database and comparing it + * against aVal. aVal is bound to a single parameter. + * + * @param aVal + * value to insert into our test table and check + */ +function insertAndCheckSingleParam(aVal) +{ + clearTable(); + + var stmt = createStatement("INSERT INTO test (val) VALUES (:val)"); + stmt.params.val = aVal; + stmt.execute(); + stmt.finalize(); + + ensureNumRows(1); + + stmt = createStatement("SELECT val FROM test WHERE id = 1"); + do_check_true(stmt.step()); + checkVal(aVal, stmt.row.val); + stmt.reset(); + stmt.finalize(); +} + +/** + * Inserts aVal into our test table and checks that insertion was successful by + * retrieving the newly inserted value from the database and comparing it + * against aVal. aVal is bound to two separate parameters, both of which are + * checked against aVal. + * + * @param aVal + * value to insert into our test table and check + */ +function insertAndCheckMultipleParams(aVal) +{ + clearTable(); + + var stmt = createStatement("INSERT INTO test (val, alt_val) " + + "VALUES (:val, :val)"); + stmt.params.val = aVal; + stmt.execute(); + stmt.finalize(); + + ensureNumRows(1); + + stmt = createStatement("SELECT val, alt_val FROM test WHERE id = 1"); + do_check_true(stmt.step()); + checkVal(aVal, stmt.row.val); + checkVal(aVal, stmt.row.alt_val); + stmt.reset(); + stmt.finalize(); +} + +/** + * A convenience function that prints out a description of aVal using + * aVal.toString and aVal.toSource. Output is useful when the test fails. + * + * @param aVal + * a value inserted or to be inserted into our test table + */ +function printValDesc(aVal) +{ + try { + var toSource = aVal.toSource(); + } catch (ex) { + toSource = ""; + } + print("Testing value: toString=" + aVal + + (toSource ? " toSource=" + toSource : "")); +} + +function run_test() +{ + setup(); + + // function JSValStorageStatementBinder in + // storage/mozStorageStatementParams.cpp tells us that the following types + // and only the following types are valid as statement parameters: + var vals = [ + 1337, // int + 3.1337, // double + "foo", // string + true, // boolean + null, // null + new Date(), // Date object + ]; + + vals.forEach(function (val) + { + printValDesc(val); + print("Single parameter"); + insertAndCheckSingleParam(val); + print("Multiple parameters"); + insertAndCheckMultipleParams(val); + }); + + cleanup(); +} diff --git a/storage/test/unit/test_storage_aggregates.js b/storage/test/unit/test_storage_aggregates.js new file mode 100644 index 000000000..400aba836 --- /dev/null +++ b/storage/test/unit/test_storage_aggregates.js @@ -0,0 +1,116 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the custom aggregate functions + +var testNums = [1, 2, 3, 4]; + +function setup() +{ + getOpenedDatabase().createTable("function_tests", "id INTEGER PRIMARY KEY"); + + var stmt = createStatement("INSERT INTO function_tests (id) VALUES(?1)"); + for (let i = 0; i < testNums.length; ++i) { + stmt.bindByIndex(0, testNums[i]); + stmt.execute(); + } + stmt.reset(); + stmt.finalize(); +} + +var testSquareAndSumFunction = { + calls: 0, + _sas: 0, + + reset() { + this.calls = 0; + this._sas = 0; + }, + + onStep(val) { + ++this.calls; + this._sas += val.getInt32(0) * val.getInt32(0); + }, + + onFinal() { + var retval = this._sas; + this._sas = 0; // Prepare for next group + return retval; + } +}; + +function test_aggregate_registration() +{ + var msc = getOpenedDatabase(); + msc.createAggregateFunction("test_sas_aggr", 1, testSquareAndSumFunction); +} + +function test_aggregate_no_double_registration() +{ + var msc = getOpenedDatabase(); + try { + msc.createAggregateFunction("test_sas_aggr", 2, testSquareAndSumFunction); + do_throw("We shouldn't get here!"); + } catch (e) { + do_check_eq(Cr.NS_ERROR_FAILURE, e.result); + } +} + +function test_aggregate_removal() +{ + var msc = getOpenedDatabase(); + msc.removeFunction("test_sas_aggr"); + // Should be Ok now + msc.createAggregateFunction("test_sas_aggr", 1, testSquareAndSumFunction); +} + +function test_aggregate_no_aliases() +{ + var msc = getOpenedDatabase(); + try { + msc.createAggregateFunction("test_sas_aggr2", 1, testSquareAndSumFunction); + do_throw("We shouldn't get here!"); + } catch (e) { + do_check_eq(Cr.NS_ERROR_FAILURE, e.result); + } +} + +function test_aggregate_call() +{ + var stmt = createStatement("SELECT test_sas_aggr(id) FROM function_tests"); + while (stmt.executeStep()) { + // Do nothing. + } + do_check_eq(testNums.length, testSquareAndSumFunction.calls); + testSquareAndSumFunction.reset(); + stmt.finalize(); +} + +function test_aggregate_result() +{ + var sas = 0; + for (var i = 0; i < testNums.length; ++i) { + sas += testNums[i] * testNums[i]; + } + var stmt = createStatement("SELECT test_sas_aggr(id) FROM function_tests"); + stmt.executeStep(); + do_check_eq(sas, stmt.getInt32(0)); + testSquareAndSumFunction.reset(); + stmt.finalize(); +} + +var tests = [test_aggregate_registration, test_aggregate_no_double_registration, + test_aggregate_removal, test_aggregate_no_aliases, test_aggregate_call, + test_aggregate_result]; + +function run_test() +{ + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_storage_connection.js b/storage/test/unit/test_storage_connection.js new file mode 100644 index 000000000..ce98d0891 --- /dev/null +++ b/storage/test/unit/test_storage_connection.js @@ -0,0 +1,763 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the functions of mozIStorageConnection + +// Test Functions + +add_task(function* test_connectionReady_open() { + // there doesn't seem to be a way for the connection to not be ready (unless + // we close it with mozIStorageConnection::Close(), but we don't for this). + // It can only fail if GetPath fails on the database file, or if we run out + // of memory trying to use an in-memory database + + var msc = getOpenedDatabase(); + do_check_true(msc.connectionReady); +}); + +add_task(function* test_connectionReady_closed() { + // This also tests mozIStorageConnection::Close() + + var msc = getOpenedDatabase(); + msc.close(); + do_check_false(msc.connectionReady); + gDBConn = null; // this is so later tests don't start to fail. +}); + +add_task(function* test_databaseFile() { + var msc = getOpenedDatabase(); + do_check_true(getTestDB().equals(msc.databaseFile)); +}); + +add_task(function* test_tableExists_not_created() { + var msc = getOpenedDatabase(); + do_check_false(msc.tableExists("foo")); +}); + +add_task(function* test_indexExists_not_created() { + var msc = getOpenedDatabase(); + do_check_false(msc.indexExists("foo")); +}); + +add_task(function* test_temp_tableExists_and_indexExists() { + var msc = getOpenedDatabase(); + msc.executeSimpleSQL("CREATE TEMP TABLE test_temp(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)"); + do_check_true(msc.tableExists("test_temp")); + + msc.executeSimpleSQL("CREATE INDEX test_temp_ind ON test_temp (name)"); + do_check_true(msc.indexExists("test_temp_ind")); + + msc.executeSimpleSQL("DROP INDEX test_temp_ind"); + msc.executeSimpleSQL("DROP TABLE test_temp"); +}); + +add_task(function* test_createTable_not_created() { + var msc = getOpenedDatabase(); + msc.createTable("test", "id INTEGER PRIMARY KEY, name TEXT"); + do_check_true(msc.tableExists("test")); +}); + +add_task(function* test_indexExists_created() { + var msc = getOpenedDatabase(); + msc.executeSimpleSQL("CREATE INDEX name_ind ON test (name)"); + do_check_true(msc.indexExists("name_ind")); +}); + +add_task(function* test_createTable_already_created() { + var msc = getOpenedDatabase(); + do_check_true(msc.tableExists("test")); + Assert.throws(() => msc.createTable("test", "id INTEGER PRIMARY KEY, name TEXT"), + /NS_ERROR_FAILURE/); +}); + +add_task(function* test_attach_createTable_tableExists_indexExists() { + var msc = getOpenedDatabase(); + var file = do_get_file("storage_attach.sqlite", true); + var msc2 = getDatabase(file); + msc.executeSimpleSQL("ATTACH DATABASE '" + file.path + "' AS sample"); + + do_check_false(msc.tableExists("sample.test")); + msc.createTable("sample.test", "id INTEGER PRIMARY KEY, name TEXT"); + do_check_true(msc.tableExists("sample.test")); + Assert.throws(() => msc.createTable("sample.test", "id INTEGER PRIMARY KEY, name TEXT"), + /NS_ERROR_FAILURE/); + + do_check_false(msc.indexExists("sample.test_ind")); + msc.executeSimpleSQL("CREATE INDEX sample.test_ind ON test (name)"); + do_check_true(msc.indexExists("sample.test_ind")); + + msc.executeSimpleSQL("DETACH DATABASE sample"); + msc2.close(); + try { + file.remove(false); + } catch (e) { + // Do nothing. + } +}); + +add_task(function* test_lastInsertRowID() { + var msc = getOpenedDatabase(); + msc.executeSimpleSQL("INSERT INTO test (name) VALUES ('foo')"); + do_check_eq(1, msc.lastInsertRowID); +}); + +add_task(function* test_transactionInProgress_no() { + var msc = getOpenedDatabase(); + do_check_false(msc.transactionInProgress); +}); + +add_task(function* test_transactionInProgress_yes() { + var msc = getOpenedDatabase(); + msc.beginTransaction(); + do_check_true(msc.transactionInProgress); + msc.commitTransaction(); + do_check_false(msc.transactionInProgress); + + msc.beginTransaction(); + do_check_true(msc.transactionInProgress); + msc.rollbackTransaction(); + do_check_false(msc.transactionInProgress); +}); + +add_task(function* test_commitTransaction_no_transaction() { + var msc = getOpenedDatabase(); + do_check_false(msc.transactionInProgress); + Assert.throws(() => msc.commitTransaction(), /NS_ERROR_UNEXPECTED/); +}); + +add_task(function* test_rollbackTransaction_no_transaction() { + var msc = getOpenedDatabase(); + do_check_false(msc.transactionInProgress); + Assert.throws(() => msc.rollbackTransaction(), /NS_ERROR_UNEXPECTED/); +}); + +add_task(function* test_get_schemaVersion_not_set() { + do_check_eq(0, getOpenedDatabase().schemaVersion); +}); + +add_task(function* test_set_schemaVersion() { + var msc = getOpenedDatabase(); + const version = 1; + msc.schemaVersion = version; + do_check_eq(version, msc.schemaVersion); +}); + +add_task(function* test_set_schemaVersion_same() { + var msc = getOpenedDatabase(); + const version = 1; + msc.schemaVersion = version; // should still work ok + do_check_eq(version, msc.schemaVersion); +}); + +add_task(function* test_set_schemaVersion_negative() { + var msc = getOpenedDatabase(); + const version = -1; + msc.schemaVersion = version; + do_check_eq(version, msc.schemaVersion); +}); + +add_task(function* test_createTable() { + var temp = getTestDB().parent; + temp.append("test_db_table"); + try { + var con = getService().openDatabase(temp); + con.createTable("a", ""); + } catch (e) { + if (temp.exists()) { + try { + temp.remove(false); + } catch (e2) { + // Do nothing. + } + } + do_check_true(e.result == Cr.NS_ERROR_NOT_INITIALIZED || + e.result == Cr.NS_ERROR_FAILURE); + } finally { + if (con) { + con.close(); + } + } +}); + +add_task(function* test_defaultSynchronousAtNormal() { + var msc = getOpenedDatabase(); + var stmt = createStatement("PRAGMA synchronous;"); + try { + stmt.executeStep(); + do_check_eq(1, stmt.getInt32(0)); + } + finally { + stmt.reset(); + stmt.finalize(); + } +}); + +// must be ran before executeAsync tests +add_task(function* test_close_does_not_spin_event_loop() { + // We want to make sure that the event loop on the calling thread does not + // spin when close is called. + let event = { + ran: false, + run() { + this.ran = true; + }, + }; + + // Post the event before we call close, so it would run if the event loop was + // spun during close. + let thread = Cc["@mozilla.org/thread-manager;1"]. + getService(Ci.nsIThreadManager). + currentThread; + thread.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL); + + // Sanity check, then close the database. Afterwards, we should not have ran! + do_check_false(event.ran); + getOpenedDatabase().close(); + do_check_false(event.ran); + + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; +}); + +add_task(function* test_asyncClose_succeeds_with_finalized_async_statement() { + // XXX this test isn't perfect since we can't totally control when events will + // run. If this paticular function fails randomly, it means we have a + // real bug. + + // We want to make sure we create a cached async statement to make sure that + // when we finalize our statement, we end up finalizing the async one too so + // close will succeed. + let stmt = createStatement("SELECT * FROM test"); + stmt.executeAsync(); + stmt.finalize(); + + yield asyncClose(getOpenedDatabase()); + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; +}); + +add_task(function* test_close_then_release_statement() { + // Testing the behavior in presence of a bad client that finalizes + // statements after the database has been closed (typically by + // letting the gc finalize the statement). + let db = getOpenedDatabase(); + let stmt = createStatement("SELECT * FROM test -- test_close_then_release_statement"); + db.close(); + stmt.finalize(); // Finalize too late - this should not crash + + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; +}); + +add_task(function* test_asyncClose_then_release_statement() { + // Testing the behavior in presence of a bad client that finalizes + // statements after the database has been async closed (typically by + // letting the gc finalize the statement). + let db = getOpenedDatabase(); + let stmt = createStatement("SELECT * FROM test -- test_asyncClose_then_release_statement"); + yield asyncClose(db); + stmt.finalize(); // Finalize too late - this should not crash + + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; +}); + +add_task(function* test_close_fails_with_async_statement_ran() { + let deferred = Promise.defer(); + let stmt = createStatement("SELECT * FROM test"); + stmt.executeAsync(); + stmt.finalize(); + + let db = getOpenedDatabase(); + Assert.throws(() => db.close(), /NS_ERROR_UNEXPECTED/); + + // Clean up after ourselves. + db.asyncClose(function () { + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; + deferred.resolve(); + }); + + yield deferred.promise; +}); + +add_task(function* test_clone_optional_param() { + let db1 = getService().openUnsharedDatabase(getTestDB()); + let db2 = db1.clone(); + do_check_true(db2.connectionReady); + + // A write statement should not fail here. + let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)"); + stmt.params.name = "dwitte"; + stmt.execute(); + stmt.finalize(); + + // And a read statement should succeed. + stmt = db2.createStatement("SELECT * FROM test"); + do_check_true(stmt.executeStep()); + stmt.finalize(); + + // Additionally check that it is a connection on the same database. + do_check_true(db1.databaseFile.equals(db2.databaseFile)); + + db1.close(); + db2.close(); +}); + +function* standardAsyncTest(promisedDB, name, shouldInit = false) { + do_print("Performing standard async test " + name); + + let adb = yield promisedDB; + do_check_true(adb instanceof Ci.mozIStorageAsyncConnection); + do_check_false(adb instanceof Ci.mozIStorageConnection); + + if (shouldInit) { + let stmt = adb.createAsyncStatement("CREATE TABLE test(name TEXT)"); + yield executeAsync(stmt); + stmt.finalize(); + } + + // Generate a name to insert and fetch back + name = "worker bee " + Math.random() + " (" + name + ")"; + + let stmt = adb.createAsyncStatement("INSERT INTO test (name) VALUES (:name)"); + stmt.params.name = name; + let result = yield executeAsync(stmt); + do_print("Request complete"); + stmt.finalize(); + do_check_true(Components.isSuccessCode(result)); + do_print("Extracting data"); + stmt = adb.createAsyncStatement("SELECT * FROM test"); + let found = false; + yield executeAsync(stmt, function (results) { + do_print("Data has been extracted"); + for (let row = results.getNextRow(); row != null; row = results.getNextRow()) { + if (row.getResultByName("name") == name) { + found = true; + break; + } + } + }); + do_check_true(found); + stmt.finalize(); + yield asyncClose(adb); + + do_print("Standard async test " + name + " complete"); +} + +add_task(function* test_open_async() { + yield standardAsyncTest(openAsyncDatabase(getTestDB(), null), "default"); + yield standardAsyncTest(openAsyncDatabase(getTestDB()), "no optional arg"); + yield standardAsyncTest(openAsyncDatabase(getTestDB(), + {shared: false, growthIncrement: 54}), "non-default options"); + yield standardAsyncTest(openAsyncDatabase("memory"), + "in-memory database", true); + yield standardAsyncTest(openAsyncDatabase("memory", + {shared: false}), + "in-memory database and options", true); + + do_print("Testing async opening with bogus options 0"); + let raised = false; + let adb = null; + + try { + adb = yield openAsyncDatabase("memory", {shared: false, growthIncrement: 54}); + } catch (ex) { + raised = true; + } finally { + if (adb) { + yield asyncClose(adb); + } + } + do_check_true(raised); + + do_print("Testing async opening with bogus options 1"); + raised = false; + adb = null; + try { + adb = yield openAsyncDatabase(getTestDB(), {shared: "forty-two"}); + } catch (ex) { + raised = true; + } finally { + if (adb) { + yield asyncClose(adb); + } + } + do_check_true(raised); + + do_print("Testing async opening with bogus options 2"); + raised = false; + adb = null; + try { + adb = yield openAsyncDatabase(getTestDB(), {growthIncrement: "forty-two"}); + } catch (ex) { + raised = true; + } finally { + if (adb) { + yield asyncClose(adb); + } + } + do_check_true(raised); +}); + + +add_task(function* test_async_open_with_shared_cache() { + do_print("Testing that opening with a shared cache doesn't break stuff"); + let adb = yield openAsyncDatabase(getTestDB(), {shared: true}); + + let stmt = adb.createAsyncStatement("INSERT INTO test (name) VALUES (:name)"); + stmt.params.name = "clockworker"; + let result = yield executeAsync(stmt); + do_print("Request complete"); + stmt.finalize(); + do_check_true(Components.isSuccessCode(result)); + do_print("Extracting data"); + stmt = adb.createAsyncStatement("SELECT * FROM test"); + let found = false; + yield executeAsync(stmt, function (results) { + do_print("Data has been extracted"); + for (let row = results.getNextRow(); row != null; row = results.getNextRow()) { + if (row.getResultByName("name") == "clockworker") { + found = true; + break; + } + } + }); + do_check_true(found); + stmt.finalize(); + yield asyncClose(adb); +}); + +add_task(function* test_clone_trivial_async() { + do_print("Open connection"); + let db = getService().openDatabase(getTestDB()); + do_check_true(db instanceof Ci.mozIStorageAsyncConnection); + do_print("AsyncClone connection"); + let clone = yield asyncClone(db, true); + do_check_true(clone instanceof Ci.mozIStorageAsyncConnection); + do_print("Close connection"); + yield asyncClose(db); + do_print("Close clone"); + yield asyncClose(clone); +}); + +add_task(function* test_clone_no_optional_param_async() { + "use strict"; + do_print("Testing async cloning"); + let adb1 = yield openAsyncDatabase(getTestDB(), null); + do_check_true(adb1 instanceof Ci.mozIStorageAsyncConnection); + + do_print("Cloning database"); + + let adb2 = yield asyncClone(adb1); + do_print("Testing that the cloned db is a mozIStorageAsyncConnection " + + "and not a mozIStorageConnection"); + do_check_true(adb2 instanceof Ci.mozIStorageAsyncConnection); + do_check_false(adb2 instanceof Ci.mozIStorageConnection); + + do_print("Inserting data into source db"); + let stmt = adb1. + createAsyncStatement("INSERT INTO test (name) VALUES (:name)"); + + stmt.params.name = "yoric"; + let result = yield executeAsync(stmt); + do_print("Request complete"); + stmt.finalize(); + do_check_true(Components.isSuccessCode(result)); + do_print("Extracting data from clone db"); + stmt = adb2.createAsyncStatement("SELECT * FROM test"); + let found = false; + yield executeAsync(stmt, function (results) { + do_print("Data has been extracted"); + for (let row = results.getNextRow(); row != null; row = results.getNextRow()) { + if (row.getResultByName("name") == "yoric") { + found = true; + break; + } + } + }); + do_check_true(found); + stmt.finalize(); + do_print("Closing databases"); + yield asyncClose(adb2); + do_print("First db closed"); + + yield asyncClose(adb1); + do_print("Second db closed"); +}); + +add_task(function* test_clone_readonly() { + let db1 = getService().openUnsharedDatabase(getTestDB()); + let db2 = db1.clone(true); + do_check_true(db2.connectionReady); + + // A write statement should fail here. + let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)"); + stmt.params.name = "reed"; + expectError(Cr.NS_ERROR_FILE_READ_ONLY, () => stmt.execute()); + stmt.finalize(); + + // And a read statement should succeed. + stmt = db2.createStatement("SELECT * FROM test"); + do_check_true(stmt.executeStep()); + stmt.finalize(); + + db1.close(); + db2.close(); +}); + +add_task(function* test_clone_shared_readonly() { + let db1 = getService().openDatabase(getTestDB()); + let db2 = db1.clone(true); + do_check_true(db2.connectionReady); + + let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)"); + stmt.params.name = "parker"; + // TODO currently SQLite does not actually work correctly here. The behavior + // we want is commented out, and the current behavior is being tested + // for. Our IDL comments will have to be updated when this starts to + // work again. + stmt.execute(); + // expectError(Components.results.NS_ERROR_FILE_READ_ONLY, () => stmt.execute()); + stmt.finalize(); + + // And a read statement should succeed. + stmt = db2.createStatement("SELECT * FROM test"); + do_check_true(stmt.executeStep()); + stmt.finalize(); + + db1.close(); + db2.close(); +}); + +add_task(function* test_close_clone_fails() { + let calls = [ + "openDatabase", + "openUnsharedDatabase", + ]; + calls.forEach(function (methodName) { + let db = getService()[methodName](getTestDB()); + db.close(); + expectError(Cr.NS_ERROR_NOT_INITIALIZED, () => db.clone()); + }); +}); + +add_task(function* test_memory_clone_fails() { + let db = getService().openSpecialDatabase("memory"); + db.close(); + expectError(Cr.NS_ERROR_NOT_INITIALIZED, () => db.clone()); +}); + +add_task(function* test_clone_copies_functions() { + const FUNC_NAME = "test_func"; + let calls = [ + "openDatabase", + "openUnsharedDatabase", + ]; + let functionMethods = [ + "createFunction", + "createAggregateFunction", + ]; + calls.forEach(function (methodName) { + [true, false].forEach(function (readOnly) { + functionMethods.forEach(function (functionMethod) { + let db1 = getService()[methodName](getTestDB()); + // Create a function for db1. + db1[functionMethod](FUNC_NAME, 1, { + onFunctionCall: () => 0, + onStep: () => 0, + onFinal: () => 0, + }); + + // Clone it, and make sure the function exists still. + let db2 = db1.clone(readOnly); + // Note: this would fail if the function did not exist. + let stmt = db2.createStatement("SELECT " + FUNC_NAME + "(id) FROM test"); + stmt.finalize(); + db1.close(); + db2.close(); + }); + }); + }); +}); + +add_task(function* test_clone_copies_overridden_functions() { + const FUNC_NAME = "lower"; + function test_func() { + this.called = false; + } + test_func.prototype = { + onFunctionCall() { + this.called = true; + }, + onStep() { + this.called = true; + }, + onFinal: () => 0, + }; + + let calls = [ + "openDatabase", + "openUnsharedDatabase", + ]; + let functionMethods = [ + "createFunction", + "createAggregateFunction", + ]; + calls.forEach(function (methodName) { + [true, false].forEach(function (readOnly) { + functionMethods.forEach(function (functionMethod) { + let db1 = getService()[methodName](getTestDB()); + // Create a function for db1. + let func = new test_func(); + db1[functionMethod](FUNC_NAME, 1, func); + do_check_false(func.called); + + // Clone it, and make sure the function gets called. + let db2 = db1.clone(readOnly); + let stmt = db2.createStatement("SELECT " + FUNC_NAME + "(id) FROM test"); + stmt.executeStep(); + do_check_true(func.called); + stmt.finalize(); + db1.close(); + db2.close(); + }); + }); + }); +}); + +add_task(function* test_clone_copies_pragmas() { + const PRAGMAS = [ + { name: "cache_size", value: 500, copied: true }, + { name: "temp_store", value: 2, copied: true }, + { name: "foreign_keys", value: 1, copied: true }, + { name: "journal_size_limit", value: 524288, copied: true }, + { name: "synchronous", value: 2, copied: true }, + { name: "wal_autocheckpoint", value: 16, copied: true }, + { name: "busy_timeout", value: 50, copied: true }, + { name: "ignore_check_constraints", value: 1, copied: false }, + ]; + + let db1 = getService().openUnsharedDatabase(getTestDB()); + + // Sanity check initial values are different from enforced ones. + PRAGMAS.forEach(function (pragma) { + let stmt = db1.createStatement("PRAGMA " + pragma.name); + do_check_true(stmt.executeStep()); + do_check_neq(pragma.value, stmt.getInt32(0)); + stmt.finalize(); + }); + // Execute pragmas. + PRAGMAS.forEach(function (pragma) { + db1.executeSimpleSQL("PRAGMA " + pragma.name + " = " + pragma.value); + }); + + let db2 = db1.clone(); + do_check_true(db2.connectionReady); + + // Check cloned connection inherited pragma values. + PRAGMAS.forEach(function (pragma) { + let stmt = db2.createStatement("PRAGMA " + pragma.name); + do_check_true(stmt.executeStep()); + let validate = pragma.copied ? do_check_eq : do_check_neq; + validate(pragma.value, stmt.getInt32(0)); + stmt.finalize(); + }); + + db1.close(); + db2.close(); +}); + +add_task(function* test_readonly_clone_copies_pragmas() { + const PRAGMAS = [ + { name: "cache_size", value: 500, copied: true }, + { name: "temp_store", value: 2, copied: true }, + { name: "foreign_keys", value: 1, copied: false }, + { name: "journal_size_limit", value: 524288, copied: false }, + { name: "synchronous", value: 2, copied: false }, + { name: "wal_autocheckpoint", value: 16, copied: false }, + { name: "busy_timeout", value: 50, copied: false }, + { name: "ignore_check_constraints", value: 1, copied: false }, + ]; + + let db1 = getService().openUnsharedDatabase(getTestDB()); + + // Sanity check initial values are different from enforced ones. + PRAGMAS.forEach(function (pragma) { + let stmt = db1.createStatement("PRAGMA " + pragma.name); + do_check_true(stmt.executeStep()); + do_check_neq(pragma.value, stmt.getInt32(0)); + stmt.finalize(); + }); + // Execute pragmas. + PRAGMAS.forEach(function (pragma) { + db1.executeSimpleSQL("PRAGMA " + pragma.name + " = " + pragma.value); + }); + + let db2 = db1.clone(true); + do_check_true(db2.connectionReady); + + // Check cloned connection inherited pragma values. + PRAGMAS.forEach(function (pragma) { + let stmt = db2.createStatement("PRAGMA " + pragma.name); + do_check_true(stmt.executeStep()); + let validate = pragma.copied ? do_check_eq : do_check_neq; + validate(pragma.value, stmt.getInt32(0)); + stmt.finalize(); + }); + + db1.close(); + db2.close(); +}); + +add_task(function* test_clone_attach_database() { + let db1 = getService().openUnsharedDatabase(getTestDB()); + + let c = 0; + function attachDB(conn, name) { + let file = dirSvc.get("ProfD", Ci.nsIFile); + file.append("test_storage_" + (++c) + ".sqlite"); + let db = getService().openUnsharedDatabase(file); + conn.executeSimpleSQL(`ATTACH DATABASE '${db.databaseFile.path}' AS ${name}`); + db.close(); + } + attachDB(db1, "attached_1"); + attachDB(db1, "attached_2"); + + // These should not throw. + db1.createStatement("SELECT * FROM attached_1.sqlite_master"); + db1.createStatement("SELECT * FROM attached_2.sqlite_master"); + + // R/W clone. + let db2 = db1.clone(); + do_check_true(db2.connectionReady); + + // These should not throw. + db2.createStatement("SELECT * FROM attached_1.sqlite_master"); + db2.createStatement("SELECT * FROM attached_2.sqlite_master"); + + // R/O clone. + let db3 = db1.clone(true); + do_check_true(db3.connectionReady); + + // These should not throw. + db3.createStatement("SELECT * FROM attached_1.sqlite_master"); + db3.createStatement("SELECT * FROM attached_2.sqlite_master"); + + db1.close(); + db2.close(); + db3.close(); +}); + +add_task(function* test_getInterface() { + let db = getOpenedDatabase(); + let target = db.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEventTarget); + // Just check that target is non-null. Other tests will ensure that it has + // the correct value. + do_check_true(target != null); + + yield asyncClose(db); + gDBConn = null; +}); diff --git a/storage/test/unit/test_storage_fulltextindex.js b/storage/test/unit/test_storage_fulltextindex.js new file mode 100644 index 000000000..1f7614067 --- /dev/null +++ b/storage/test/unit/test_storage_fulltextindex.js @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests support for the fts3 (full-text index) module. + +// Example statements in these tests are taken from the Full Text Index page +// on the SQLite wiki: http://www.sqlite.org/cvstrac/wiki?p=FullTextIndex + +function test_table_creation() +{ + var msc = getOpenedUnsharedDatabase(); + + msc.executeSimpleSQL( + "CREATE VIRTUAL TABLE recipe USING fts3(name, ingredients)"); + + do_check_true(msc.tableExists("recipe")); +} + +function test_insertion() +{ + var msc = getOpenedUnsharedDatabase(); + + msc.executeSimpleSQL("INSERT INTO recipe (name, ingredients) VALUES " + + "('broccoli stew', 'broccoli peppers cheese tomatoes')"); + msc.executeSimpleSQL("INSERT INTO recipe (name, ingredients) VALUES " + + "('pumpkin stew', 'pumpkin onions garlic celery')"); + msc.executeSimpleSQL("INSERT INTO recipe (name, ingredients) VALUES " + + "('broccoli pie', 'broccoli cheese onions flour')"); + msc.executeSimpleSQL("INSERT INTO recipe (name, ingredients) VALUES " + + "('pumpkin pie', 'pumpkin sugar flour butter')"); + + var stmt = msc.createStatement("SELECT COUNT(*) FROM recipe"); + stmt.executeStep(); + + do_check_eq(stmt.getInt32(0), 4); + + stmt.reset(); + stmt.finalize(); +} + +function test_selection() +{ + var msc = getOpenedUnsharedDatabase(); + + var stmt = msc.createStatement( + "SELECT rowid, name, ingredients FROM recipe WHERE name MATCH 'pie'"); + + do_check_true(stmt.executeStep()); + do_check_eq(stmt.getInt32(0), 3); + do_check_eq(stmt.getString(1), "broccoli pie"); + do_check_eq(stmt.getString(2), "broccoli cheese onions flour"); + + do_check_true(stmt.executeStep()); + do_check_eq(stmt.getInt32(0), 4); + do_check_eq(stmt.getString(1), "pumpkin pie"); + do_check_eq(stmt.getString(2), "pumpkin sugar flour butter"); + + do_check_false(stmt.executeStep()); + + stmt.reset(); + stmt.finalize(); +} + +var tests = [test_table_creation, test_insertion, test_selection]; + +function run_test() +{ + // It's extra important to start from scratch, since these tests won't work + // with an existing shared cache connection, so we do it even though the last + // test probably did it already. + cleanup(); + + try { + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + } + // It's extra important to clean up afterwards, since later tests that use + // a shared cache connection will not be able to read the database we create, + // so we do this in a finally block to ensure it happens even if some of our + // tests fail. + finally { + cleanup(); + } +} diff --git a/storage/test/unit/test_storage_function.js b/storage/test/unit/test_storage_function.js new file mode 100644 index 000000000..6532709ec --- /dev/null +++ b/storage/test/unit/test_storage_function.js @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the custom functions + +var testNums = [1, 2, 3, 4]; + +function setup() +{ + getOpenedDatabase().createTable("function_tests", "id INTEGER PRIMARY KEY"); + + var stmt = createStatement("INSERT INTO function_tests (id) VALUES(?1)"); + for (let i = 0; i < testNums.length; ++i) { + stmt.bindByIndex(0, testNums[i]); + stmt.execute(); + } + stmt.reset(); + stmt.finalize(); +} + +var testSquareFunction = { + calls: 0, + + onFunctionCall(val) { + ++this.calls; + return val.getInt32(0) * val.getInt32(0); + } +}; + +function test_function_registration() +{ + var msc = getOpenedDatabase(); + msc.createFunction("test_square", 1, testSquareFunction); +} + +function test_function_no_double_registration() +{ + var msc = getOpenedDatabase(); + try { + msc.createFunction("test_square", 2, testSquareFunction); + do_throw("We shouldn't get here!"); + } catch (e) { + do_check_eq(Cr.NS_ERROR_FAILURE, e.result); + } +} + +function test_function_removal() +{ + var msc = getOpenedDatabase(); + msc.removeFunction("test_square"); + // Should be Ok now + msc.createFunction("test_square", 1, testSquareFunction); +} + +function test_function_aliases() +{ + var msc = getOpenedDatabase(); + msc.createFunction("test_square2", 1, testSquareFunction); +} + +function test_function_call() +{ + var stmt = createStatement("SELECT test_square(id) FROM function_tests"); + while (stmt.executeStep()) { + // Do nothing. + } + do_check_eq(testNums.length, testSquareFunction.calls); + testSquareFunction.calls = 0; + stmt.finalize(); +} + +function test_function_result() +{ + var stmt = createStatement("SELECT test_square(42) FROM function_tests"); + stmt.executeStep(); + do_check_eq(42 * 42, stmt.getInt32(0)); + testSquareFunction.calls = 0; + stmt.finalize(); +} + +var tests = [test_function_registration, test_function_no_double_registration, + test_function_removal, test_function_aliases, test_function_call, + test_function_result]; + +function run_test() +{ + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_storage_progresshandler.js b/storage/test/unit/test_storage_progresshandler.js new file mode 100644 index 000000000..c06a57e83 --- /dev/null +++ b/storage/test/unit/test_storage_progresshandler.js @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the custom progress handlers + +function setup() +{ + var msc = getOpenedDatabase(); + msc.createTable("handler_tests", "id INTEGER PRIMARY KEY, num INTEGER"); + msc.beginTransaction(); + + var stmt = createStatement("INSERT INTO handler_tests (id, num) VALUES(?1, ?2)"); + for (let i = 0; i < 100; ++i) { + stmt.bindByIndex(0, i); + stmt.bindByIndex(1, Math.floor(Math.random() * 1000)); + stmt.execute(); + } + stmt.reset(); + msc.commitTransaction(); + stmt.finalize(); +} + +var testProgressHandler = { + calls: 0, + abort: false, + + onProgress(comm) { + ++this.calls; + return this.abort; + } +}; + +function test_handler_registration() +{ + var msc = getOpenedDatabase(); + msc.setProgressHandler(10, testProgressHandler); +} + +function test_handler_return() +{ + var msc = getOpenedDatabase(); + var oldH = msc.setProgressHandler(5, testProgressHandler); + do_check_true(oldH instanceof Ci.mozIStorageProgressHandler); +} + +function test_handler_removal() +{ + var msc = getOpenedDatabase(); + msc.removeProgressHandler(); + var oldH = msc.removeProgressHandler(); + do_check_eq(oldH, null); +} + +function test_handler_call() +{ + var msc = getOpenedDatabase(); + msc.setProgressHandler(50, testProgressHandler); + // Some long-executing request + var stmt = createStatement( + "SELECT SUM(t1.num * t2.num) FROM handler_tests AS t1, handler_tests AS t2"); + while (stmt.executeStep()) { + // Do nothing. + } + do_check_true(testProgressHandler.calls > 0); + stmt.finalize(); +} + +function test_handler_abort() +{ + var msc = getOpenedDatabase(); + testProgressHandler.abort = true; + msc.setProgressHandler(50, testProgressHandler); + // Some long-executing request + var stmt = createStatement( + "SELECT SUM(t1.num * t2.num) FROM handler_tests AS t1, handler_tests AS t2"); + + const SQLITE_INTERRUPT = 9; + try { + while (stmt.executeStep()) { + // Do nothing. + } + do_throw("We shouldn't get here!"); + } catch (e) { + do_check_eq(Cr.NS_ERROR_ABORT, e.result); + do_check_eq(SQLITE_INTERRUPT, msc.lastError); + } + try { + stmt.finalize(); + do_throw("We shouldn't get here!"); + } catch (e) { + // finalize should return the error code since we encountered an error + do_check_eq(Cr.NS_ERROR_ABORT, e.result); + do_check_eq(SQLITE_INTERRUPT, msc.lastError); + } +} + +var tests = [test_handler_registration, test_handler_return, + test_handler_removal, test_handler_call, + test_handler_abort]; + +function run_test() +{ + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_storage_service.js b/storage/test/unit/test_storage_service.js new file mode 100644 index 000000000..9cf46620e --- /dev/null +++ b/storage/test/unit/test_storage_service.js @@ -0,0 +1,142 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the functions of mozIStorageService except for +// openUnsharedDatabase, which is tested by test_storage_service_unshared.js. + +const BACKUP_FILE_NAME = "test_storage.sqlite.backup"; + +function test_openSpecialDatabase_invalid_arg() +{ + try { + getService().openSpecialDatabase("abcd"); + do_throw("We should not get here!"); + } catch (e) { + print(e); + print("e.result is " + e.result); + do_check_eq(Cr.NS_ERROR_INVALID_ARG, e.result); + } +} + +function test_openDatabase_null_file() +{ + try { + getService().openDatabase(null); + do_throw("We should not get here!"); + } catch (e) { + print(e); + print("e.result is " + e.result); + do_check_eq(Cr.NS_ERROR_INVALID_ARG, e.result); + } +} + +function test_openUnsharedDatabase_null_file() +{ + try { + getService().openUnsharedDatabase(null); + do_throw("We should not get here!"); + } catch (e) { + print(e); + print("e.result is " + e.result); + do_check_eq(Cr.NS_ERROR_INVALID_ARG, e.result); + } +} + +function test_openDatabase_file_DNE() +{ + // the file should be created after calling + var db = getTestDB(); + do_check_false(db.exists()); + getService().openDatabase(db); + do_check_true(db.exists()); +} + +function test_openDatabase_file_exists() +{ + // it should already exist from our last test + var db = getTestDB(); + do_check_true(db.exists()); + getService().openDatabase(db); + do_check_true(db.exists()); +} + +function test_corrupt_db_throws_with_openDatabase() +{ + try { + getDatabase(getCorruptDB()); + do_throw("should not be here"); + } + catch (e) { + do_check_eq(Cr.NS_ERROR_FILE_CORRUPTED, e.result); + } +} + +function test_fake_db_throws_with_openDatabase() +{ + try { + getDatabase(getFakeDB()); + do_throw("should not be here"); + } + catch (e) { + do_check_eq(Cr.NS_ERROR_FILE_CORRUPTED, e.result); + } +} + +function test_backup_not_new_filename() +{ + const fname = getTestDB().leafName; + + var backup = getService().backupDatabaseFile(getTestDB(), fname); + do_check_neq(fname, backup.leafName); + + backup.remove(false); +} + +function test_backup_new_filename() +{ + var backup = getService().backupDatabaseFile(getTestDB(), BACKUP_FILE_NAME); + do_check_eq(BACKUP_FILE_NAME, backup.leafName); + + backup.remove(false); +} + +function test_backup_new_folder() +{ + var parentDir = getTestDB().parent; + parentDir.append("test_storage_temp"); + if (parentDir.exists()) + parentDir.remove(true); + parentDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + do_check_true(parentDir.exists()); + + var backup = getService().backupDatabaseFile(getTestDB(), BACKUP_FILE_NAME, + parentDir); + do_check_eq(BACKUP_FILE_NAME, backup.leafName); + do_check_true(parentDir.equals(backup.parent)); + + parentDir.remove(true); +} + +var tests = [ + test_openSpecialDatabase_invalid_arg, + test_openDatabase_null_file, + test_openUnsharedDatabase_null_file, + test_openDatabase_file_DNE, + test_openDatabase_file_exists, + test_corrupt_db_throws_with_openDatabase, + test_fake_db_throws_with_openDatabase, + test_backup_not_new_filename, + test_backup_new_filename, + test_backup_new_folder, +]; + +function run_test() +{ + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} + diff --git a/storage/test/unit/test_storage_service_unshared.js b/storage/test/unit/test_storage_service_unshared.js new file mode 100644 index 000000000..70efb2a43 --- /dev/null +++ b/storage/test/unit/test_storage_service_unshared.js @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the openUnsharedDatabase function of mozIStorageService. + +function test_openUnsharedDatabase_file_DNE() +{ + // the file should be created after calling + var db = getTestDB(); + do_check_false(db.exists()); + getService().openUnsharedDatabase(db); + do_check_true(db.exists()); +} + +function test_openUnsharedDatabase_file_exists() +{ + // it should already exist from our last test + var db = getTestDB(); + do_check_true(db.exists()); + getService().openUnsharedDatabase(db); + do_check_true(db.exists()); +} + +var tests = [test_openUnsharedDatabase_file_DNE, + test_openUnsharedDatabase_file_exists]; + +function run_test() +{ + for (var i = 0; i < tests.length; i++) + tests[i](); + + cleanup(); +} + diff --git a/storage/test/unit/test_storage_statement.js b/storage/test/unit/test_storage_statement.js new file mode 100644 index 000000000..026e271ac --- /dev/null +++ b/storage/test/unit/test_storage_statement.js @@ -0,0 +1,184 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the functions of mozIStorageStatement + +function setup() +{ + getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, name TEXT"); +} + +function test_parameterCount_none() +{ + var stmt = createStatement("SELECT * FROM test"); + do_check_eq(0, stmt.parameterCount); + stmt.reset(); + stmt.finalize(); +} + +function test_parameterCount_one() +{ + var stmt = createStatement("SELECT * FROM test WHERE id = ?1"); + do_check_eq(1, stmt.parameterCount); + stmt.reset(); + stmt.finalize(); +} + +function test_getParameterName() +{ + var stmt = createStatement("SELECT * FROM test WHERE id = :id"); + do_check_eq(":id", stmt.getParameterName(0)); + stmt.reset(); + stmt.finalize(); +} + +function test_getParameterIndex_different() +{ + var stmt = createStatement("SELECT * FROM test WHERE id = :id OR name = :name"); + do_check_eq(0, stmt.getParameterIndex("id")); + do_check_eq(1, stmt.getParameterIndex("name")); + stmt.reset(); + stmt.finalize(); +} + +function test_getParameterIndex_same() +{ + var stmt = createStatement("SELECT * FROM test WHERE id = :test OR name = :test"); + do_check_eq(0, stmt.getParameterIndex("test")); + stmt.reset(); + stmt.finalize(); +} + +function test_columnCount() +{ + var stmt = createStatement("SELECT * FROM test WHERE id = ?1 OR name = ?2"); + do_check_eq(2, stmt.columnCount); + stmt.reset(); + stmt.finalize(); +} + +function test_getColumnName() +{ + var stmt = createStatement("SELECT name, id FROM test"); + do_check_eq("id", stmt.getColumnName(1)); + do_check_eq("name", stmt.getColumnName(0)); + stmt.reset(); + stmt.finalize(); +} + +function test_getColumnIndex_same_case() +{ + var stmt = createStatement("SELECT name, id FROM test"); + do_check_eq(0, stmt.getColumnIndex("name")); + do_check_eq(1, stmt.getColumnIndex("id")); + stmt.reset(); + stmt.finalize(); +} + +function test_getColumnIndex_different_case() +{ + var stmt = createStatement("SELECT name, id FROM test"); + try { + do_check_eq(0, stmt.getColumnIndex("NaMe")); + do_throw("should not get here"); + } catch (e) { + do_check_eq(Cr.NS_ERROR_INVALID_ARG, e.result); + } + try { + do_check_eq(1, stmt.getColumnIndex("Id")); + do_throw("should not get here"); + } catch (e) { + do_check_eq(Cr.NS_ERROR_INVALID_ARG, e.result); + } + stmt.reset(); + stmt.finalize(); +} + +function test_state_ready() +{ + var stmt = createStatement("SELECT name, id FROM test"); + do_check_eq(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_READY, stmt.state); + stmt.reset(); + stmt.finalize(); +} + +function test_state_executing() +{ + var stmt = createStatement("INSERT INTO test (name) VALUES ('foo')"); + stmt.execute(); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("SELECT name, id FROM test"); + stmt.executeStep(); + do_check_eq(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_EXECUTING, + stmt.state); + stmt.executeStep(); + do_check_eq(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_EXECUTING, + stmt.state); + stmt.reset(); + do_check_eq(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_READY, stmt.state); + stmt.finalize(); +} + +function test_state_after_finalize() +{ + var stmt = createStatement("SELECT name, id FROM test"); + stmt.executeStep(); + stmt.finalize(); + do_check_eq(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_INVALID, stmt.state); +} + +function test_failed_execute() +{ + var stmt = createStatement("INSERT INTO test (name) VALUES ('foo')"); + stmt.execute(); + stmt.finalize(); + var id = getOpenedDatabase().lastInsertRowID; + stmt = createStatement("INSERT INTO test(id, name) VALUES(:id, 'bar')"); + stmt.params.id = id; + try { + // Should throw a constraint error + stmt.execute(); + do_throw("Should have seen a constraint error"); + } + catch (e) { + do_check_eq(getOpenedDatabase().lastError, Ci.mozIStorageError.CONSTRAINT); + } + do_check_eq(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_READY, stmt.state); + // Should succeed without needing to reset the statement manually + stmt.finalize(); +} + +function test_bind_undefined() +{ + var stmt = createStatement("INSERT INTO test (name) VALUES ('foo')"); + + expectError(Cr.NS_ERROR_ILLEGAL_VALUE, + () => stmt.bindParameters(undefined)); + + stmt.finalize(); +} + +var tests = [test_parameterCount_none, test_parameterCount_one, + test_getParameterName, test_getParameterIndex_different, + test_getParameterIndex_same, test_columnCount, + test_getColumnName, test_getColumnIndex_same_case, + test_getColumnIndex_different_case, test_state_ready, + test_state_executing, test_state_after_finalize, + test_failed_execute, + test_bind_undefined, +]; + +function run_test() +{ + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} + diff --git a/storage/test/unit/test_storage_value_array.js b/storage/test/unit/test_storage_value_array.js new file mode 100644 index 000000000..27bd23992 --- /dev/null +++ b/storage/test/unit/test_storage_value_array.js @@ -0,0 +1,182 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the functions of mozIStorageValueArray + +add_task(function* setup() { + getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, name TEXT," + + "number REAL, nuller NULL, blobber BLOB"); + + var stmt = createStatement("INSERT INTO test (name, number, blobber) " + + "VALUES (?1, ?2, ?3)"); + stmt.bindByIndex(0, "foo"); + stmt.bindByIndex(1, 2.34); + stmt.bindBlobByIndex(2, [], 0); + stmt.execute(); + + stmt.bindByIndex(0, ""); + stmt.bindByIndex(1, 1.23); + stmt.bindBlobByIndex(2, [1, 2], 2); + stmt.execute(); + + stmt.reset(); + stmt.finalize(); + + do_register_cleanup(cleanup); +}); + +add_task(function* test_getIsNull_for_null() { + var stmt = createStatement("SELECT nuller, blobber FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + do_check_true(stmt.executeStep()); + + do_check_true(stmt.getIsNull(0)); // null field + do_check_true(stmt.getIsNull(1)); // data is null if size is 0 + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_getIsNull_for_non_null() { + var stmt = createStatement("SELECT name, blobber FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + do_check_true(stmt.executeStep()); + + do_check_false(stmt.getIsNull(0)); + do_check_false(stmt.getIsNull(1)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_value_type_null() { + var stmt = createStatement("SELECT nuller FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + do_check_true(stmt.executeStep()); + + do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_NULL, + stmt.getTypeOfIndex(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_value_type_integer() { + var stmt = createStatement("SELECT id FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + do_check_true(stmt.executeStep()); + + do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER, + stmt.getTypeOfIndex(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_value_type_float() { + var stmt = createStatement("SELECT number FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + do_check_true(stmt.executeStep()); + + do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT, + stmt.getTypeOfIndex(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_value_type_text() { + var stmt = createStatement("SELECT name FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + do_check_true(stmt.executeStep()); + + do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_TEXT, + stmt.getTypeOfIndex(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_value_type_blob() { + var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + do_check_true(stmt.executeStep()); + + do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_BLOB, + stmt.getTypeOfIndex(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_numEntries_one() { + var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + do_check_true(stmt.executeStep()); + + do_check_eq(1, stmt.numEntries); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_numEntries_all() { + var stmt = createStatement("SELECT * FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + do_check_true(stmt.executeStep()); + + do_check_eq(5, stmt.numEntries); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_getInt() { + var stmt = createStatement("SELECT id FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + do_check_true(stmt.executeStep()); + + do_check_eq(2, stmt.getInt32(0)); + do_check_eq(2, stmt.getInt64(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_getDouble() { + var stmt = createStatement("SELECT number FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + do_check_true(stmt.executeStep()); + + do_check_eq(1.23, stmt.getDouble(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_getUTF8String() { + var stmt = createStatement("SELECT name FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + do_check_true(stmt.executeStep()); + + do_check_eq("foo", stmt.getUTF8String(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_getString() { + var stmt = createStatement("SELECT name FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + do_check_true(stmt.executeStep()); + + do_check_eq("", stmt.getString(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_getBlob() { + var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + do_check_true(stmt.executeStep()); + + var count = { value: 0 }; + var arr = { value: null }; + stmt.getBlob(0, count, arr); + do_check_eq(2, count.value); + do_check_eq(1, arr.value[0]); + do_check_eq(2, arr.value[1]); + stmt.reset(); + stmt.finalize(); +}); + + diff --git a/storage/test/unit/test_telemetry_vfs.js b/storage/test/unit/test_telemetry_vfs.js new file mode 100644 index 000000000..0822fe3e7 --- /dev/null +++ b/storage/test/unit/test_telemetry_vfs.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Make sure that there are telemetry entries created by sqlite io + +function run_sql(d, sql) { + var stmt = d.createStatement(sql); + stmt.execute(); + stmt.finalize(); +} + +function new_file(name) +{ + var file = dirSvc.get("ProfD", Ci.nsIFile); + file.append(name); + return file; +} +function run_test() +{ + const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry); + let read_hgram = Telemetry.getHistogramById("MOZ_SQLITE_OTHER_READ_B"); + let old_sum = read_hgram.snapshot().sum; + const file = new_file("telemetry.sqlite"); + var d = getDatabase(file); + run_sql(d, "CREATE TABLE bloat(data varchar)"); + run_sql(d, "DROP TABLE bloat"); + do_check_true(read_hgram.snapshot().sum > old_sum); +} + diff --git a/storage/test/unit/test_unicode.js b/storage/test/unit/test_unicode.js new file mode 100644 index 000000000..7753bbfdb --- /dev/null +++ b/storage/test/unit/test_unicode.js @@ -0,0 +1,83 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file tests the unicode functions that we have added + +const LATIN1_AE = "\xc6"; // "Æ" +const LATIN1_ae = "\xe6"; // "æ" + +add_task(function* setup() { + getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, name TEXT"); + + var stmt = createStatement("INSERT INTO test (name, id) VALUES (?1, ?2)"); + stmt.bindByIndex(0, LATIN1_AE); + stmt.bindByIndex(1, 1); + stmt.execute(); + stmt.bindByIndex(0, "A"); + stmt.bindByIndex(1, 2); + stmt.execute(); + stmt.bindByIndex(0, "b"); + stmt.bindByIndex(1, 3); + stmt.execute(); + stmt.bindByIndex(0, LATIN1_ae); + stmt.bindByIndex(1, 4); + stmt.execute(); + stmt.finalize(); + + do_register_cleanup(cleanup); +}); + +add_task(function* test_upper_ascii() { + var stmt = createStatement("SELECT name, id FROM test WHERE name = upper('a')"); + do_check_true(stmt.executeStep()); + do_check_eq("A", stmt.getString(0)); + do_check_eq(2, stmt.getInt32(1)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_upper_non_ascii() { + var stmt = createStatement("SELECT name, id FROM test WHERE name = upper(?1)"); + stmt.bindByIndex(0, LATIN1_ae); + do_check_true(stmt.executeStep()); + do_check_eq(LATIN1_AE, stmt.getString(0)); + do_check_eq(1, stmt.getInt32(1)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_lower_ascii() { + var stmt = createStatement("SELECT name, id FROM test WHERE name = lower('B')"); + do_check_true(stmt.executeStep()); + do_check_eq("b", stmt.getString(0)); + do_check_eq(3, stmt.getInt32(1)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_lower_non_ascii() { + var stmt = createStatement("SELECT name, id FROM test WHERE name = lower(?1)"); + stmt.bindByIndex(0, LATIN1_AE); + do_check_true(stmt.executeStep()); + do_check_eq(LATIN1_ae, stmt.getString(0)); + do_check_eq(4, stmt.getInt32(1)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(function* test_like_search_different() { + var stmt = createStatement("SELECT COUNT(*) FROM test WHERE name LIKE ?1"); + stmt.bindByIndex(0, LATIN1_AE); + do_check_true(stmt.executeStep()); + do_check_eq(2, stmt.getInt32(0)); + stmt.finalize(); +}); + +add_task(function* test_like_search_same() { + var stmt = createStatement("SELECT COUNT(*) FROM test WHERE name LIKE ?1"); + stmt.bindByIndex(0, LATIN1_ae); + do_check_true(stmt.executeStep()); + do_check_eq(2, stmt.getInt32(0)); + stmt.finalize(); +}); diff --git a/storage/test/unit/test_vacuum.js b/storage/test/unit/test_vacuum.js new file mode 100644 index 000000000..e284f78c7 --- /dev/null +++ b/storage/test/unit/test_vacuum.js @@ -0,0 +1,335 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This file tests the Vacuum Manager. + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +/** + * Loads a test component that will register as a vacuum-participant. + * If other participants are found they will be unregistered, to avoid conflicts + * with the test itself. + */ +function load_test_vacuum_component() +{ + const CATEGORY_NAME = "vacuum-participant"; + + do_load_manifest("vacuumParticipant.manifest"); + + // This is a lazy check, there could be more participants than just this test + // we just mind that the test exists though. + const EXPECTED_ENTRIES = ["vacuumParticipant"]; + let catMan = Cc["@mozilla.org/categorymanager;1"]. + getService(Ci.nsICategoryManager); + let found = false; + let entries = catMan.enumerateCategory(CATEGORY_NAME); + while (entries.hasMoreElements()) { + let entry = entries.getNext().QueryInterface(Ci.nsISupportsCString).data; + print("Check if the found category entry (" + entry + ") is expected."); + if (EXPECTED_ENTRIES.indexOf(entry) != -1) { + print("Check that only one test entry exists."); + do_check_false(found); + found = true; + } + else { + // Temporary unregister other participants for this test. + catMan.deleteCategoryEntry("vacuum-participant", entry, false); + } + } + print("Check the test entry exists."); + do_check_true(found); +} + +/** + * Sends a fake idle-daily notification to the VACUUM Manager. + */ +function synthesize_idle_daily() +{ + let vm = Cc["@mozilla.org/storage/vacuum;1"].getService(Ci.nsIObserver); + vm.observe(null, "idle-daily", null); +} + +/** + * Returns a new nsIFile reference for a profile database. + * @param filename for the database, excluded the .sqlite extension. + */ +function new_db_file(name) +{ + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append(name + ".sqlite"); + return file; +} + +function run_test() +{ + do_test_pending(); + + // Change initial page size. Do it immediately since it would require an + // additional vacuum op to do it later. As a bonus this makes the page size + // change test really fast since it only has to check results. + let conn = getDatabase(new_db_file("testVacuum")); + conn.executeSimpleSQL("PRAGMA page_size = 1024"); + print("Check current page size."); + let stmt = conn.createStatement("PRAGMA page_size"); + try { + while (stmt.executeStep()) { + do_check_eq(stmt.row.page_size, 1024); + } + } + finally { + stmt.finalize(); + } + + load_test_vacuum_component(); + + run_next_test(); +} + +const TESTS = [ + + function test_common_vacuum() + { + print("\n*** Test that a VACUUM correctly happens and all notifications are fired."); + // Wait for VACUUM begin. + let beginVacuumReceived = false; + Services.obs.addObserver(function onVacuum(aSubject, aTopic, aData) { + Services.obs.removeObserver(onVacuum, aTopic); + beginVacuumReceived = true; + }, "test-begin-vacuum", false); + + // Wait for heavy IO notifications. + let heavyIOTaskBeginReceived = false; + let heavyIOTaskEndReceived = false; + Services.obs.addObserver(function onVacuum(aSubject, aTopic, aData) { + if (heavyIOTaskBeginReceived && heavyIOTaskEndReceived) { + Services.obs.removeObserver(onVacuum, aTopic); + } + + if (aData == "vacuum-begin") { + heavyIOTaskBeginReceived = true; + } + else if (aData == "vacuum-end") { + heavyIOTaskEndReceived = true; + } + }, "heavy-io-task", false); + + // Wait for VACUUM end. + Services.obs.addObserver(function onVacuum(aSubject, aTopic, aData) { + Services.obs.removeObserver(onVacuum, aTopic); + print("Check we received onBeginVacuum"); + do_check_true(beginVacuumReceived); + print("Check we received heavy-io-task notifications"); + do_check_true(heavyIOTaskBeginReceived); + do_check_true(heavyIOTaskEndReceived); + print("Received onEndVacuum"); + run_next_test(); + }, "test-end-vacuum", false); + + synthesize_idle_daily(); + }, + + function test_skipped_if_recent_vacuum() + { + print("\n*** Test that a VACUUM is skipped if it was run recently."); + Services.prefs.setIntPref("storage.vacuum.last.testVacuum.sqlite", + parseInt(Date.now() / 1000)); + + // Wait for VACUUM begin. + let vacuumObserver = { + gotNotification: false, + observe: function VO_observe(aSubject, aTopic, aData) { + this.gotNotification = true; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]) + }; + Services.obs.addObserver(vacuumObserver, "test-begin-vacuum", false); + + // Check after a couple seconds that no VACUUM has been run. + do_timeout(2000, function () { + print("Check VACUUM did not run."); + do_check_false(vacuumObserver.gotNotification); + Services.obs.removeObserver(vacuumObserver, "test-begin-vacuum"); + run_next_test(); + }); + + synthesize_idle_daily(); + }, + + function test_page_size_change() + { + print("\n*** Test that a VACUUM changes page_size"); + + // We did setup the database with a small page size, the previous vacuum + // should have updated it. + print("Check that page size was updated."); + let conn = getDatabase(new_db_file("testVacuum")); + let stmt = conn.createStatement("PRAGMA page_size"); + try { + while (stmt.executeStep()) { + do_check_eq(stmt.row.page_size, conn.defaultPageSize); + } + } + finally { + stmt.finalize(); + } + + run_next_test(); + }, + + function test_skipped_optout_vacuum() + { + print("\n*** Test that a VACUUM is skipped if the participant wants to opt-out."); + Services.obs.notifyObservers(null, "test-options", "opt-out"); + + // Wait for VACUUM begin. + let vacuumObserver = { + gotNotification: false, + observe: function VO_observe(aSubject, aTopic, aData) { + this.gotNotification = true; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]) + }; + Services.obs.addObserver(vacuumObserver, "test-begin-vacuum", false); + + // Check after a couple seconds that no VACUUM has been run. + do_timeout(2000, function () { + print("Check VACUUM did not run."); + do_check_false(vacuumObserver.gotNotification); + Services.obs.removeObserver(vacuumObserver, "test-begin-vacuum"); + run_next_test(); + }); + + synthesize_idle_daily(); + }, + + /* Changing page size on WAL is not supported till Bug 634374 is properly fixed. + function test_page_size_change_with_wal() + { + print("\n*** Test that a VACUUM changes page_size with WAL mode"); + Services.obs.notifyObservers(null, "test-options", "wal"); + + // Set a small page size. + let conn = getDatabase(new_db_file("testVacuum2")); + conn.executeSimpleSQL("PRAGMA page_size = 1024"); + let stmt = conn.createStatement("PRAGMA page_size"); + try { + while (stmt.executeStep()) { + do_check_eq(stmt.row.page_size, 1024); + } + } + finally { + stmt.finalize(); + } + + // Use WAL journal mode. + conn.executeSimpleSQL("PRAGMA journal_mode = WAL"); + stmt = conn.createStatement("PRAGMA journal_mode"); + try { + while (stmt.executeStep()) { + do_check_eq(stmt.row.journal_mode, "wal"); + } + } + finally { + stmt.finalize(); + } + + // Wait for VACUUM end. + let vacuumObserver = { + observe: function VO_observe(aSubject, aTopic, aData) { + Services.obs.removeObserver(this, aTopic); + print("Check page size has been updated."); + let stmt = conn.createStatement("PRAGMA page_size"); + try { + while (stmt.executeStep()) { + do_check_eq(stmt.row.page_size, Ci.mozIStorageConnection.DEFAULT_PAGE_SIZE); + } + } + finally { + stmt.finalize(); + } + + print("Check journal mode has been restored."); + stmt = conn.createStatement("PRAGMA journal_mode"); + try { + while (stmt.executeStep()) { + do_check_eq(stmt.row.journal_mode, "wal"); + } + } + finally { + stmt.finalize(); + } + + run_next_test(); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]) + } + Services.obs.addObserver(vacuumObserver, "test-end-vacuum", false); + + synthesize_idle_daily(); + }, + */ + + function test_memory_database_crash() + { + print("\n*** Test that we don't crash trying to vacuum a memory database"); + Services.obs.notifyObservers(null, "test-options", "memory"); + + // Wait for VACUUM begin. + let vacuumObserver = { + gotNotification: false, + observe: function VO_observe(aSubject, aTopic, aData) { + this.gotNotification = true; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]) + }; + Services.obs.addObserver(vacuumObserver, "test-begin-vacuum", false); + + // Check after a couple seconds that no VACUUM has been run. + do_timeout(2000, function () { + print("Check VACUUM did not run."); + do_check_false(vacuumObserver.gotNotification); + Services.obs.removeObserver(vacuumObserver, "test-begin-vacuum"); + run_next_test(); + }); + + synthesize_idle_daily(); + }, + + /* Changing page size on WAL is not supported till Bug 634374 is properly fixed. + function test_wal_restore_fail() + { + print("\n*** Test that a failing WAL restoration notifies failure"); + Services.obs.notifyObservers(null, "test-options", "wal-fail"); + + // Wait for VACUUM end. + let vacuumObserver = { + observe: function VO_observe(aSubject, aTopic, aData) { + Services.obs.removeObserver(vacuumObserver, "test-end-vacuum"); + print("Check WAL restoration failed."); + do_check_false(aData); + run_next_test(); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]) + } + Services.obs.addObserver(vacuumObserver, "test-end-vacuum", false); + + synthesize_idle_daily(); + }, + */ +]; + +function run_next_test() +{ + if (TESTS.length == 0) { + Services.obs.notifyObservers(null, "test-options", "dispose"); + do_test_finished(); + } + else { + // Set last VACUUM to a date in the past. + Services.prefs.setIntPref("storage.vacuum.last.testVacuum.sqlite", + parseInt(Date.now() / 1000 - 31 * 86400)); + do_execute_soon(TESTS.shift()); + } +} diff --git a/storage/test/unit/vacuumParticipant.js b/storage/test/unit/vacuumParticipant.js new file mode 100644 index 000000000..01b980178 --- /dev/null +++ b/storage/test/unit/vacuumParticipant.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This testing component is used in test_vacuum* tests. + +const Cc = Components.classes; +const Ci = Components.interfaces; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +/** + * Returns a new nsIFile reference for a profile database. + * @param filename for the database, excluded the .sqlite extension. + */ +function new_db_file(name) +{ + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append(name + ".sqlite"); + return file; +} + +/** + * Opens and returns a connection to the provided database file. + * @param nsIFile interface to the database file. + */ +function getDatabase(aFile) +{ + return Cc["@mozilla.org/storage/service;1"].getService(Ci.mozIStorageService) + .openDatabase(aFile); +} + +function vacuumParticipant() +{ + this._dbConn = getDatabase(new_db_file("testVacuum")); + Services.obs.addObserver(this, "test-options", false); +} + +vacuumParticipant.prototype = +{ + classDescription: "vacuumParticipant", + classID: Components.ID("{52aa0b22-b82f-4e38-992a-c3675a3355d2}"), + contractID: "@unit.test.com/test-vacuum-participant;1", + + get expectedDatabasePageSize() { + return this._dbConn.defaultPageSize; + }, + get databaseConnection() { + return this._dbConn; + }, + + _grant: true, + onBeginVacuum: function TVP_onBeginVacuum() + { + if (!this._grant) { + this._grant = true; + return false; + } + Services.obs.notifyObservers(null, "test-begin-vacuum", null); + return true; + }, + onEndVacuum: function TVP_EndVacuum(aSucceeded) + { + if (this._stmt) { + this._stmt.finalize(); + } + Services.obs.notifyObservers(null, "test-end-vacuum", aSucceeded); + }, + + observe: function TVP_observe(aSubject, aTopic, aData) + { + if (aData == "opt-out") { + this._grant = false; + } + else if (aData == "wal") { + try { + this._dbConn.close(); + } catch (e) { + // Do nothing. + } + this._dbConn = getDatabase(new_db_file("testVacuum2")); + } + else if (aData == "wal-fail") { + try { + this._dbConn.close(); + } catch (e) { + // Do nothing. + } + this._dbConn = getDatabase(new_db_file("testVacuum3")); + // Use WAL journal mode. + this._dbConn.executeSimpleSQL("PRAGMA journal_mode = WAL"); + // Create a not finalized statement. + this._stmt = this._dbConn.createStatement("SELECT :test"); + this._stmt.params.test = 1; + this._stmt.executeStep(); + } + else if (aData == "memory") { + try { + this._dbConn.asyncClose(); + } catch (e) { + // Do nothing. + } + this._dbConn = Cc["@mozilla.org/storage/service;1"]. + getService(Ci.mozIStorageService). + openSpecialDatabase("memory"); + } + else if (aData == "dispose") { + Services.obs.removeObserver(this, "test-options"); + try { + this._dbConn.asyncClose(); + } catch (e) { + // Do nothing. + } + } + }, + + QueryInterface: XPCOMUtils.generateQI([ + Ci.mozIStorageVacuumParticipant, + Ci.nsIObserver, + ]) +}; + +var gComponentsArray = [vacuumParticipant]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(gComponentsArray); diff --git a/storage/test/unit/vacuumParticipant.manifest b/storage/test/unit/vacuumParticipant.manifest new file mode 100644 index 000000000..cf359a80f --- /dev/null +++ b/storage/test/unit/vacuumParticipant.manifest @@ -0,0 +1,3 @@ +component {52aa0b22-b82f-4e38-992a-c3675a3355d2} vacuumParticipant.js +contract @unit.test.com/test-vacuum-participant;1 {52aa0b22-b82f-4e38-992a-c3675a3355d2} +category vacuum-participant vacuumParticipant @unit.test.com/test-vacuum-participant;1 diff --git a/storage/test/unit/xpcshell.ini b/storage/test/unit/xpcshell.ini new file mode 100644 index 000000000..e93c7d5b9 --- /dev/null +++ b/storage/test/unit/xpcshell.ini @@ -0,0 +1,46 @@ +[DEFAULT] +head = head_storage.js +tail = +support-files = + corruptDB.sqlite + fakeDB.sqlite + locale_collation.txt + vacuumParticipant.js + vacuumParticipant.manifest + +[test_bug-365166.js] +[test_bug-393952.js] +[test_bug-429521.js] +[test_bug-444233.js] +[test_cache_size.js] +[test_chunk_growth.js] +# Bug 676981: test fails consistently on Android +fail-if = os == "android" +[test_connection_asyncClose.js] +[test_connection_executeAsync.js] +[test_connection_executeSimpleSQLAsync.js] +[test_js_helpers.js] +[test_levenshtein.js] +[test_like.js] +[test_like_escape.js] +[test_locale_collation.js] +[test_page_size_is_32k.js] +[test_sqlite_secure_delete.js] +[test_statement_executeAsync.js] +[test_statement_wrapper_automatically.js] +[test_storage_aggregates.js] +[test_storage_connection.js] +# Bug 676981: test fails consistently on Android +fail-if = os == "android" +[test_storage_fulltextindex.js] +[test_storage_function.js] +[test_storage_progresshandler.js] +[test_storage_service.js] +[test_storage_service_unshared.js] +[test_storage_statement.js] +[test_storage_value_array.js] +[test_unicode.js] +[test_vacuum.js] +[test_telemetry_vfs.js] +# Bug 676981: test fails consistently on Android +# fail-if = os == "android" |