summaryrefslogtreecommitdiffstats
path: root/storage/test/unit
diff options
context:
space:
mode:
Diffstat (limited to 'storage/test/unit')
-rw-r--r--storage/test/unit/corruptDB.sqlitebin0 -> 32772 bytes
-rw-r--r--storage/test/unit/fakeDB.sqlite1
-rw-r--r--storage/test/unit/head_storage.js372
-rw-r--r--storage/test/unit/locale_collation.txt174
-rw-r--r--storage/test/unit/test_bug-365166.js26
-rw-r--r--storage/test/unit/test_bug-393952.js38
-rw-r--r--storage/test/unit/test_bug-429521.js46
-rw-r--r--storage/test/unit/test_bug-444233.js51
-rw-r--r--storage/test/unit/test_cache_size.js72
-rw-r--r--storage/test/unit/test_chunk_growth.js52
-rw-r--r--storage/test/unit/test_connection_asyncClose.js125
-rw-r--r--storage/test/unit/test_connection_executeAsync.js171
-rw-r--r--storage/test/unit/test_connection_executeSimpleSQLAsync.js79
-rw-r--r--storage/test/unit/test_js_helpers.js125
-rw-r--r--storage/test/unit/test_levenshtein.js74
-rw-r--r--storage/test/unit/test_like.js202
-rw-r--r--storage/test/unit/test_like_escape.js60
-rw-r--r--storage/test/unit/test_locale_collation.js304
-rw-r--r--storage/test/unit/test_page_size_is_32k.js35
-rw-r--r--storage/test/unit/test_sqlite_secure_delete.js80
-rw-r--r--storage/test/unit/test_statement_executeAsync.js998
-rw-r--r--storage/test/unit/test_statement_wrapper_automatically.js167
-rw-r--r--storage/test/unit/test_storage_aggregates.js116
-rw-r--r--storage/test/unit/test_storage_connection.js763
-rw-r--r--storage/test/unit/test_storage_fulltextindex.js86
-rw-r--r--storage/test/unit/test_storage_function.js95
-rw-r--r--storage/test/unit/test_storage_progresshandler.js111
-rw-r--r--storage/test/unit/test_storage_service.js142
-rw-r--r--storage/test/unit/test_storage_service_unshared.js35
-rw-r--r--storage/test/unit/test_storage_statement.js184
-rw-r--r--storage/test/unit/test_storage_value_array.js182
-rw-r--r--storage/test/unit/test_telemetry_vfs.js30
-rw-r--r--storage/test/unit/test_unicode.js83
-rw-r--r--storage/test/unit/test_vacuum.js335
-rw-r--r--storage/test/unit/vacuumParticipant.js125
-rw-r--r--storage/test/unit/vacuumParticipant.manifest3
-rw-r--r--storage/test/unit/xpcshell.ini46
37 files changed, 5588 insertions, 0 deletions
diff --git a/storage/test/unit/corruptDB.sqlite b/storage/test/unit/corruptDB.sqlite
new file mode 100644
index 000000000..b234246ca
--- /dev/null
+++ b/storage/test/unit/corruptDB.sqlite
Binary files differ
diff --git a/storage/test/unit/fakeDB.sqlite b/storage/test/unit/fakeDB.sqlite
new file mode 100644
index 000000000..5f7498bfc
--- /dev/null
+++ b/storage/test/unit/fakeDB.sqlite
@@ -0,0 +1 @@
+BACON
diff --git a/storage/test/unit/head_storage.js b/storage/test/unit/head_storage.js
new file mode 100644
index 000000000..40374afad
--- /dev/null
+++ b/storage/test/unit/head_storage.js
@@ -0,0 +1,372 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+
+
+do_get_profile();
+var dirSvc = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties);
+
+var gDBConn = null;
+
+function getTestDB()
+{
+ var db = dirSvc.get("ProfD", Ci.nsIFile);
+ db.append("test_storage.sqlite");
+ return db;
+}
+
+/**
+ * Obtains a corrupt database to test against.
+ */
+function getCorruptDB()
+{
+ return do_get_file("corruptDB.sqlite");
+}
+
+/**
+ * Obtains a fake (non-SQLite format) database to test against.
+ */
+function getFakeDB()
+{
+ return do_get_file("fakeDB.sqlite");
+}
+
+/**
+ * Delete the test database file.
+ */
+function deleteTestDB()
+{
+ print("*** Storage Tests: Trying to remove file!");
+ var dbFile = getTestDB();
+ if (dbFile.exists())
+ try { dbFile.remove(false); } catch (e) { /* stupid windows box */ }
+}
+
+function cleanup()
+{
+ // close the connection
+ print("*** Storage Tests: Trying to close!");
+ getOpenedDatabase().close();
+
+ // we need to null out the database variable to get a new connection the next
+ // time getOpenedDatabase is called
+ gDBConn = null;
+
+ // removing test db
+ deleteTestDB();
+}
+
+/**
+ * Use asyncClose to cleanup a connection. Synchronous by means of internally
+ * spinning an event loop.
+ */
+function asyncCleanup()
+{
+ let closed = false;
+
+ // close the connection
+ print("*** Storage Tests: Trying to asyncClose!");
+ getOpenedDatabase().asyncClose(function () { closed = true; });
+
+ let curThread = Components.classes["@mozilla.org/thread-manager;1"]
+ .getService().currentThread;
+ while (!closed)
+ curThread.processNextEvent(true);
+
+ // we need to null out the database variable to get a new connection the next
+ // time getOpenedDatabase is called
+ gDBConn = null;
+
+ // removing test db
+ deleteTestDB();
+}
+
+function getService()
+{
+ return Cc["@mozilla.org/storage/service;1"].getService(Ci.mozIStorageService);
+}
+
+/**
+ * Get a connection to the test database. Creates and caches the connection
+ * if necessary, otherwise reuses the existing cached connection. This
+ * connection shares its cache.
+ *
+ * @returns the mozIStorageConnection for the file.
+ */
+function getOpenedDatabase()
+{
+ if (!gDBConn) {
+ gDBConn = getService().openDatabase(getTestDB());
+ }
+ return gDBConn;
+}
+
+/**
+ * Get a connection to the test database. Creates and caches the connection
+ * if necessary, otherwise reuses the existing cached connection. This
+ * connection doesn't share its cache.
+ *
+ * @returns the mozIStorageConnection for the file.
+ */
+function getOpenedUnsharedDatabase()
+{
+ if (!gDBConn) {
+ gDBConn = getService().openUnsharedDatabase(getTestDB());
+ }
+ return gDBConn;
+}
+
+/**
+ * Obtains a specific database to use.
+ *
+ * @param aFile
+ * The nsIFile representing the db file to open.
+ * @returns the mozIStorageConnection for the file.
+ */
+function getDatabase(aFile)
+{
+ return getService().openDatabase(aFile);
+}
+
+function createStatement(aSQL)
+{
+ return getOpenedDatabase().createStatement(aSQL);
+}
+
+/**
+ * Creates an asynchronous SQL statement.
+ *
+ * @param aSQL
+ * The SQL to parse into a statement.
+ * @returns a mozIStorageAsyncStatement from aSQL.
+ */
+function createAsyncStatement(aSQL)
+{
+ return getOpenedDatabase().createAsyncStatement(aSQL);
+}
+
+/**
+ * Invoke the given function and assert that it throws an exception expressing
+ * the provided error code in its 'result' attribute. JS function expressions
+ * can be used to do this concisely.
+ *
+ * Example:
+ * expectError(Cr.NS_ERROR_INVALID_ARG, () => explodingFunction());
+ *
+ * @param aErrorCode
+ * The error code to expect from invocation of aFunction.
+ * @param aFunction
+ * The function to invoke and expect an XPCOM-style error from.
+ */
+function expectError(aErrorCode, aFunction)
+{
+ let exceptionCaught = false;
+ try {
+ aFunction();
+ }
+ catch (e) {
+ if (e.result != aErrorCode) {
+ do_throw("Got an exception, but the result code was not the expected " +
+ "one. Expected " + aErrorCode + ", got " + e.result);
+ }
+ exceptionCaught = true;
+ }
+ if (!exceptionCaught)
+ do_throw(aFunction + " should have thrown an exception but did not!");
+}
+
+/**
+ * Run a query synchronously and verify that we get back the expected results.
+ *
+ * @param aSQLString
+ * The SQL string for the query.
+ * @param aBind
+ * The value to bind at index 0.
+ * @param aResults
+ * A list of the expected values returned in the sole result row.
+ * Express blobs as lists.
+ */
+function verifyQuery(aSQLString, aBind, aResults)
+{
+ let stmt = getOpenedDatabase().createStatement(aSQLString);
+ stmt.bindByIndex(0, aBind);
+ try {
+ do_check_true(stmt.executeStep());
+ let nCols = stmt.numEntries;
+ if (aResults.length != nCols)
+ do_throw("Expected " + aResults.length + " columns in result but " +
+ "there are only " + aResults.length + "!");
+ for (let iCol = 0; iCol < nCols; iCol++) {
+ let expectedVal = aResults[iCol];
+ let valType = stmt.getTypeOfIndex(iCol);
+ if (expectedVal === null) {
+ do_check_eq(stmt.VALUE_TYPE_NULL, valType);
+ do_check_true(stmt.getIsNull(iCol));
+ }
+ else if (typeof expectedVal == "number") {
+ if (Math.floor(expectedVal) == expectedVal) {
+ do_check_eq(stmt.VALUE_TYPE_INTEGER, valType);
+ do_check_eq(expectedVal, stmt.getInt32(iCol));
+ }
+ else {
+ do_check_eq(stmt.VALUE_TYPE_FLOAT, valType);
+ do_check_eq(expectedVal, stmt.getDouble(iCol));
+ }
+ }
+ else if (typeof expectedVal == "string") {
+ do_check_eq(stmt.VALUE_TYPE_TEXT, valType);
+ do_check_eq(expectedVal, stmt.getUTF8String(iCol));
+ }
+ else { // blob
+ do_check_eq(stmt.VALUE_TYPE_BLOB, valType);
+ let count = { value: 0 }, blob = { value: null };
+ stmt.getBlob(iCol, count, blob);
+ do_check_eq(count.value, expectedVal.length);
+ for (let i = 0; i < count.value; i++) {
+ do_check_eq(expectedVal[i], blob.value[i]);
+ }
+ }
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+}
+
+/**
+ * Return the number of rows in the able with the given name using a synchronous
+ * query.
+ *
+ * @param aTableName
+ * The name of the table.
+ * @return The number of rows.
+ */
+function getTableRowCount(aTableName)
+{
+ var currentRows = 0;
+ var countStmt = getOpenedDatabase().createStatement(
+ "SELECT COUNT(1) AS count FROM " + aTableName
+ );
+ try {
+ do_check_true(countStmt.executeStep());
+ currentRows = countStmt.row.count;
+ }
+ finally {
+ countStmt.finalize();
+ }
+ return currentRows;
+}
+
+// Promise-Returning Functions
+
+function asyncClone(db, readOnly) {
+ let deferred = Promise.defer();
+ db.asyncClone(readOnly, function (status, db2) {
+ if (Components.isSuccessCode(status)) {
+ deferred.resolve(db2);
+ } else {
+ deferred.reject(status);
+ }
+ });
+ return deferred.promise;
+}
+
+function asyncClose(db) {
+ let deferred = Promise.defer();
+ db.asyncClose(function (status) {
+ if (Components.isSuccessCode(status)) {
+ deferred.resolve();
+ } else {
+ deferred.reject(status);
+ }
+ });
+ return deferred.promise;
+}
+
+function openAsyncDatabase(file, options) {
+ let deferred = Promise.defer();
+ let properties;
+ if (options) {
+ properties = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+ for (let k in options) {
+ properties.setProperty(k, options[k]);
+ }
+ }
+ getService().openAsyncDatabase(file, properties, function (status, db) {
+ if (Components.isSuccessCode(status)) {
+ deferred.resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection));
+ } else {
+ deferred.reject(status);
+ }
+ });
+ return deferred.promise;
+}
+
+function executeAsync(statement, onResult) {
+ let deferred = Promise.defer();
+ statement.executeAsync({
+ handleError: function (error) {
+ deferred.reject(error);
+ },
+ handleResult: function (result) {
+ if (onResult) {
+ onResult(result);
+ }
+ },
+ handleCompletion: function (result) {
+ deferred.resolve(result);
+ }
+ });
+ return deferred.promise;
+}
+
+function executeMultipleStatementsAsync(db, statements, onResult) {
+ let deferred = Promise.defer();
+ db.executeAsync(statements, statements.length, {
+ handleError: function (error) {
+ deferred.reject(error);
+ },
+ handleResult: function (result) {
+ if (onResult) {
+ onResult(result);
+ }
+ },
+ handleCompletion: function (result) {
+ deferred.resolve(result);
+ }
+ });
+ return deferred.promise;
+}
+
+function executeSimpleSQLAsync(db, query, onResult) {
+ let deferred = Promise.defer();
+ db.executeSimpleSQLAsync(query, {
+ handleError(error) {
+ deferred.reject(error);
+ },
+ handleResult(result) {
+ if (onResult) {
+ onResult(result);
+ } else {
+ do_throw("No results were expected");
+ }
+ },
+ handleCompletion(result) {
+ deferred.resolve(result);
+ }
+ });
+ return deferred.promise;
+}
+
+cleanup();
diff --git a/storage/test/unit/locale_collation.txt b/storage/test/unit/locale_collation.txt
new file mode 100644
index 000000000..86f50579b
--- /dev/null
+++ b/storage/test/unit/locale_collation.txt
@@ -0,0 +1,174 @@
+
+!
+"
+#
+$
+%
+&
+'
+(
+)
+*
++
+,
+-
+.
+/
+0
+1
+2
+3
+4
+5
+6
+7
+8
+9
+:
+;
+<
+=
+>
+?
+@
+A
+B
+C
+D
+E
+F
+G
+H
+I
+J
+K
+L
+M
+N
+O
+P
+Q
+R
+S
+T
+U
+V
+W
+X
+Y
+Z
+[
+\
+]
+^
+_
+`
+a
+b
+c
+d
+e
+f
+g
+h
+i
+j
+k
+l
+m
+n
+o
+p
+q
+r
+s
+t
+u
+v
+w
+x
+y
+z
+{
+|
+}
+~
+ludwig van beethoven
+Ludwig van Beethoven
+Ludwig van beethoven
+Jane
+jane
+JANE
+jAne
+jaNe
+janE
+JAne
+JaNe
+JanE
+JANe
+JaNE
+JAnE
+jANE
+Umberto Eco
+Umberto eco
+umberto eco
+umberto Eco
+UMBERTO ECO
+ace
+bash
+*ace
+!ace
+%ace
+~ace
+#ace
+cork
+denizen
+[denizen]
+(denizen)
+{denizen}
+/denizen/
+#denizen#
+$denizen$
+@denizen
+elf
+full
+gnome
+gnomic investigation of typological factors in the grammaticalization process of Malayo-Polynesian substaratum in the protoAltaic vocabulary core in the proto-layers of pre-historic Japanese
+gnomic investigation of typological factors in the grammaticalization process of Malayo-Polynesian substaratum in the protoAltaic vocabulary core in the proto-layers of pre-historic Javanese
+hint
+Internationalization
+Zinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalization
+Zinternationalization internationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizatio
+n
+Zinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalization internationalizationinternationalizationinternationalizationinternationalizationinternationalization
+ZinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizaTioninternationalizationinternationalizationinternationalizationinternationalization
+ZinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizaTion
+jostle
+kin
+Laymen
+lumens
+Lumens
+motleycrew
+motley crew
+niven's creative talents
+nivens creative talents
+opie
+posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the Rockies
+posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the Rokkies
+posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the rockies
+quilt's
+quilts
+quilt
+Rondo
+street
+tamale oxidization and iodization in rapid progress
+tamale oxidization and iodization in rapid Progress
+until
+vera
+Wobble
+Xanadu's legenary imaginary floccinaucinihilipilification in localization of theoretical portions of glottochronological understanding of the phoneme
+Xanadu's legenary imaginary floccinaucinihilipilification in localization of theoretical portions of glottochronological understanding of the phoname
+yearn
+zodiac
+a
diff --git a/storage/test/unit/test_bug-365166.js b/storage/test/unit/test_bug-365166.js
new file mode 100644
index 000000000..ebcdc6900
--- /dev/null
+++ b/storage/test/unit/test_bug-365166.js
@@ -0,0 +1,26 @@
+// Testcase for bug 365166 - crash [@ strlen] calling
+// mozIStorageStatement::getColumnName of a statement created with
+// "PRAGMA user_version" or "PRAGMA schema_version"
+function run_test() {
+ test('user');
+ test('schema');
+
+ function test(param)
+ {
+ var colName = param + "_version";
+ var sql = "PRAGMA " + colName;
+
+ var file = getTestDB();
+ var storageService = Components.classes["@mozilla.org/storage/service;1"].
+ getService(Components.interfaces.mozIStorageService);
+ var conn = storageService.openDatabase(file);
+ var statement = conn.createStatement(sql);
+ try {
+ // This shouldn't crash:
+ do_check_eq(statement.getColumnName(0), colName);
+ } finally {
+ statement.reset();
+ statement.finalize();
+ }
+ }
+}
diff --git a/storage/test/unit/test_bug-393952.js b/storage/test/unit/test_bug-393952.js
new file mode 100644
index 000000000..f3c61f62b
--- /dev/null
+++ b/storage/test/unit/test_bug-393952.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Testcase for bug 393952: crash when I try to VACUUM and one of the tables
+// has a UNIQUE text column. StorageUnicodeFunctions::likeFunction()
+// needs to handle null aArgv[0] and aArgv[1]
+
+function setup()
+{
+ getOpenedDatabase().createTable("t1", "x TEXT UNIQUE");
+
+ var stmt = createStatement("INSERT INTO t1 (x) VALUES ('a')");
+ stmt.execute();
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_vacuum()
+{
+ var stmt = createStatement("VACUUM;");
+ stmt.executeStep();
+ stmt.reset();
+ stmt.finalize();
+}
+
+var tests = [test_vacuum];
+
+function run_test()
+{
+ setup();
+
+ for (var i = 0; i < tests.length; i++)
+ tests[i]();
+
+ cleanup();
+}
+
diff --git a/storage/test/unit/test_bug-429521.js b/storage/test/unit/test_bug-429521.js
new file mode 100644
index 000000000..a9eafc1c2
--- /dev/null
+++ b/storage/test/unit/test_bug-429521.js
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function setup() {
+ getOpenedDatabase().createTable("t1", "x TEXT");
+
+ var stmt = createStatement("INSERT INTO t1 (x) VALUES ('/mozilla.org/20070129_1/Europe/Berlin')");
+ stmt.execute();
+ stmt.finalize();
+}
+
+function test_bug429521() {
+ var stmt = createStatement(
+ "SELECT DISTINCT(zone) FROM (" +
+ "SELECT x AS zone FROM t1 WHERE x LIKE '/mozilla.org%'" +
+ ");");
+
+ print("*** test_bug429521: started");
+
+ try {
+ while (stmt.executeStep()) {
+ print("*** test_bug429521: step() Read wrapper.row.zone");
+
+ // BUG: the print commands after the following statement
+ // are never executed. Script stops immediately.
+ var tzId = stmt.row.zone;
+
+ print("*** test_bug429521: step() Read wrapper.row.zone finished");
+ }
+ } catch (e) {
+ print("*** test_bug429521: " + e);
+ }
+
+ print("*** test_bug429521: finished");
+
+ stmt.finalize();
+}
+
+function run_test() {
+ setup();
+
+ test_bug429521();
+
+ cleanup();
+}
diff --git a/storage/test/unit/test_bug-444233.js b/storage/test/unit/test_bug-444233.js
new file mode 100644
index 000000000..eb315f934
--- /dev/null
+++ b/storage/test/unit/test_bug-444233.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function setup() {
+ // Create the table
+ getOpenedDatabase().createTable("test_bug444233",
+ "id INTEGER PRIMARY KEY, value TEXT");
+
+ // Insert dummy data, using wrapper methods
+ var stmt = createStatement("INSERT INTO test_bug444233 (value) VALUES (:value)");
+ stmt.params.value = "value1";
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO test_bug444233 (value) VALUES (:value)");
+ stmt.params.value = "value2";
+ stmt.execute();
+ stmt.finalize();
+}
+
+function test_bug444233() {
+ print("*** test_bug444233: started");
+
+ // Check that there are 2 results
+ var stmt = createStatement("SELECT COUNT(*) AS number FROM test_bug444233");
+ do_check_true(stmt.executeStep());
+ do_check_eq(2, stmt.row.number);
+ stmt.reset();
+ stmt.finalize();
+
+ print("*** test_bug444233: doing delete");
+
+ // Now try to delete using IN
+ // Cheating since we know ids are 1,2
+ try {
+ var ids = [1, 2];
+ stmt = createStatement("DELETE FROM test_bug444233 WHERE id IN (:ids)");
+ stmt.params.ids = ids;
+ } catch (e) {
+ print("*** test_bug444233: successfully caught exception");
+ }
+ stmt.finalize();
+}
+
+function run_test() {
+ setup();
+ test_bug444233();
+ cleanup();
+}
+
diff --git a/storage/test/unit/test_cache_size.js b/storage/test/unit/test_cache_size.js
new file mode 100644
index 000000000..e0a5e8723
--- /dev/null
+++ b/storage/test/unit/test_cache_size.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This file tests that dbs of various page sizes are using the right cache
+// size (bug 703113).
+
+/**
+ * In order to change the cache size, we must open a DB, change the page
+ * size, create a table, close the DB, then re-open the DB. We then check
+ * the cache size after reopening.
+ *
+ * @param dbOpener
+ * function that opens the DB specified in file
+ * @param file
+ * file holding the database
+ * @param pageSize
+ * the DB's page size
+ * @param expectedCacheSize
+ * the expected cache size for the given page size
+ */
+function check_size(dbOpener, file, pageSize, expectedCacheSize)
+{
+ // Open the DB, immediately change its page size.
+ let db = dbOpener(file);
+ db.executeSimpleSQL("PRAGMA page_size = " + pageSize);
+
+ // Check the page size change worked.
+ let stmt = db.createStatement("PRAGMA page_size");
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.page_size, pageSize);
+ stmt.finalize();
+
+ // Create a simple table.
+ db.executeSimpleSQL("CREATE TABLE test ( id INTEGER PRIMARY KEY )");
+
+ // Close and re-open the DB.
+ db.close();
+ db = dbOpener(file);
+
+ // Check cache size is as expected.
+ stmt = db.createStatement("PRAGMA cache_size");
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.cache_size, expectedCacheSize);
+ stmt.finalize();
+}
+
+function new_file(name)
+{
+ let file = dirSvc.get("ProfD", Ci.nsIFile);
+ file.append(name + ".sqlite");
+ do_check_false(file.exists());
+ return file;
+}
+
+function run_test()
+{
+ const kExpectedCacheSize = -2048; // 2MiB
+
+ let pageSizes = [
+ 1024,
+ 4096,
+ 32768,
+ ];
+
+ for (let i = 0; i < pageSizes.length; i++) {
+ let pageSize = pageSizes[i];
+ check_size(getDatabase,
+ new_file("shared" + pageSize), pageSize, kExpectedCacheSize);
+ check_size(getService().openUnsharedDatabase,
+ new_file("unshared" + pageSize), pageSize, kExpectedCacheSize);
+ }
+}
diff --git a/storage/test/unit/test_chunk_growth.js b/storage/test/unit/test_chunk_growth.js
new file mode 100644
index 000000000..2cf91fe16
--- /dev/null
+++ b/storage/test/unit/test_chunk_growth.js
@@ -0,0 +1,52 @@
+// This file tests SQLITE_FCNTL_CHUNK_SIZE behaves as expected
+
+function run_sql(d, sql) {
+ var stmt = d.createStatement(sql);
+ stmt.execute();
+ stmt.finalize();
+}
+
+function new_file(name) {
+ var file = dirSvc.get("ProfD", Ci.nsIFile);
+ file.append(name);
+ return file;
+}
+
+function get_size(name) {
+ return new_file(name).fileSize;
+}
+
+function run_test() {
+ const filename = "chunked.sqlite";
+ const CHUNK_SIZE = 512 * 1024;
+ var d = getDatabase(new_file(filename));
+ try {
+ d.setGrowthIncrement(CHUNK_SIZE, "");
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_FILE_TOO_BIG) {
+ throw e;
+ }
+ print("Too little free space to set CHUNK_SIZE!");
+ return;
+ }
+ run_sql(d, "CREATE TABLE bloat(data varchar)");
+
+ var orig_size = get_size(filename);
+ /* Dump in at least 32K worth of data.
+ * While writing ensure that the file size growth in chunksize set above.
+ */
+ const str1024 = new Array(1024).join("T");
+ for (var i = 0; i < 32; i++) {
+ run_sql(d, "INSERT INTO bloat VALUES('" + str1024 + "')");
+ var size = get_size(filename);
+ // Must not grow in small increments.
+ do_check_true(size == orig_size || size >= CHUNK_SIZE);
+ }
+ /* In addition to growing in chunk-size increments, the db
+ * should shrink in chunk-size increments too.
+ */
+ run_sql(d, "DELETE FROM bloat");
+ run_sql(d, "VACUUM");
+ do_check_true(get_size(filename) >= CHUNK_SIZE);
+}
+
diff --git a/storage/test/unit/test_connection_asyncClose.js b/storage/test/unit/test_connection_asyncClose.js
new file mode 100644
index 000000000..7704dcc81
--- /dev/null
+++ b/storage/test/unit/test_connection_asyncClose.js
@@ -0,0 +1,125 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Thorough branch coverage for asyncClose.
+ *
+ * Coverage of asyncClose by connection state at time of AsyncClose invocation:
+ * - (asyncThread && mDBConn) => AsyncCloseConnection used, actually closes
+ * - test_asyncClose_does_not_complete_before_statements
+ * - test_double_asyncClose_throws
+ * - test_asyncClose_does_not_throw_without_callback
+ * - (asyncThread && !mDBConn) => AsyncCloseConnection used, although no close
+ * is required. Note that this is only possible in the event that
+ * openAsyncDatabase was used and we failed to open the database.
+ * Additionally, the async connection will never be exposed to the caller and
+ * AsyncInitDatabase will be the one to (automatically) call AsyncClose.
+ * - test_asyncClose_failed_open
+ * - (!asyncThread && mDBConn) => Close() invoked, actually closes
+ * - test_asyncClose_on_sync_db
+ * - (!asyncThread && !mDBConn) => Close() invoked, no close needed, errors.
+ * This happens if the database has already been closed.
+ * - test_double_asyncClose_throws
+ */
+
+
+/**
+ * Sanity check that our close indeed happens after asynchronously executed
+ * statements scheduled during the same turn of the event loop. Note that we
+ * just care that the statement says it completed without error, we're not
+ * worried that the close will happen and then the statement will magically
+ * complete.
+ */
+add_task(function* test_asyncClose_does_not_complete_before_statements() {
+ let db = getService().openDatabase(getTestDB());
+ let stmt = db.createStatement("SELECT * FROM sqlite_master");
+ // Issue the executeAsync but don't yield for it...
+ let asyncStatementPromise = executeAsync(stmt);
+ stmt.finalize();
+
+ // Issue the close. (And now the order of yielding doesn't matter.)
+ // Branch coverage: (asyncThread && mDBConn)
+ yield asyncClose(db);
+ equal((yield asyncStatementPromise),
+ Ci.mozIStorageStatementCallback.REASON_FINISHED);
+});
+
+/**
+ * Open an async database (ensures the async thread is created) and then invoke
+ * AsyncClose() twice without yielding control flow. The first will initiate
+ * the actual async close after calling setClosedState which synchronously
+ * impacts what the second call will observe. The second call will then see the
+ * async thread is not available and fall back to invoking Close() which will
+ * notice the mDBConn is already gone.
+ */
+add_task(function* test_double_asyncClose_throws() {
+ let db = yield openAsyncDatabase(getTestDB());
+
+ // (Don't yield control flow yet, save the promise for after we make the
+ // second call.)
+ // Branch coverage: (asyncThread && mDBConn)
+ let realClosePromise = yield asyncClose(db);
+ try {
+ // Branch coverage: (!asyncThread && !mDBConn)
+ db.asyncClose();
+ ok(false, "should have thrown");
+ } catch (e) {
+ equal(e.result, Cr.NS_ERROR_NOT_INITIALIZED);
+ }
+
+ yield realClosePromise;
+});
+
+/**
+ * Create a sync db connection and never take it asynchronous and then call
+ * asyncClose on it. This will bring the async thread to life to perform the
+ * shutdown to avoid blocking the main thread, although we won't be able to
+ * tell the difference between this happening and the method secretly shunting
+ * to close().
+ */
+add_task(function* test_asyncClose_on_sync_db() {
+ let db = getService().openDatabase(getTestDB());
+
+ // Branch coverage: (!asyncThread && mDBConn)
+ yield asyncClose(db);
+ ok(true, 'closed sync connection asynchronously');
+});
+
+/**
+ * Fail to asynchronously open a DB in order to get an async thread existing
+ * without having an open database and asyncClose invoked. As per the file
+ * doc-block, note that asyncClose will automatically be invoked by the
+ * AsyncInitDatabase when it fails to open the database. We will never be
+ * provided with a reference to the connection and so cannot call AsyncClose on
+ * it ourselves.
+ */
+add_task(function* test_asyncClose_failed_open() {
+ // This will fail and the promise will be rejected.
+ let openPromise = openAsyncDatabase(getFakeDB());
+ yield openPromise.then(
+ () => {
+ ok(false, 'we should have failed to open the db; this test is broken!');
+ },
+ () => {
+ ok(true, 'correctly failed to open db; bg asyncClose should happen');
+ }
+ );
+ // (NB: we are unable to observe the thread shutdown, but since we never open
+ // a database, this test is not going to interfere with other tests so much.)
+});
+
+// THE TEST BELOW WANTS TO BE THE LAST TEST WE RUN. DO NOT MAKE IT SAD.
+/**
+ * Verify that asyncClose without a callback does not explode. Without a
+ * callback the shutdown is not actually observable, so we run this test last
+ * in order to avoid weird overlaps.
+ */
+add_task(function* test_asyncClose_does_not_throw_without_callback() {
+ let db = yield openAsyncDatabase(getTestDB());
+ // Branch coverage: (asyncThread && mDBConn)
+ db.asyncClose();
+ ok(true, 'if we shutdown cleanly and do not crash, then we succeeded');
+});
+// OBEY SHOUTING UPPER-CASE COMMENTS.
+// ADD TESTS ABOVE THE FORMER TEST, NOT BELOW IT.
diff --git a/storage/test/unit/test_connection_executeAsync.js b/storage/test/unit/test_connection_executeAsync.js
new file mode 100644
index 000000000..e56d98e55
--- /dev/null
+++ b/storage/test/unit/test_connection_executeAsync.js
@@ -0,0 +1,171 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * This file tests the functionality of mozIStorageConnection::executeAsync for
+ * both mozIStorageStatement and mozIStorageAsyncStatement.
+ *
+ * A single database connection is used for the entirety of the test, which is
+ * a legacy thing, but we otherwise use the modern promise-based driver and
+ * async helpers.
+ */
+
+const INTEGER = 1;
+const TEXT = "this is test text";
+const REAL = 3.23;
+const BLOB = [1, 2];
+
+add_task(function* test_first_create_and_add() {
+ // synchronously open the database and let gDBConn hold onto it because we
+ // use this database
+ let db = getOpenedDatabase();
+ // synchronously set up our table *that will be used for the rest of the file*
+ db.executeSimpleSQL(
+ "CREATE TABLE test (" +
+ "id INTEGER, " +
+ "string TEXT, " +
+ "number REAL, " +
+ "nuller NULL, " +
+ "blober BLOB" +
+ ")"
+ );
+
+ let stmts = [];
+ stmts[0] = db.createStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) VALUES (?, ?, ?, ?, ?)"
+ );
+ stmts[0].bindByIndex(0, INTEGER);
+ stmts[0].bindByIndex(1, TEXT);
+ stmts[0].bindByIndex(2, REAL);
+ stmts[0].bindByIndex(3, null);
+ stmts[0].bindBlobByIndex(4, BLOB, BLOB.length);
+ stmts[1] = getOpenedDatabase().createAsyncStatement(
+ "INSERT INTO test (string, number, nuller, blober) VALUES (?, ?, ?, ?)"
+ );
+ stmts[1].bindByIndex(0, TEXT);
+ stmts[1].bindByIndex(1, REAL);
+ stmts[1].bindByIndex(2, null);
+ stmts[1].bindBlobByIndex(3, BLOB, BLOB.length);
+
+ // asynchronously execute the statements
+ let execResult = yield executeMultipleStatementsAsync(
+ db,
+ stmts,
+ function(aResultSet) {
+ ok(false, 'we only did inserts so we should not have gotten results!');
+ });
+ equal(Ci.mozIStorageStatementCallback.REASON_FINISHED, execResult,
+ 'execution should have finished successfully.');
+
+ // Check that the result is in the table
+ let stmt = db.createStatement(
+ "SELECT string, number, nuller, blober FROM test WHERE id = ?"
+ );
+ stmt.bindByIndex(0, INTEGER);
+ try {
+ do_check_true(stmt.executeStep());
+ do_check_eq(TEXT, stmt.getString(0));
+ do_check_eq(REAL, stmt.getDouble(1));
+ do_check_true(stmt.getIsNull(2));
+ let count = { value: 0 };
+ let blob = { value: null };
+ stmt.getBlob(3, count, blob);
+ do_check_eq(BLOB.length, count.value);
+ for (let i = 0; i < BLOB.length; i++)
+ do_check_eq(BLOB[i], blob.value[i]);
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ // Make sure we have two rows in the table
+ stmt = db.createStatement(
+ "SELECT COUNT(1) FROM test"
+ );
+ try {
+ do_check_true(stmt.executeStep());
+ do_check_eq(2, stmt.getInt32(0));
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ stmts[0].finalize();
+ stmts[1].finalize();
+});
+
+add_task(function* test_last_multiple_bindings_on_statements() {
+ // This tests to make sure that we pass all the statements multiply bound
+ // parameters when we call executeAsync.
+ const AMOUNT_TO_ADD = 5;
+ const ITERATIONS = 5;
+
+ let stmts = [];
+ let db = getOpenedDatabase();
+ let sqlString = "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (:int, :text, :real, :null, :blob)";
+ // We run the same statement twice, and should insert 2 * AMOUNT_TO_ADD.
+ for (let i = 0; i < ITERATIONS; i++) {
+ // alternate the type of statement we create
+ if (i % 2)
+ stmts[i] = db.createStatement(sqlString);
+ else
+ stmts[i] = db.createAsyncStatement(sqlString);
+
+ let params = stmts[i].newBindingParamsArray();
+ for (let j = 0; j < AMOUNT_TO_ADD; j++) {
+ let bp = params.newBindingParams();
+ bp.bindByName("int", INTEGER);
+ bp.bindByName("text", TEXT);
+ bp.bindByName("real", REAL);
+ bp.bindByName("null", null);
+ bp.bindBlobByName("blob", BLOB, BLOB.length);
+ params.addParams(bp);
+ }
+ stmts[i].bindParameters(params);
+ }
+
+ // Get our current number of rows in the table.
+ let currentRows = 0;
+ let countStmt = getOpenedDatabase().createStatement(
+ "SELECT COUNT(1) AS count FROM test"
+ );
+ try {
+ do_check_true(countStmt.executeStep());
+ currentRows = countStmt.row.count;
+ }
+ finally {
+ countStmt.reset();
+ }
+
+ // Execute asynchronously.
+ let execResult = yield executeMultipleStatementsAsync(
+ db,
+ stmts,
+ function(aResultSet) {
+ ok(false, 'we only did inserts so we should not have gotten results!');
+ });
+ equal(Ci.mozIStorageStatementCallback.REASON_FINISHED, execResult,
+ 'execution should have finished successfully.');
+
+ // Check to make sure we added all of our rows.
+ try {
+ do_check_true(countStmt.executeStep());
+ do_check_eq(currentRows + (ITERATIONS * AMOUNT_TO_ADD),
+ countStmt.row.count);
+ }
+ finally {
+ countStmt.finalize();
+ }
+
+ stmts.forEach(stmt => stmt.finalize());
+
+ // we are the last test using this connection and since it has gone async
+ // we *must* call asyncClose on it.
+ yield asyncClose(db);
+ gDBConn = null;
+});
+
+// If you add a test down here you will need to move the asyncClose or clean
+// things up a little more.
diff --git a/storage/test/unit/test_connection_executeSimpleSQLAsync.js b/storage/test/unit/test_connection_executeSimpleSQLAsync.js
new file mode 100644
index 000000000..142cc8e14
--- /dev/null
+++ b/storage/test/unit/test_connection_executeSimpleSQLAsync.js
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * This file tests the functionality of
+ * mozIStorageAsyncConnection::executeSimpleSQLAsync.
+ */
+
+const INTEGER = 1;
+const TEXT = "this is test text";
+const REAL = 3.23;
+
+add_task(function* test_create_and_add() {
+ let adb = yield openAsyncDatabase(getTestDB());
+
+ let completion = yield executeSimpleSQLAsync(adb,
+ "CREATE TABLE test (id INTEGER, string TEXT, number REAL)");
+
+ do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion);
+
+ completion = yield executeSimpleSQLAsync(adb,
+ "INSERT INTO test (id, string, number) " +
+ "VALUES (" + INTEGER + ", \"" + TEXT + "\", " + REAL + ")");
+
+ do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion);
+
+ let result = null;
+
+ completion = yield executeSimpleSQLAsync(adb,
+ "SELECT string, number FROM test WHERE id = 1",
+ function (aResultSet) {
+ result = aResultSet.getNextRow();
+ do_check_eq(2, result.numEntries);
+ do_check_eq(TEXT, result.getString(0));
+ do_check_eq(REAL, result.getDouble(1));
+ }
+ );
+
+ do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion);
+ do_check_neq(result, null);
+ result = null;
+
+ yield executeSimpleSQLAsync(adb, "SELECT COUNT(0) FROM test",
+ function (aResultSet) {
+ result = aResultSet.getNextRow();
+ do_check_eq(1, result.getInt32(0));
+ });
+
+ do_check_neq(result, null);
+
+ yield asyncClose(adb);
+});
+
+
+add_task(function* test_asyncClose_does_not_complete_before_statement() {
+ let adb = yield openAsyncDatabase(getTestDB());
+ let executed = false;
+
+ let reason = yield executeSimpleSQLAsync(adb, "SELECT * FROM test",
+ function (aResultSet) {
+ let result = aResultSet.getNextRow();
+
+ do_check_neq(result, null);
+ do_check_eq(3, result.numEntries);
+ do_check_eq(INTEGER, result.getInt32(0));
+ do_check_eq(TEXT, result.getString(1));
+ do_check_eq(REAL, result.getDouble(2));
+ executed = true;
+ }
+ );
+
+ do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, reason);
+
+ // Ensure that the statement executed to completion.
+ do_check_true(executed);
+
+ yield asyncClose(adb);
+});
diff --git a/storage/test/unit/test_js_helpers.js b/storage/test/unit/test_js_helpers.js
new file mode 100644
index 000000000..dde9fac20
--- /dev/null
+++ b/storage/test/unit/test_js_helpers.js
@@ -0,0 +1,125 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sw=2 ts=2 sts=2 et : */
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This file tests that the JS language helpers in various ways.
+ */
+
+// Test Functions
+
+function test_params_enumerate()
+{
+ let stmt = createStatement(
+ "SELECT * FROM test WHERE id IN (:a, :b, :c)"
+ );
+
+ // Make sure they are right.
+ let expected = ["a", "b", "c"];
+ let index = 0;
+ for (let name in stmt.params) {
+ if (name == "QueryInterface")
+ continue;
+ do_check_eq(name, expected[index++]);
+ }
+}
+
+function test_params_prototype()
+{
+ let stmt = createStatement(
+ "SELECT * FROM sqlite_master"
+ );
+
+ // Set a property on the prototype and make sure it exist (will not be a
+ // bindable parameter, however).
+ Object.getPrototypeOf(stmt.params).test = 2;
+ do_check_eq(stmt.params.test, 2);
+ stmt.finalize();
+}
+
+function test_row_prototype()
+{
+ let stmt = createStatement(
+ "SELECT * FROM sqlite_master"
+ );
+
+ do_check_true(stmt.executeStep());
+
+ // Set a property on the prototype and make sure it exists (will not be in the
+ // results, however).
+ Object.getPrototypeOf(stmt.row).test = 2;
+ do_check_eq(stmt.row.test, 2);
+
+ // Clean up after ourselves.
+ delete Object.getPrototypeOf(stmt.row).test;
+ stmt.finalize();
+}
+
+function test_params_gets_sync()
+{
+ // Added for bug 562866.
+ /*
+ let stmt = createStatement(
+ "SELECT * FROM test WHERE id IN (:a, :b, :c)"
+ );
+
+ // Make sure we do not assert in getting the value.
+ let originalCount = Object.getOwnPropertyNames(stmt.params).length;
+ let expected = ["a", "b", "c"];
+ for (let name of expected) {
+ stmt.params[name];
+ }
+
+ // Now make sure we didn't magically get any additional properties.
+ let finalCount = Object.getOwnPropertyNames(stmt.params).length;
+ do_check_eq(originalCount + expected.length, finalCount);
+ */
+}
+
+function test_params_gets_async()
+{
+ // Added for bug 562866.
+ /*
+ let stmt = createAsyncStatement(
+ "SELECT * FROM test WHERE id IN (:a, :b, :c)"
+ );
+
+ // Make sure we do not assert in getting the value.
+ let originalCount = Object.getOwnPropertyNames(stmt.params).length;
+ let expected = ["a", "b", "c"];
+ for (let name of expected) {
+ stmt.params[name];
+ }
+
+ // Now make sure we didn't magically get any additional properties.
+ let finalCount = Object.getOwnPropertyNames(stmt.params).length;
+ do_check_eq(originalCount + expected.length, finalCount);
+ */
+}
+
+// Test Runner
+
+var tests = [
+ test_params_enumerate,
+ test_params_prototype,
+ test_row_prototype,
+ test_params_gets_sync,
+ test_params_gets_async,
+];
+function run_test()
+{
+ cleanup();
+
+ // Create our database.
+ getOpenedDatabase().executeSimpleSQL(
+ "CREATE TABLE test (" +
+ "id INTEGER PRIMARY KEY " +
+ ")"
+ );
+
+ // Run the tests.
+ tests.forEach(test => test());
+}
diff --git a/storage/test/unit/test_levenshtein.js b/storage/test/unit/test_levenshtein.js
new file mode 100644
index 000000000..ced141abd
--- /dev/null
+++ b/storage/test/unit/test_levenshtein.js
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file tests the Levenshtein Distance function we've registered.
+
+function createUtf16Database()
+{
+ print("Creating the in-memory UTF-16-encoded database.");
+ let conn = getService().openSpecialDatabase("memory");
+ conn.executeSimpleSQL("PRAGMA encoding = 'UTF-16'");
+
+ print("Make sure the encoding was set correctly and is now UTF-16.");
+ let stmt = conn.createStatement("PRAGMA encoding");
+ do_check_true(stmt.executeStep());
+ let enc = stmt.getString(0);
+ stmt.finalize();
+
+ // The value returned will actually be UTF-16le or UTF-16be.
+ do_check_true(enc === "UTF-16le" || enc === "UTF-16be");
+
+ return conn;
+}
+
+function check_levenshtein(db, s, t, expectedDistance)
+{
+ var stmt = db.createStatement("SELECT levenshteinDistance(:s, :t) AS result");
+ stmt.params.s = s;
+ stmt.params.t = t;
+ try {
+ do_check_true(stmt.executeStep());
+ do_check_eq(expectedDistance, stmt.row.result);
+ } finally {
+ stmt.reset();
+ stmt.finalize();
+ }
+}
+
+function testLevenshtein(db)
+{
+ // Basic tests.
+ check_levenshtein(db, "", "", 0);
+ check_levenshtein(db, "foo", "", 3);
+ check_levenshtein(db, "", "bar", 3);
+ check_levenshtein(db, "yellow", "hello", 2);
+ check_levenshtein(db, "gumbo", "gambol", 2);
+ check_levenshtein(db, "kitten", "sitten", 1);
+ check_levenshtein(db, "sitten", "sittin", 1);
+ check_levenshtein(db, "sittin", "sitting", 1);
+ check_levenshtein(db, "kitten", "sitting", 3);
+ check_levenshtein(db, "Saturday", "Sunday", 3);
+ check_levenshtein(db, "YHCQPGK", "LAHYQQKPGKA", 6);
+
+ // Test SQL NULL handling.
+ check_levenshtein(db, "foo", null, null);
+ check_levenshtein(db, null, "bar", null);
+ check_levenshtein(db, null, null, null);
+
+ // The levenshteinDistance function allocates temporary memory on the stack
+ // if it can. Test some strings long enough to force a heap allocation.
+ var dots1000 = Array(1001).join(".");
+ var dashes1000 = Array(1001).join("-");
+ check_levenshtein(db, dots1000, dashes1000, 1000);
+}
+
+function run_test()
+{
+ testLevenshtein(getOpenedDatabase());
+ testLevenshtein(createUtf16Database());
+}
+
+
+
+
diff --git a/storage/test/unit/test_like.js b/storage/test/unit/test_like.js
new file mode 100644
index 000000000..b42f2eb27
--- /dev/null
+++ b/storage/test/unit/test_like.js
@@ -0,0 +1,202 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file tests our LIKE implementation since we override it for unicode
+
+function setup()
+{
+ getOpenedDatabase().createTable("t1", "x TEXT");
+
+ var stmt = createStatement("INSERT INTO t1 (x) VALUES ('a')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('ab')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('abc')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('abcd')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('acd')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('abd')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('bc')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('bcd')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('xyz')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('ABC')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('CDE')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES ('ABC abc xyz')");
+ stmt.execute();
+ stmt.finalize();
+}
+
+function test_count()
+{
+ var stmt = createStatement("SELECT count(*) FROM t1;");
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.getInt32(0), 12);
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_like_1()
+{
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;");
+ stmt.bindByIndex(0, 'abc');
+ var solutions = ["abc", "ABC"];
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_false(stmt.executeStep());
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_like_2()
+{
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;");
+ stmt.bindByIndex(0, 'ABC');
+ var solutions = ["abc", "ABC"];
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_false(stmt.executeStep());
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_like_3()
+{
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;");
+ stmt.bindByIndex(0, 'aBc');
+ var solutions = ["abc", "ABC"];
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_false(stmt.executeStep());
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_like_4()
+{
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;");
+ stmt.bindByIndex(0, 'abc%');
+ var solutions = ["abc", "abcd", "ABC", "ABC abc xyz"];
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_false(stmt.executeStep());
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_like_5()
+{
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;");
+ stmt.bindByIndex(0, 'a_c');
+ var solutions = ["abc", "ABC"];
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_false(stmt.executeStep());
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_like_6()
+{
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;");
+ stmt.bindByIndex(0, 'ab%d');
+ var solutions = ["abcd", "abd"];
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_false(stmt.executeStep());
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_like_7()
+{
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;");
+ stmt.bindByIndex(0, 'a_c%');
+ var solutions = ["abc", "abcd", "ABC", "ABC abc xyz"];
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_false(stmt.executeStep());
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_like_8()
+{
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;");
+ stmt.bindByIndex(0, '%bcd');
+ var solutions = ["abcd", "bcd"];
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_true(stmt.executeStep());
+ do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+ do_check_false(stmt.executeStep());
+ stmt.reset();
+ stmt.finalize();
+}
+
+var tests = [test_count, test_like_1, test_like_2, test_like_3, test_like_4,
+ test_like_5, test_like_6, test_like_7, test_like_8];
+
+function run_test()
+{
+ setup();
+
+ for (var i = 0; i < tests.length; i++) {
+ tests[i]();
+ }
+
+ cleanup();
+}
+
diff --git a/storage/test/unit/test_like_escape.js b/storage/test/unit/test_like_escape.js
new file mode 100644
index 000000000..414f6237c
--- /dev/null
+++ b/storage/test/unit/test_like_escape.js
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const LATIN1_AE = "\xc6";
+const LATIN1_ae = "\xe6";
+
+function setup()
+{
+ getOpenedDatabase().createTable("t1", "x TEXT");
+
+ var stmt = createStatement("INSERT INTO t1 (x) VALUES ('foo/bar_baz%20cheese')");
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("INSERT INTO t1 (x) VALUES (?1)");
+ // insert LATIN_ae, but search on LATIN_AE
+ stmt.bindByIndex(0, "foo%20" + LATIN1_ae + "/_bar");
+ stmt.execute();
+ stmt.finalize();
+}
+
+function test_escape_for_like_ascii()
+{
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?1 ESCAPE '/'");
+ var paramForLike = stmt.escapeStringForLIKE("oo/bar_baz%20chees", '/');
+ // verify that we escaped / _ and %
+ do_check_eq(paramForLike, "oo//bar/_baz/%20chees");
+ // prepend and append with % for "contains"
+ stmt.bindByIndex(0, "%" + paramForLike + "%");
+ stmt.executeStep();
+ do_check_eq("foo/bar_baz%20cheese", stmt.getString(0));
+ stmt.finalize();
+}
+
+function test_escape_for_like_non_ascii()
+{
+ var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?1 ESCAPE '/'");
+ var paramForLike = stmt.escapeStringForLIKE("oo%20" + LATIN1_AE + "/_ba", '/');
+ // verify that we escaped / _ and %
+ do_check_eq(paramForLike, "oo/%20" + LATIN1_AE + "///_ba");
+ // prepend and append with % for "contains"
+ stmt.bindByIndex(0, "%" + paramForLike + "%");
+ stmt.executeStep();
+ do_check_eq("foo%20" + LATIN1_ae + "/_bar", stmt.getString(0));
+ stmt.finalize();
+}
+
+var tests = [test_escape_for_like_ascii, test_escape_for_like_non_ascii];
+
+function run_test()
+{
+ setup();
+
+ for (var i = 0; i < tests.length; i++) {
+ tests[i]();
+ }
+
+ cleanup();
+}
diff --git a/storage/test/unit/test_locale_collation.js b/storage/test/unit/test_locale_collation.js
new file mode 100644
index 000000000..12ba2b943
--- /dev/null
+++ b/storage/test/unit/test_locale_collation.js
@@ -0,0 +1,304 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Bug 499990 - Locale-aware collation
+ *
+ * Tests our custom, locale-aware collating sequences.
+ */
+
+// The name of the file containing the strings we'll sort during this test.
+// The file's data is taken from intl/locale/tests/sort/us-ascii_base.txt and
+// and intl/locale/tests/sort/us-ascii_sort.txt.
+const DATA_BASENAME = "locale_collation.txt";
+
+// The test data from DATA_BASENAME is read into this array.
+var gStrings;
+
+// A collation created from the application's locale. Used by localeCompare().
+var gLocaleCollation;
+
+// A connection to our in-memory UTF-16-encoded database.
+var gUtf16Conn;
+
+// Helper Functions
+
+/**
+ * Since we create a UTF-16 database we have to clean it up, in addition to
+ * the normal cleanup of Storage tests.
+ */
+function cleanupLocaleTests()
+{
+ print("-- Cleaning up test_locale_collation.js suite.");
+ gUtf16Conn.close();
+ cleanup();
+}
+
+/**
+ * Creates a test database similar to the default one created in
+ * head_storage.js, except that this one uses UTF-16 encoding.
+ *
+ * @return A connection to the database.
+ */
+function createUtf16Database()
+{
+ print("Creating the in-memory UTF-16-encoded database.");
+ let conn = getService().openSpecialDatabase("memory");
+ conn.executeSimpleSQL("PRAGMA encoding = 'UTF-16'");
+
+ print("Make sure the encoding was set correctly and is now UTF-16.");
+ let stmt = conn.createStatement("PRAGMA encoding");
+ do_check_true(stmt.executeStep());
+ let enc = stmt.getString(0);
+ stmt.finalize();
+
+ // The value returned will actually be UTF-16le or UTF-16be.
+ do_check_true(enc === "UTF-16le" || enc === "UTF-16be");
+
+ return conn;
+}
+
+/**
+ * Compares aActual to aExpected, ensuring that the numbers and orderings of
+ * the two arrays' elements are the same.
+ *
+ * @param aActual
+ * An array of strings retrieved from the database.
+ * @param aExpected
+ * An array of strings to which aActual should be equivalent.
+ */
+function ensureResultsAreCorrect(aActual, aExpected)
+{
+ print("Actual results: " + aActual);
+ print("Expected results: " + aExpected);
+
+ do_check_eq(aActual.length, aExpected.length);
+ for (let i = 0; i < aActual.length; i++)
+ do_check_eq(aActual[i], aExpected[i]);
+}
+
+/**
+ * Synchronously SELECTs all rows from the test table of the given database
+ * using the given collation.
+ *
+ * @param aCollation
+ * The name of one of our custom locale collations. The rows are
+ * ordered by this collation.
+ * @param aConn
+ * A connection to either the UTF-8 database or the UTF-16 database.
+ * @return The resulting strings in an array.
+ */
+function getResults(aCollation, aConn)
+{
+ let results = [];
+ let stmt = aConn.createStatement("SELECT t FROM test " +
+ "ORDER BY t COLLATE " + aCollation + " ASC");
+ while (stmt.executeStep())
+ results.push(stmt.row.t);
+ stmt.finalize();
+ return results;
+}
+
+/**
+ * Inserts strings into our test table of the given database in the order given.
+ *
+ * @param aStrings
+ * An array of strings.
+ * @param aConn
+ * A connection to either the UTF-8 database or the UTF-16 database.
+ */
+function initTableWithStrings(aStrings, aConn)
+{
+ print("Initializing test table.");
+
+ aConn.executeSimpleSQL("DROP TABLE IF EXISTS test");
+ aConn.createTable("test", "t TEXT");
+ let stmt = aConn.createStatement("INSERT INTO test (t) VALUES (:t)");
+ aStrings.forEach(function (str) {
+ stmt.params.t = str;
+ stmt.execute();
+ stmt.reset();
+ });
+ stmt.finalize();
+}
+
+/**
+ * Returns a sorting function suitable for passing to Array.prototype.sort().
+ * The returned function uses the application's locale to compare strings.
+ *
+ * @param aCollation
+ * The name of one of our custom locale collations. The sorting
+ * strength is computed from this value.
+ * @return A function to use as a sorting callback.
+ */
+function localeCompare(aCollation)
+{
+ var strength;
+
+ switch (aCollation) {
+ case "locale":
+ strength = Ci.nsICollation.kCollationCaseInSensitive;
+ break;
+ case "locale_case_sensitive":
+ strength = Ci.nsICollation.kCollationAccentInsenstive;
+ break;
+ case "locale_accent_sensitive":
+ strength = Ci.nsICollation.kCollationCaseInsensitiveAscii;
+ break;
+ case "locale_case_accent_sensitive":
+ strength = Ci.nsICollation.kCollationCaseSensitive;
+ break;
+ default:
+ do_throw("Error in test: unknown collation '" + aCollation + "'");
+ break;
+ }
+ return function (aStr1, aStr2) {
+ return gLocaleCollation.compareString(strength, aStr1, aStr2);
+ };
+}
+
+/**
+ * Reads in the test data from the file DATA_BASENAME and returns it as an array
+ * of strings.
+ *
+ * @return The test data as an array of strings.
+ */
+function readTestData()
+{
+ print("Reading in test data.");
+
+ let file = do_get_file(DATA_BASENAME);
+
+ let istream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ istream.init(file, -1, -1, 0);
+ istream.QueryInterface(Components.interfaces.nsILineInputStream);
+
+ let line = {};
+ let lines = [];
+ while (istream.readLine(line)) {
+ lines.push(line.value);
+ }
+ istream.close();
+
+ return lines;
+}
+
+/**
+ * Gets the results from the given database using the given collation and
+ * ensures that they match gStrings sorted by the same collation.
+ *
+ * @param aCollation
+ * The name of one of our custom locale collations. The rows from the
+ * database and the expected results are ordered by this collation.
+ * @param aConn
+ * A connection to either the UTF-8 database or the UTF-16 database.
+ */
+function runTest(aCollation, aConn)
+{
+ ensureResultsAreCorrect(getResults(aCollation, aConn),
+ gStrings.slice(0).sort(localeCompare(aCollation)));
+}
+
+/**
+ * Gets the results from the UTF-8 database using the given collation and
+ * ensures that they match gStrings sorted by the same collation.
+ *
+ * @param aCollation
+ * The name of one of our custom locale collations. The rows from the
+ * database and the expected results are ordered by this collation.
+ */
+function runUtf8Test(aCollation)
+{
+ runTest(aCollation, getOpenedDatabase());
+}
+
+/**
+ * Gets the results from the UTF-16 database using the given collation and
+ * ensures that they match gStrings sorted by the same collation.
+ *
+ * @param aCollation
+ * The name of one of our custom locale collations. The rows from the
+ * database and the expected results are ordered by this collation.
+ */
+function runUtf16Test(aCollation)
+{
+ runTest(aCollation, gUtf16Conn);
+}
+
+/**
+ * Sets up the test suite.
+ */
+function setup()
+{
+ print("-- Setting up the test_locale_collation.js suite.");
+
+ gStrings = readTestData();
+
+ initTableWithStrings(gStrings, getOpenedDatabase());
+
+ gUtf16Conn = createUtf16Database();
+ initTableWithStrings(gStrings, gUtf16Conn);
+
+ let localeSvc = Cc["@mozilla.org/intl/nslocaleservice;1"].
+ getService(Ci.nsILocaleService);
+ let collFact = Cc["@mozilla.org/intl/collation-factory;1"].
+ createInstance(Ci.nsICollationFactory);
+ gLocaleCollation = collFact.CreateCollation(localeSvc.getApplicationLocale());
+}
+
+// Test Runs
+
+var gTests = [
+ {
+ desc: "Case and accent sensitive UTF-8",
+ run: () => runUtf8Test("locale_case_accent_sensitive")
+ },
+
+ {
+ desc: "Case sensitive, accent insensitive UTF-8",
+ run: () => runUtf8Test("locale_case_sensitive")
+ },
+
+ {
+ desc: "Case insensitive, accent sensitive UTF-8",
+ run: () => runUtf8Test("locale_accent_sensitive")
+ },
+
+ {
+ desc: "Case and accent insensitive UTF-8",
+ run: () => runUtf8Test("locale")
+ },
+
+ {
+ desc: "Case and accent sensitive UTF-16",
+ run: () => runUtf16Test("locale_case_accent_sensitive")
+ },
+
+ {
+ desc: "Case sensitive, accent insensitive UTF-16",
+ run: () => runUtf16Test("locale_case_sensitive")
+ },
+
+ {
+ desc: "Case insensitive, accent sensitive UTF-16",
+ run: () => runUtf16Test("locale_accent_sensitive")
+ },
+
+ {
+ desc: "Case and accent insensitive UTF-16",
+ run: () => runUtf16Test("locale")
+ },
+];
+
+function run_test()
+{
+ setup();
+ gTests.forEach(function (test) {
+ print("-- Running test: " + test.desc);
+ test.run();
+ });
+}
diff --git a/storage/test/unit/test_page_size_is_32k.js b/storage/test/unit/test_page_size_is_32k.js
new file mode 100644
index 000000000..a2548d1e6
--- /dev/null
+++ b/storage/test/unit/test_page_size_is_32k.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This file tests that dbs are using 32k pagesize
+
+const kExpectedPageSize = 32768; // 32K
+const kExpectedCacheSize = -2048; // 2MiB
+
+function check_size(db)
+{
+ var stmt = db.createStatement("PRAGMA page_size");
+ stmt.executeStep();
+ do_check_eq(stmt.getInt32(0), kExpectedPageSize);
+ stmt.finalize();
+ stmt = db.createStatement("PRAGMA cache_size");
+ stmt.executeStep();
+ do_check_eq(stmt.getInt32(0), kExpectedCacheSize);
+ stmt.finalize();
+}
+
+function new_file(name)
+{
+ var file = dirSvc.get("ProfD", Ci.nsIFile);
+ file.append(name + ".sqlite");
+ do_check_false(file.exists());
+ return file;
+}
+
+function run_test()
+{
+ check_size(getDatabase(new_file("shared32k")));
+ check_size(getService().openUnsharedDatabase(new_file("unshared32k")));
+}
+
diff --git a/storage/test/unit/test_sqlite_secure_delete.js b/storage/test/unit/test_sqlite_secure_delete.js
new file mode 100644
index 000000000..1eff34f70
--- /dev/null
+++ b/storage/test/unit/test_sqlite_secure_delete.js
@@ -0,0 +1,80 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ *vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This file tests to make sure that SQLite was compiled with
+ * SQLITE_SECURE_DELETE=1.
+ */
+
+// Helper Methods
+
+/**
+ * Reads the contents of a file and returns it as a string.
+ *
+ * @param aFile
+ * The file to return from.
+ * @return the contents of the file in the form of a string.
+ */
+function getFileContents(aFile)
+{
+ let fstream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ fstream.init(aFile, -1, 0, 0);
+
+ let bstream = Cc["@mozilla.org/binaryinputstream;1"].
+ createInstance(Ci.nsIBinaryInputStream);
+ bstream.setInputStream(fstream);
+ return bstream.readBytes(bstream.available());
+}
+
+// Tests
+
+add_test(function test_delete_removes_data() {
+ const TEST_STRING = "SomeRandomStringToFind";
+
+ let file = getTestDB();
+ let db = getService().openDatabase(file);
+
+ // Create the table and insert the data.
+ db.createTable("test", "data TEXT");
+ let stmt = db.createStatement("INSERT INTO test VALUES(:data)");
+ stmt.params.data = TEST_STRING;
+ try {
+ stmt.execute();
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ // Make sure this test is actually testing what it thinks by making sure the
+ // string shows up in the database. Because the previous statement was
+ // automatically wrapped in a transaction, the contents are already on disk.
+ let contents = getFileContents(file);
+ do_check_neq(-1, contents.indexOf(TEST_STRING));
+
+ // Delete the data, and then close the database.
+ stmt = db.createStatement("DELETE FROM test WHERE data = :data");
+ stmt.params.data = TEST_STRING;
+ try {
+ stmt.execute();
+ }
+ finally {
+ stmt.finalize();
+ }
+ db.close();
+
+ // Check the file to see if the string can be found.
+ contents = getFileContents(file);
+ do_check_eq(-1, contents.indexOf(TEST_STRING));
+
+ run_next_test();
+});
+
+function run_test()
+{
+ cleanup();
+ run_next_test();
+}
diff --git a/storage/test/unit/test_statement_executeAsync.js b/storage/test/unit/test_statement_executeAsync.js
new file mode 100644
index 000000000..edcecb999
--- /dev/null
+++ b/storage/test/unit/test_statement_executeAsync.js
@@ -0,0 +1,998 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * This file tests the functionality of mozIStorageBaseStatement::executeAsync
+ * for both mozIStorageStatement and mozIStorageAsyncStatement.
+ */
+
+const INTEGER = 1;
+const TEXT = "this is test text";
+const REAL = 3.23;
+const BLOB = [1, 2];
+
+/**
+ * Execute the given statement asynchronously, spinning an event loop until the
+ * async statement completes.
+ *
+ * @param aStmt
+ * The statement to execute.
+ * @param [aOptions={}]
+ * @param [aOptions.error=false]
+ * If true we should expect an error whose code we do not care about. If
+ * a numeric value, that's the error code we expect and require. If we
+ * are expecting an error, we expect a completion reason of REASON_ERROR.
+ * Otherwise we expect no error notification and a completion reason of
+ * REASON_FINISHED.
+ * @param [aOptions.cancel]
+ * If true we cancel the pending statement and additionally return the
+ * pending statement in case you want to further manipulate it.
+ * @param [aOptions.returnPending=false]
+ * If true we keep the pending statement around and return it to you. We
+ * normally avoid doing this to try and minimize the amount of time a
+ * reference is held to the returned pending statement.
+ * @param [aResults]
+ * If omitted, we assume no results rows are expected. If it is a
+ * number, we assume it is the number of results rows expected. If it is
+ * a function, we assume it is a function that takes the 1) result row
+ * number, 2) result tuple, 3) call stack for the original call to
+ * execAsync as arguments. If it is a list, we currently assume it is a
+ * list of functions where each function is intended to evaluate the
+ * result row at that ordinal position and takes the result tuple and
+ * the call stack for the original call.
+ */
+function execAsync(aStmt, aOptions, aResults)
+{
+ let caller = Components.stack.caller;
+ if (aOptions == null)
+ aOptions = {};
+
+ let resultsExpected;
+ let resultsChecker;
+ if (aResults == null) {
+ resultsExpected = 0;
+ }
+ else if (typeof aResults == "number") {
+ resultsExpected = aResults;
+ }
+ else if (typeof aResults == "function") {
+ resultsChecker = aResults;
+ }
+ else { // array
+ resultsExpected = aResults.length;
+ resultsChecker = function (aResultNum, aTup, aCaller) {
+ aResults[aResultNum](aTup, aCaller);
+ };
+ }
+ let resultsSeen = 0;
+
+ let errorCodeExpected = false;
+ let reasonExpected = Ci.mozIStorageStatementCallback.REASON_FINISHED;
+ let altReasonExpected = null;
+ if ("error" in aOptions) {
+ errorCodeExpected = aOptions.error;
+ if (errorCodeExpected)
+ reasonExpected = Ci.mozIStorageStatementCallback.REASON_ERROR;
+ }
+ let errorCodeSeen = false;
+
+ if ("cancel" in aOptions && aOptions.cancel)
+ altReasonExpected = Ci.mozIStorageStatementCallback.REASON_CANCELED;
+
+ let completed = false;
+
+ let listener = {
+ handleResult(aResultSet)
+ {
+ let row, resultsSeenThisCall = 0;
+ while ((row = aResultSet.getNextRow()) != null) {
+ if (resultsChecker)
+ resultsChecker(resultsSeen, row, caller);
+ resultsSeen++;
+ resultsSeenThisCall++;
+ }
+
+ if (!resultsSeenThisCall)
+ do_throw("handleResult invoked with 0 result rows!");
+ },
+ handleError(aError)
+ {
+ if (errorCodeSeen != false)
+ do_throw("handleError called when we already had an error!");
+ errorCodeSeen = aError.result;
+ },
+ handleCompletion(aReason)
+ {
+ if (completed) // paranoia check
+ do_throw("Received a second handleCompletion notification!", caller);
+
+ if (resultsSeen != resultsExpected)
+ do_throw("Expected " + resultsExpected + " rows of results but " +
+ "got " + resultsSeen + " rows!", caller);
+
+ if (errorCodeExpected == true && errorCodeSeen == false)
+ do_throw("Expected an error, but did not see one.", caller);
+ else if (errorCodeExpected != errorCodeSeen)
+ do_throw("Expected error code " + errorCodeExpected + " but got " +
+ errorCodeSeen, caller);
+
+ if (aReason != reasonExpected && aReason != altReasonExpected)
+ do_throw("Expected reason " + reasonExpected +
+ (altReasonExpected ? (" or " + altReasonExpected) : "") +
+ " but got " + aReason, caller);
+
+ completed = true;
+ }
+ };
+
+ let pending;
+ // Only get a pending reference if we're supposed to do.
+ // (note: This does not stop XPConnect from holding onto one currently.)
+ if (("cancel" in aOptions && aOptions.cancel) ||
+ ("returnPending" in aOptions && aOptions.returnPending)) {
+ pending = aStmt.executeAsync(listener);
+ }
+ else {
+ aStmt.executeAsync(listener);
+ }
+
+ if ("cancel" in aOptions && aOptions.cancel)
+ pending.cancel();
+
+ let curThread = Components.classes["@mozilla.org/thread-manager;1"]
+ .getService().currentThread;
+ while (!completed && !_quit)
+ curThread.processNextEvent(true);
+
+ return pending;
+}
+
+/**
+ * Make sure that illegal SQL generates the expected runtime error and does not
+ * result in any crashes. Async-only since the synchronous case generates the
+ * error synchronously (and is tested elsewhere).
+ */
+function test_illegal_sql_async_deferred()
+{
+ // gibberish
+ let stmt = makeTestStatement("I AM A ROBOT. DO AS I SAY.");
+ execAsync(stmt, {error: Ci.mozIStorageError.ERROR});
+ stmt.finalize();
+
+ // legal SQL syntax, but with semantics issues.
+ stmt = makeTestStatement("SELECT destination FROM funkytown");
+ execAsync(stmt, {error: Ci.mozIStorageError.ERROR});
+ stmt.finalize();
+
+ run_next_test();
+}
+test_illegal_sql_async_deferred.asyncOnly = true;
+
+function test_create_table()
+{
+ // Ensure our table doesn't exist
+ do_check_false(getOpenedDatabase().tableExists("test"));
+
+ var stmt = makeTestStatement(
+ "CREATE TABLE test (" +
+ "id INTEGER, " +
+ "string TEXT, " +
+ "number REAL, " +
+ "nuller NULL, " +
+ "blober BLOB" +
+ ")"
+ );
+ execAsync(stmt);
+ stmt.finalize();
+
+ // Check that the table has been created
+ do_check_true(getOpenedDatabase().tableExists("test"));
+
+ // Verify that it's created correctly (this will throw if it wasn't)
+ let checkStmt = getOpenedDatabase().createStatement(
+ "SELECT id, string, number, nuller, blober FROM test"
+ );
+ checkStmt.finalize();
+ run_next_test();
+}
+
+function test_add_data()
+{
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (?, ?, ?, ?, ?)"
+ );
+ stmt.bindBlobByIndex(4, BLOB, BLOB.length);
+ stmt.bindByIndex(3, null);
+ stmt.bindByIndex(2, REAL);
+ stmt.bindByIndex(1, TEXT);
+ stmt.bindByIndex(0, INTEGER);
+
+ execAsync(stmt);
+ stmt.finalize();
+
+ // Check that the result is in the table
+ verifyQuery("SELECT string, number, nuller, blober FROM test WHERE id = ?",
+ INTEGER,
+ [TEXT, REAL, null, BLOB]);
+ run_next_test();
+}
+
+function test_get_data()
+{
+ var stmt = makeTestStatement(
+ "SELECT string, number, nuller, blober, id FROM test WHERE id = ?"
+ );
+ stmt.bindByIndex(0, INTEGER);
+ execAsync(stmt, {}, [
+ function (tuple) {
+ do_check_neq(null, tuple);
+
+ // Check that it's what we expect
+ do_check_false(tuple.getIsNull(0));
+ do_check_eq(tuple.getResultByName("string"), tuple.getResultByIndex(0));
+ do_check_eq(TEXT, tuple.getResultByName("string"));
+ do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_TEXT,
+ tuple.getTypeOfIndex(0));
+
+ do_check_false(tuple.getIsNull(1));
+ do_check_eq(tuple.getResultByName("number"), tuple.getResultByIndex(1));
+ do_check_eq(REAL, tuple.getResultByName("number"));
+ do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT,
+ tuple.getTypeOfIndex(1));
+
+ do_check_true(tuple.getIsNull(2));
+ do_check_eq(tuple.getResultByName("nuller"), tuple.getResultByIndex(2));
+ do_check_eq(null, tuple.getResultByName("nuller"));
+ do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_NULL,
+ tuple.getTypeOfIndex(2));
+
+ do_check_false(tuple.getIsNull(3));
+ var blobByName = tuple.getResultByName("blober");
+ do_check_eq(BLOB.length, blobByName.length);
+ var blobByIndex = tuple.getResultByIndex(3);
+ do_check_eq(BLOB.length, blobByIndex.length);
+ for (let i = 0; i < BLOB.length; i++) {
+ do_check_eq(BLOB[i], blobByName[i]);
+ do_check_eq(BLOB[i], blobByIndex[i]);
+ }
+ var count = { value: 0 };
+ var blob = { value: null };
+ tuple.getBlob(3, count, blob);
+ do_check_eq(BLOB.length, count.value);
+ for (let i = 0; i < BLOB.length; i++)
+ do_check_eq(BLOB[i], blob.value[i]);
+ do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_BLOB,
+ tuple.getTypeOfIndex(3));
+
+ do_check_false(tuple.getIsNull(4));
+ do_check_eq(tuple.getResultByName("id"), tuple.getResultByIndex(4));
+ do_check_eq(INTEGER, tuple.getResultByName("id"));
+ do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER,
+ tuple.getTypeOfIndex(4));
+ }]);
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_tuple_out_of_bounds()
+{
+ var stmt = makeTestStatement(
+ "SELECT string FROM test"
+ );
+ execAsync(stmt, {}, [
+ function (tuple) {
+ do_check_neq(null, tuple);
+
+ // Check all out of bounds - should throw
+ var methods = [
+ "getTypeOfIndex",
+ "getInt32",
+ "getInt64",
+ "getDouble",
+ "getUTF8String",
+ "getString",
+ "getIsNull",
+ ];
+ for (var i in methods) {
+ try {
+ tuple[methods[i]](tuple.numEntries);
+ do_throw("did not throw :(");
+ }
+ catch (e) {
+ do_check_eq(Cr.NS_ERROR_ILLEGAL_VALUE, e.result);
+ }
+ }
+
+ // getBlob requires more args...
+ try {
+ var blob = { value: null };
+ var size = { value: 0 };
+ tuple.getBlob(tuple.numEntries, blob, size);
+ do_throw("did not throw :(");
+ }
+ catch (e) {
+ do_check_eq(Cr.NS_ERROR_ILLEGAL_VALUE, e.result);
+ }
+ }]);
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_no_listener_works_on_success()
+{
+ var stmt = makeTestStatement(
+ "DELETE FROM test WHERE id = ?"
+ );
+ stmt.bindByIndex(0, 0);
+ stmt.executeAsync();
+ stmt.finalize();
+
+ // Run the next test.
+ run_next_test();
+}
+
+function test_no_listener_works_on_results()
+{
+ var stmt = makeTestStatement(
+ "SELECT ?"
+ );
+ stmt.bindByIndex(0, 1);
+ stmt.executeAsync();
+ stmt.finalize();
+
+ // Run the next test.
+ run_next_test();
+}
+
+function test_no_listener_works_on_error()
+{
+ // commit without a transaction will trigger an error
+ var stmt = makeTestStatement(
+ "COMMIT"
+ );
+ stmt.executeAsync();
+ stmt.finalize();
+
+ // Run the next test.
+ run_next_test();
+}
+
+function test_partial_listener_works()
+{
+ var stmt = makeTestStatement(
+ "DELETE FROM test WHERE id = ?"
+ );
+ stmt.bindByIndex(0, 0);
+ stmt.executeAsync({
+ handleResult(aResultSet) {}
+ });
+ stmt.executeAsync({
+ handleError(aError) {}
+ });
+ stmt.executeAsync({
+ handleCompletion(aReason) {}
+ });
+ stmt.finalize();
+
+ // Run the next test.
+ run_next_test();
+}
+
+/**
+ * Dubious cancellation test that depends on system loading may or may not
+ * succeed in canceling things. It does at least test if calling cancel blows
+ * up. test_AsyncCancellation in test_true_async.cpp is our test that canceling
+ * actually works correctly.
+ */
+function test_immediate_cancellation()
+{
+ var stmt = makeTestStatement(
+ "DELETE FROM test WHERE id = ?"
+ );
+ stmt.bindByIndex(0, 0);
+ execAsync(stmt, {cancel: true});
+ stmt.finalize();
+ run_next_test();
+}
+
+/**
+ * Test that calling cancel twice throws the second time.
+ */
+function test_double_cancellation()
+{
+ var stmt = makeTestStatement(
+ "DELETE FROM test WHERE id = ?"
+ );
+ stmt.bindByIndex(0, 0);
+ let pendingStatement = execAsync(stmt, {cancel: true});
+ // And cancel again - expect an exception
+ expectError(Cr.NS_ERROR_UNEXPECTED,
+ () => pendingStatement.cancel());
+
+ stmt.finalize();
+ run_next_test();
+}
+
+/**
+ * Verify that nothing untoward happens if we try and cancel something after it
+ * has fully run to completion.
+ */
+function test_cancellation_after_execution()
+{
+ var stmt = makeTestStatement(
+ "DELETE FROM test WHERE id = ?"
+ );
+ stmt.bindByIndex(0, 0);
+ let pendingStatement = execAsync(stmt, {returnPending: true});
+ // (the statement has fully executed at this point)
+ // canceling after the statement has run to completion should not throw!
+ pendingStatement.cancel();
+
+ stmt.finalize();
+ run_next_test();
+}
+
+/**
+ * Verifies that a single statement can be executed more than once. Might once
+ * have been intended to also ensure that callback notifications were not
+ * incorrectly interleaved, but that part was brittle (it's totally fine for
+ * handleResult to get called multiple times) and not comprehensive.
+ */
+function test_double_execute()
+{
+ var stmt = makeTestStatement(
+ "SELECT 1"
+ );
+ execAsync(stmt, null, 1);
+ execAsync(stmt, null, 1);
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_finalized_statement_does_not_crash()
+{
+ var stmt = makeTestStatement(
+ "SELECT * FROM TEST"
+ );
+ stmt.finalize();
+ // we are concerned about a crash here; an error is fine.
+ try {
+ stmt.executeAsync();
+ } catch (ex) {
+ // Do nothing.
+ }
+
+ // Run the next test.
+ run_next_test();
+}
+
+/**
+ * Bind by mozIStorageBindingParams on the mozIStorageBaseStatement by index.
+ */
+function test_bind_direct_binding_params_by_index()
+{
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (?, ?, ?, ?, ?)"
+ );
+ let insertId = nextUniqueId++;
+ stmt.bindByIndex(0, insertId);
+ stmt.bindByIndex(1, TEXT);
+ stmt.bindByIndex(2, REAL);
+ stmt.bindByIndex(3, null);
+ stmt.bindBlobByIndex(4, BLOB, BLOB.length);
+ execAsync(stmt);
+ stmt.finalize();
+ verifyQuery("SELECT string, number, nuller, blober FROM test WHERE id = ?",
+ insertId,
+ [TEXT, REAL, null, BLOB]);
+ run_next_test();
+}
+
+/**
+ * Bind by mozIStorageBindingParams on the mozIStorageBaseStatement by name.
+ */
+function test_bind_direct_binding_params_by_name()
+{
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (:int, :text, :real, :null, :blob)"
+ );
+ let insertId = nextUniqueId++;
+ stmt.bindByName("int", insertId);
+ stmt.bindByName("text", TEXT);
+ stmt.bindByName("real", REAL);
+ stmt.bindByName("null", null);
+ stmt.bindBlobByName("blob", BLOB, BLOB.length);
+ execAsync(stmt);
+ stmt.finalize();
+ verifyQuery("SELECT string, number, nuller, blober FROM test WHERE id = ?",
+ insertId,
+ [TEXT, REAL, null, BLOB]);
+ run_next_test();
+}
+
+function test_bind_js_params_helper_by_index()
+{
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (?, ?, ?, ?, NULL)"
+ );
+ let insertId = nextUniqueId++;
+ // we cannot bind blobs this way; no blober
+ stmt.params[3] = null;
+ stmt.params[2] = REAL;
+ stmt.params[1] = TEXT;
+ stmt.params[0] = insertId;
+ execAsync(stmt);
+ stmt.finalize();
+ verifyQuery("SELECT string, number, nuller FROM test WHERE id = ?", insertId,
+ [TEXT, REAL, null]);
+ run_next_test();
+}
+
+function test_bind_js_params_helper_by_name()
+{
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (:int, :text, :real, :null, NULL)"
+ );
+ let insertId = nextUniqueId++;
+ // we cannot bind blobs this way; no blober
+ stmt.params.null = null;
+ stmt.params.real = REAL;
+ stmt.params.text = TEXT;
+ stmt.params.int = insertId;
+ execAsync(stmt);
+ stmt.finalize();
+ verifyQuery("SELECT string, number, nuller FROM test WHERE id = ?", insertId,
+ [TEXT, REAL, null]);
+ run_next_test();
+}
+
+function test_bind_multiple_rows_by_index()
+{
+ const AMOUNT_TO_ADD = 5;
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (?, ?, ?, ?, ?)"
+ );
+ var array = stmt.newBindingParamsArray();
+ for (let i = 0; i < AMOUNT_TO_ADD; i++) {
+ let bp = array.newBindingParams();
+ bp.bindByIndex(0, INTEGER);
+ bp.bindByIndex(1, TEXT);
+ bp.bindByIndex(2, REAL);
+ bp.bindByIndex(3, null);
+ bp.bindBlobByIndex(4, BLOB, BLOB.length);
+ array.addParams(bp);
+ do_check_eq(array.length, i + 1);
+ }
+ stmt.bindParameters(array);
+
+ let rowCount = getTableRowCount("test");
+ execAsync(stmt);
+ do_check_eq(rowCount + AMOUNT_TO_ADD, getTableRowCount("test"));
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_bind_multiple_rows_by_name()
+{
+ const AMOUNT_TO_ADD = 5;
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (:int, :text, :real, :null, :blob)"
+ );
+ var array = stmt.newBindingParamsArray();
+ for (let i = 0; i < AMOUNT_TO_ADD; i++) {
+ let bp = array.newBindingParams();
+ bp.bindByName("int", INTEGER);
+ bp.bindByName("text", TEXT);
+ bp.bindByName("real", REAL);
+ bp.bindByName("null", null);
+ bp.bindBlobByName("blob", BLOB, BLOB.length);
+ array.addParams(bp);
+ do_check_eq(array.length, i + 1);
+ }
+ stmt.bindParameters(array);
+
+ let rowCount = getTableRowCount("test");
+ execAsync(stmt);
+ do_check_eq(rowCount + AMOUNT_TO_ADD, getTableRowCount("test"));
+ stmt.finalize();
+ run_next_test();
+}
+
+/**
+ * Verify that a mozIStorageStatement instance throws immediately when we
+ * try and bind to an illegal index.
+ */
+function test_bind_out_of_bounds_sync_immediate()
+{
+ let stmt = makeTestStatement(
+ "INSERT INTO test (id) " +
+ "VALUES (?)"
+ );
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+
+ // Check variant binding.
+ expectError(Cr.NS_ERROR_INVALID_ARG,
+ () => bp.bindByIndex(1, INTEGER));
+ // Check blob binding.
+ expectError(Cr.NS_ERROR_INVALID_ARG,
+ () => bp.bindBlobByIndex(1, BLOB, BLOB.length));
+
+ stmt.finalize();
+ run_next_test();
+}
+test_bind_out_of_bounds_sync_immediate.syncOnly = true;
+
+/**
+ * Verify that a mozIStorageAsyncStatement reports an error asynchronously when
+ * we bind to an illegal index.
+ */
+function test_bind_out_of_bounds_async_deferred()
+{
+ let stmt = makeTestStatement(
+ "INSERT INTO test (id) " +
+ "VALUES (?)"
+ );
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+
+ // There is no difference between variant and blob binding for async purposes.
+ bp.bindByIndex(1, INTEGER);
+ array.addParams(bp);
+ stmt.bindParameters(array);
+ execAsync(stmt, {error: Ci.mozIStorageError.RANGE});
+
+ stmt.finalize();
+ run_next_test();
+}
+test_bind_out_of_bounds_async_deferred.asyncOnly = true;
+
+function test_bind_no_such_name_sync_immediate()
+{
+ let stmt = makeTestStatement(
+ "INSERT INTO test (id) " +
+ "VALUES (:foo)"
+ );
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+
+ // Check variant binding.
+ expectError(Cr.NS_ERROR_INVALID_ARG,
+ () => bp.bindByName("doesnotexist", INTEGER));
+ // Check blob binding.
+ expectError(Cr.NS_ERROR_INVALID_ARG,
+ () => bp.bindBlobByName("doesnotexist", BLOB, BLOB.length));
+
+ stmt.finalize();
+ run_next_test();
+}
+test_bind_no_such_name_sync_immediate.syncOnly = true;
+
+function test_bind_no_such_name_async_deferred()
+{
+ let stmt = makeTestStatement(
+ "INSERT INTO test (id) " +
+ "VALUES (:foo)"
+ );
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+
+ bp.bindByName("doesnotexist", INTEGER);
+ array.addParams(bp);
+ stmt.bindParameters(array);
+ execAsync(stmt, {error: Ci.mozIStorageError.RANGE});
+
+ stmt.finalize();
+ run_next_test();
+}
+test_bind_no_such_name_async_deferred.asyncOnly = true;
+
+function test_bind_bogus_type_by_index()
+{
+ // We try to bind a JS Object here that should fail to bind.
+ let stmt = makeTestStatement(
+ "INSERT INTO test (blober) " +
+ "VALUES (?)"
+ );
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+ Assert.throws(() => bp.bindByIndex(0, run_test), /NS_ERROR_UNEXPECTED/);
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_bind_bogus_type_by_name()
+{
+ // We try to bind a JS Object here that should fail to bind.
+ let stmt = makeTestStatement(
+ "INSERT INTO test (blober) " +
+ "VALUES (:blob)"
+ );
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+ Assert.throws(() => bp.bindByName("blob", run_test), /NS_ERROR_UNEXPECTED/);
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_bind_params_already_locked()
+{
+ let stmt = makeTestStatement(
+ "INSERT INTO test (id) " +
+ "VALUES (:int)"
+ );
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+ bp.bindByName("int", INTEGER);
+ array.addParams(bp);
+
+ // We should get an error after we call addParams and try to bind again.
+ expectError(Cr.NS_ERROR_UNEXPECTED,
+ () => bp.bindByName("int", INTEGER));
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_bind_params_array_already_locked()
+{
+ let stmt = makeTestStatement(
+ "INSERT INTO test (id) " +
+ "VALUES (:int)"
+ );
+
+ let array = stmt.newBindingParamsArray();
+ let bp1 = array.newBindingParams();
+ bp1.bindByName("int", INTEGER);
+ array.addParams(bp1);
+ let bp2 = array.newBindingParams();
+ stmt.bindParameters(array);
+ bp2.bindByName("int", INTEGER);
+
+ // We should get an error after we have bound the array to the statement.
+ expectError(Cr.NS_ERROR_UNEXPECTED,
+ () => array.addParams(bp2));
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_no_binding_params_from_locked_array()
+{
+ let stmt = makeTestStatement(
+ "INSERT INTO test (id) " +
+ "VALUES (:int)"
+ );
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+ bp.bindByName("int", INTEGER);
+ array.addParams(bp);
+ stmt.bindParameters(array);
+
+ // We should not be able to get a new BindingParams object after we have bound
+ // to the statement.
+ expectError(Cr.NS_ERROR_UNEXPECTED,
+ () => array.newBindingParams());
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_not_right_owning_array()
+{
+ let stmt = makeTestStatement(
+ "INSERT INTO test (id) " +
+ "VALUES (:int)"
+ );
+
+ let array1 = stmt.newBindingParamsArray();
+ let array2 = stmt.newBindingParamsArray();
+ let bp = array1.newBindingParams();
+ bp.bindByName("int", INTEGER);
+
+ // We should not be able to add bp to array2 since it was created from array1.
+ expectError(Cr.NS_ERROR_UNEXPECTED,
+ () => array2.addParams(bp));
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_not_right_owning_statement()
+{
+ let stmt1 = makeTestStatement(
+ "INSERT INTO test (id) " +
+ "VALUES (:int)"
+ );
+ let stmt2 = makeTestStatement(
+ "INSERT INTO test (id) " +
+ "VALUES (:int)"
+ );
+
+ let array1 = stmt1.newBindingParamsArray();
+ let array2 = stmt2.newBindingParamsArray();
+ let bp = array1.newBindingParams();
+ bp.bindByName("int", INTEGER);
+ array1.addParams(bp);
+
+ // We should not be able to bind array1 since it was created from stmt1.
+ expectError(Cr.NS_ERROR_UNEXPECTED,
+ () => stmt2.bindParameters(array1));
+
+ stmt1.finalize();
+ stmt2.finalize();
+ run_next_test();
+}
+
+function test_bind_empty_array()
+{
+ let stmt = makeTestStatement(
+ "INSERT INTO test (id) " +
+ "VALUES (:int)"
+ );
+
+ let paramsArray = stmt.newBindingParamsArray();
+
+ // We should not be able to bind this array to the statement because it is
+ // empty.
+ expectError(Cr.NS_ERROR_UNEXPECTED,
+ () => stmt.bindParameters(paramsArray));
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_multiple_results()
+{
+ let expectedResults = getTableRowCount("test");
+ // Sanity check - we should have more than one result, but let's be sure.
+ do_check_true(expectedResults > 1);
+
+ // Now check that we get back two rows of data from our async query.
+ let stmt = makeTestStatement("SELECT * FROM test");
+ execAsync(stmt, {}, expectedResults);
+
+ stmt.finalize();
+ run_next_test();
+}
+
+// Test Runner
+
+const TEST_PASS_SYNC = 0;
+const TEST_PASS_ASYNC = 1;
+/**
+ * We run 2 passes against the test. One where makeTestStatement generates
+ * synchronous (mozIStorageStatement) statements and one where it generates
+ * asynchronous (mozIStorageAsyncStatement) statements.
+ *
+ * Because of differences in the ability to know the number of parameters before
+ * dispatching, some tests are sync/async specific. These functions are marked
+ * with 'syncOnly' or 'asyncOnly' attributes and run_next_test knows what to do.
+ */
+var testPass = TEST_PASS_SYNC;
+
+/**
+ * Create a statement of the type under test per testPass.
+ *
+ * @param aSQL
+ * The SQL string from which to build a statement.
+ * @return a statement of the type under test per testPass.
+ */
+function makeTestStatement(aSQL) {
+ if (testPass == TEST_PASS_SYNC) {
+ return getOpenedDatabase().createStatement(aSQL);
+ }
+ return getOpenedDatabase().createAsyncStatement(aSQL);
+}
+
+var tests = [
+ test_illegal_sql_async_deferred,
+ test_create_table,
+ test_add_data,
+ test_get_data,
+ test_tuple_out_of_bounds,
+ test_no_listener_works_on_success,
+ test_no_listener_works_on_results,
+ test_no_listener_works_on_error,
+ test_partial_listener_works,
+ test_immediate_cancellation,
+ test_double_cancellation,
+ test_cancellation_after_execution,
+ test_double_execute,
+ test_finalized_statement_does_not_crash,
+ test_bind_direct_binding_params_by_index,
+ test_bind_direct_binding_params_by_name,
+ test_bind_js_params_helper_by_index,
+ test_bind_js_params_helper_by_name,
+ test_bind_multiple_rows_by_index,
+ test_bind_multiple_rows_by_name,
+ test_bind_out_of_bounds_sync_immediate,
+ test_bind_out_of_bounds_async_deferred,
+ test_bind_no_such_name_sync_immediate,
+ test_bind_no_such_name_async_deferred,
+ test_bind_bogus_type_by_index,
+ test_bind_bogus_type_by_name,
+ test_bind_params_already_locked,
+ test_bind_params_array_already_locked,
+ test_bind_empty_array,
+ test_no_binding_params_from_locked_array,
+ test_not_right_owning_array,
+ test_not_right_owning_statement,
+ test_multiple_results,
+];
+var index = 0;
+
+const STARTING_UNIQUE_ID = 2;
+var nextUniqueId = STARTING_UNIQUE_ID;
+
+function run_next_test()
+{
+ function _run_next_test() {
+ // use a loop so we can skip tests...
+ while (index < tests.length) {
+ let test = tests[index++];
+ // skip tests not appropriate to the current test pass
+ if ((testPass == TEST_PASS_SYNC && ("asyncOnly" in test)) ||
+ (testPass == TEST_PASS_ASYNC && ("syncOnly" in test)))
+ continue;
+
+ // Asynchronous tests means that exceptions don't kill the test.
+ try {
+ print("****** Running the next test: " + test.name);
+ test();
+ return;
+ }
+ catch (e) {
+ do_throw(e);
+ }
+ }
+
+ // if we only completed the first pass, move to the next pass
+ if (testPass == TEST_PASS_SYNC) {
+ print("********* Beginning mozIStorageAsyncStatement pass.");
+ testPass++;
+ index = 0;
+ // a new pass demands a new database
+ asyncCleanup();
+ nextUniqueId = STARTING_UNIQUE_ID;
+ _run_next_test();
+ return;
+ }
+
+ // we did some async stuff; we need to clean up.
+ asyncCleanup();
+ do_test_finished();
+ }
+
+ // Don't actually schedule another test if we're quitting.
+ if (!_quit) {
+ // For saner stacks, we execute this code RSN.
+ do_execute_soon(_run_next_test);
+ }
+}
+
+function run_test()
+{
+ cleanup();
+
+ do_test_pending();
+ run_next_test();
+}
diff --git a/storage/test/unit/test_statement_wrapper_automatically.js b/storage/test/unit/test_statement_wrapper_automatically.js
new file mode 100644
index 000000000..58b27dd2d
--- /dev/null
+++ b/storage/test/unit/test_statement_wrapper_automatically.js
@@ -0,0 +1,167 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ vim:set ts=2 sw=2 sts=2 et:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file tests the functions of mozIStorageStatementWrapper
+
+function setup()
+{
+ getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, val NONE," +
+ "alt_val NONE");
+}
+
+/**
+ * A convenience wrapper for do_check_eq. Calls do_check_eq on aActualVal
+ * and aReturnedVal, with one caveat.
+ *
+ * Date objects are converted before parameter binding to PRTime's (microsecs
+ * since epoch). They are not reconverted when retrieved from the database.
+ * This function abstracts away this reconversion so that you can pass in,
+ * for example:
+ *
+ * checkVal(new Date(), aReturnedVal) // this
+ * checkVal(new Date().valueOf() * 1000.0, aReturnedVal) // instead of this
+ *
+ * Should any other types require conversion in the future, their conversions
+ * may also be abstracted away here.
+ *
+ * @param aActualVal
+ * the value inserted into the database
+ * @param aReturnedVal
+ * the value retrieved from the database
+ */
+function checkVal(aActualVal, aReturnedVal)
+{
+ if (aActualVal instanceof Date) aActualVal = aActualVal.valueOf() * 1000.0;
+ do_check_eq(aActualVal, aReturnedVal);
+}
+
+/**
+ * Removes all rows from our test table.
+ */
+function clearTable()
+{
+ var stmt = createStatement("DELETE FROM test");
+ stmt.execute();
+ stmt.finalize();
+ ensureNumRows(0);
+}
+
+/**
+ * Ensures that the number of rows in our test table is equal to aNumRows.
+ * Calls do_check_eq on aNumRows and the value retrieved by SELECT'ing COUNT(*).
+ *
+ * @param aNumRows
+ * the number of rows our test table should contain
+ */
+function ensureNumRows(aNumRows)
+{
+ var stmt = createStatement("SELECT COUNT(*) AS number FROM test");
+ do_check_true(stmt.step());
+ do_check_eq(aNumRows, stmt.row.number);
+ stmt.reset();
+ stmt.finalize();
+}
+
+/**
+ * Inserts aVal into our test table and checks that insertion was successful by
+ * retrieving the newly inserted value from the database and comparing it
+ * against aVal. aVal is bound to a single parameter.
+ *
+ * @param aVal
+ * value to insert into our test table and check
+ */
+function insertAndCheckSingleParam(aVal)
+{
+ clearTable();
+
+ var stmt = createStatement("INSERT INTO test (val) VALUES (:val)");
+ stmt.params.val = aVal;
+ stmt.execute();
+ stmt.finalize();
+
+ ensureNumRows(1);
+
+ stmt = createStatement("SELECT val FROM test WHERE id = 1");
+ do_check_true(stmt.step());
+ checkVal(aVal, stmt.row.val);
+ stmt.reset();
+ stmt.finalize();
+}
+
+/**
+ * Inserts aVal into our test table and checks that insertion was successful by
+ * retrieving the newly inserted value from the database and comparing it
+ * against aVal. aVal is bound to two separate parameters, both of which are
+ * checked against aVal.
+ *
+ * @param aVal
+ * value to insert into our test table and check
+ */
+function insertAndCheckMultipleParams(aVal)
+{
+ clearTable();
+
+ var stmt = createStatement("INSERT INTO test (val, alt_val) " +
+ "VALUES (:val, :val)");
+ stmt.params.val = aVal;
+ stmt.execute();
+ stmt.finalize();
+
+ ensureNumRows(1);
+
+ stmt = createStatement("SELECT val, alt_val FROM test WHERE id = 1");
+ do_check_true(stmt.step());
+ checkVal(aVal, stmt.row.val);
+ checkVal(aVal, stmt.row.alt_val);
+ stmt.reset();
+ stmt.finalize();
+}
+
+/**
+ * A convenience function that prints out a description of aVal using
+ * aVal.toString and aVal.toSource. Output is useful when the test fails.
+ *
+ * @param aVal
+ * a value inserted or to be inserted into our test table
+ */
+function printValDesc(aVal)
+{
+ try {
+ var toSource = aVal.toSource();
+ } catch (ex) {
+ toSource = "";
+ }
+ print("Testing value: toString=" + aVal +
+ (toSource ? " toSource=" + toSource : ""));
+}
+
+function run_test()
+{
+ setup();
+
+ // function JSValStorageStatementBinder in
+ // storage/mozStorageStatementParams.cpp tells us that the following types
+ // and only the following types are valid as statement parameters:
+ var vals = [
+ 1337, // int
+ 3.1337, // double
+ "foo", // string
+ true, // boolean
+ null, // null
+ new Date(), // Date object
+ ];
+
+ vals.forEach(function (val)
+ {
+ printValDesc(val);
+ print("Single parameter");
+ insertAndCheckSingleParam(val);
+ print("Multiple parameters");
+ insertAndCheckMultipleParams(val);
+ });
+
+ cleanup();
+}
diff --git a/storage/test/unit/test_storage_aggregates.js b/storage/test/unit/test_storage_aggregates.js
new file mode 100644
index 000000000..400aba836
--- /dev/null
+++ b/storage/test/unit/test_storage_aggregates.js
@@ -0,0 +1,116 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file tests the custom aggregate functions
+
+var testNums = [1, 2, 3, 4];
+
+function setup()
+{
+ getOpenedDatabase().createTable("function_tests", "id INTEGER PRIMARY KEY");
+
+ var stmt = createStatement("INSERT INTO function_tests (id) VALUES(?1)");
+ for (let i = 0; i < testNums.length; ++i) {
+ stmt.bindByIndex(0, testNums[i]);
+ stmt.execute();
+ }
+ stmt.reset();
+ stmt.finalize();
+}
+
+var testSquareAndSumFunction = {
+ calls: 0,
+ _sas: 0,
+
+ reset() {
+ this.calls = 0;
+ this._sas = 0;
+ },
+
+ onStep(val) {
+ ++this.calls;
+ this._sas += val.getInt32(0) * val.getInt32(0);
+ },
+
+ onFinal() {
+ var retval = this._sas;
+ this._sas = 0; // Prepare for next group
+ return retval;
+ }
+};
+
+function test_aggregate_registration()
+{
+ var msc = getOpenedDatabase();
+ msc.createAggregateFunction("test_sas_aggr", 1, testSquareAndSumFunction);
+}
+
+function test_aggregate_no_double_registration()
+{
+ var msc = getOpenedDatabase();
+ try {
+ msc.createAggregateFunction("test_sas_aggr", 2, testSquareAndSumFunction);
+ do_throw("We shouldn't get here!");
+ } catch (e) {
+ do_check_eq(Cr.NS_ERROR_FAILURE, e.result);
+ }
+}
+
+function test_aggregate_removal()
+{
+ var msc = getOpenedDatabase();
+ msc.removeFunction("test_sas_aggr");
+ // Should be Ok now
+ msc.createAggregateFunction("test_sas_aggr", 1, testSquareAndSumFunction);
+}
+
+function test_aggregate_no_aliases()
+{
+ var msc = getOpenedDatabase();
+ try {
+ msc.createAggregateFunction("test_sas_aggr2", 1, testSquareAndSumFunction);
+ do_throw("We shouldn't get here!");
+ } catch (e) {
+ do_check_eq(Cr.NS_ERROR_FAILURE, e.result);
+ }
+}
+
+function test_aggregate_call()
+{
+ var stmt = createStatement("SELECT test_sas_aggr(id) FROM function_tests");
+ while (stmt.executeStep()) {
+ // Do nothing.
+ }
+ do_check_eq(testNums.length, testSquareAndSumFunction.calls);
+ testSquareAndSumFunction.reset();
+ stmt.finalize();
+}
+
+function test_aggregate_result()
+{
+ var sas = 0;
+ for (var i = 0; i < testNums.length; ++i) {
+ sas += testNums[i] * testNums[i];
+ }
+ var stmt = createStatement("SELECT test_sas_aggr(id) FROM function_tests");
+ stmt.executeStep();
+ do_check_eq(sas, stmt.getInt32(0));
+ testSquareAndSumFunction.reset();
+ stmt.finalize();
+}
+
+var tests = [test_aggregate_registration, test_aggregate_no_double_registration,
+ test_aggregate_removal, test_aggregate_no_aliases, test_aggregate_call,
+ test_aggregate_result];
+
+function run_test()
+{
+ setup();
+
+ for (var i = 0; i < tests.length; i++) {
+ tests[i]();
+ }
+
+ cleanup();
+}
diff --git a/storage/test/unit/test_storage_connection.js b/storage/test/unit/test_storage_connection.js
new file mode 100644
index 000000000..ce98d0891
--- /dev/null
+++ b/storage/test/unit/test_storage_connection.js
@@ -0,0 +1,763 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file tests the functions of mozIStorageConnection
+
+// Test Functions
+
+add_task(function* test_connectionReady_open() {
+ // there doesn't seem to be a way for the connection to not be ready (unless
+ // we close it with mozIStorageConnection::Close(), but we don't for this).
+ // It can only fail if GetPath fails on the database file, or if we run out
+ // of memory trying to use an in-memory database
+
+ var msc = getOpenedDatabase();
+ do_check_true(msc.connectionReady);
+});
+
+add_task(function* test_connectionReady_closed() {
+ // This also tests mozIStorageConnection::Close()
+
+ var msc = getOpenedDatabase();
+ msc.close();
+ do_check_false(msc.connectionReady);
+ gDBConn = null; // this is so later tests don't start to fail.
+});
+
+add_task(function* test_databaseFile() {
+ var msc = getOpenedDatabase();
+ do_check_true(getTestDB().equals(msc.databaseFile));
+});
+
+add_task(function* test_tableExists_not_created() {
+ var msc = getOpenedDatabase();
+ do_check_false(msc.tableExists("foo"));
+});
+
+add_task(function* test_indexExists_not_created() {
+ var msc = getOpenedDatabase();
+ do_check_false(msc.indexExists("foo"));
+});
+
+add_task(function* test_temp_tableExists_and_indexExists() {
+ var msc = getOpenedDatabase();
+ msc.executeSimpleSQL("CREATE TEMP TABLE test_temp(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)");
+ do_check_true(msc.tableExists("test_temp"));
+
+ msc.executeSimpleSQL("CREATE INDEX test_temp_ind ON test_temp (name)");
+ do_check_true(msc.indexExists("test_temp_ind"));
+
+ msc.executeSimpleSQL("DROP INDEX test_temp_ind");
+ msc.executeSimpleSQL("DROP TABLE test_temp");
+});
+
+add_task(function* test_createTable_not_created() {
+ var msc = getOpenedDatabase();
+ msc.createTable("test", "id INTEGER PRIMARY KEY, name TEXT");
+ do_check_true(msc.tableExists("test"));
+});
+
+add_task(function* test_indexExists_created() {
+ var msc = getOpenedDatabase();
+ msc.executeSimpleSQL("CREATE INDEX name_ind ON test (name)");
+ do_check_true(msc.indexExists("name_ind"));
+});
+
+add_task(function* test_createTable_already_created() {
+ var msc = getOpenedDatabase();
+ do_check_true(msc.tableExists("test"));
+ Assert.throws(() => msc.createTable("test", "id INTEGER PRIMARY KEY, name TEXT"),
+ /NS_ERROR_FAILURE/);
+});
+
+add_task(function* test_attach_createTable_tableExists_indexExists() {
+ var msc = getOpenedDatabase();
+ var file = do_get_file("storage_attach.sqlite", true);
+ var msc2 = getDatabase(file);
+ msc.executeSimpleSQL("ATTACH DATABASE '" + file.path + "' AS sample");
+
+ do_check_false(msc.tableExists("sample.test"));
+ msc.createTable("sample.test", "id INTEGER PRIMARY KEY, name TEXT");
+ do_check_true(msc.tableExists("sample.test"));
+ Assert.throws(() => msc.createTable("sample.test", "id INTEGER PRIMARY KEY, name TEXT"),
+ /NS_ERROR_FAILURE/);
+
+ do_check_false(msc.indexExists("sample.test_ind"));
+ msc.executeSimpleSQL("CREATE INDEX sample.test_ind ON test (name)");
+ do_check_true(msc.indexExists("sample.test_ind"));
+
+ msc.executeSimpleSQL("DETACH DATABASE sample");
+ msc2.close();
+ try {
+ file.remove(false);
+ } catch (e) {
+ // Do nothing.
+ }
+});
+
+add_task(function* test_lastInsertRowID() {
+ var msc = getOpenedDatabase();
+ msc.executeSimpleSQL("INSERT INTO test (name) VALUES ('foo')");
+ do_check_eq(1, msc.lastInsertRowID);
+});
+
+add_task(function* test_transactionInProgress_no() {
+ var msc = getOpenedDatabase();
+ do_check_false(msc.transactionInProgress);
+});
+
+add_task(function* test_transactionInProgress_yes() {
+ var msc = getOpenedDatabase();
+ msc.beginTransaction();
+ do_check_true(msc.transactionInProgress);
+ msc.commitTransaction();
+ do_check_false(msc.transactionInProgress);
+
+ msc.beginTransaction();
+ do_check_true(msc.transactionInProgress);
+ msc.rollbackTransaction();
+ do_check_false(msc.transactionInProgress);
+});
+
+add_task(function* test_commitTransaction_no_transaction() {
+ var msc = getOpenedDatabase();
+ do_check_false(msc.transactionInProgress);
+ Assert.throws(() => msc.commitTransaction(), /NS_ERROR_UNEXPECTED/);
+});
+
+add_task(function* test_rollbackTransaction_no_transaction() {
+ var msc = getOpenedDatabase();
+ do_check_false(msc.transactionInProgress);
+ Assert.throws(() => msc.rollbackTransaction(), /NS_ERROR_UNEXPECTED/);
+});
+
+add_task(function* test_get_schemaVersion_not_set() {
+ do_check_eq(0, getOpenedDatabase().schemaVersion);
+});
+
+add_task(function* test_set_schemaVersion() {
+ var msc = getOpenedDatabase();
+ const version = 1;
+ msc.schemaVersion = version;
+ do_check_eq(version, msc.schemaVersion);
+});
+
+add_task(function* test_set_schemaVersion_same() {
+ var msc = getOpenedDatabase();
+ const version = 1;
+ msc.schemaVersion = version; // should still work ok
+ do_check_eq(version, msc.schemaVersion);
+});
+
+add_task(function* test_set_schemaVersion_negative() {
+ var msc = getOpenedDatabase();
+ const version = -1;
+ msc.schemaVersion = version;
+ do_check_eq(version, msc.schemaVersion);
+});
+
+add_task(function* test_createTable() {
+ var temp = getTestDB().parent;
+ temp.append("test_db_table");
+ try {
+ var con = getService().openDatabase(temp);
+ con.createTable("a", "");
+ } catch (e) {
+ if (temp.exists()) {
+ try {
+ temp.remove(false);
+ } catch (e2) {
+ // Do nothing.
+ }
+ }
+ do_check_true(e.result == Cr.NS_ERROR_NOT_INITIALIZED ||
+ e.result == Cr.NS_ERROR_FAILURE);
+ } finally {
+ if (con) {
+ con.close();
+ }
+ }
+});
+
+add_task(function* test_defaultSynchronousAtNormal() {
+ var msc = getOpenedDatabase();
+ var stmt = createStatement("PRAGMA synchronous;");
+ try {
+ stmt.executeStep();
+ do_check_eq(1, stmt.getInt32(0));
+ }
+ finally {
+ stmt.reset();
+ stmt.finalize();
+ }
+});
+
+// must be ran before executeAsync tests
+add_task(function* test_close_does_not_spin_event_loop() {
+ // We want to make sure that the event loop on the calling thread does not
+ // spin when close is called.
+ let event = {
+ ran: false,
+ run() {
+ this.ran = true;
+ },
+ };
+
+ // Post the event before we call close, so it would run if the event loop was
+ // spun during close.
+ let thread = Cc["@mozilla.org/thread-manager;1"].
+ getService(Ci.nsIThreadManager).
+ currentThread;
+ thread.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL);
+
+ // Sanity check, then close the database. Afterwards, we should not have ran!
+ do_check_false(event.ran);
+ getOpenedDatabase().close();
+ do_check_false(event.ran);
+
+ // Reset gDBConn so that later tests will get a new connection object.
+ gDBConn = null;
+});
+
+add_task(function* test_asyncClose_succeeds_with_finalized_async_statement() {
+ // XXX this test isn't perfect since we can't totally control when events will
+ // run. If this paticular function fails randomly, it means we have a
+ // real bug.
+
+ // We want to make sure we create a cached async statement to make sure that
+ // when we finalize our statement, we end up finalizing the async one too so
+ // close will succeed.
+ let stmt = createStatement("SELECT * FROM test");
+ stmt.executeAsync();
+ stmt.finalize();
+
+ yield asyncClose(getOpenedDatabase());
+ // Reset gDBConn so that later tests will get a new connection object.
+ gDBConn = null;
+});
+
+add_task(function* test_close_then_release_statement() {
+ // Testing the behavior in presence of a bad client that finalizes
+ // statements after the database has been closed (typically by
+ // letting the gc finalize the statement).
+ let db = getOpenedDatabase();
+ let stmt = createStatement("SELECT * FROM test -- test_close_then_release_statement");
+ db.close();
+ stmt.finalize(); // Finalize too late - this should not crash
+
+ // Reset gDBConn so that later tests will get a new connection object.
+ gDBConn = null;
+});
+
+add_task(function* test_asyncClose_then_release_statement() {
+ // Testing the behavior in presence of a bad client that finalizes
+ // statements after the database has been async closed (typically by
+ // letting the gc finalize the statement).
+ let db = getOpenedDatabase();
+ let stmt = createStatement("SELECT * FROM test -- test_asyncClose_then_release_statement");
+ yield asyncClose(db);
+ stmt.finalize(); // Finalize too late - this should not crash
+
+ // Reset gDBConn so that later tests will get a new connection object.
+ gDBConn = null;
+});
+
+add_task(function* test_close_fails_with_async_statement_ran() {
+ let deferred = Promise.defer();
+ let stmt = createStatement("SELECT * FROM test");
+ stmt.executeAsync();
+ stmt.finalize();
+
+ let db = getOpenedDatabase();
+ Assert.throws(() => db.close(), /NS_ERROR_UNEXPECTED/);
+
+ // Clean up after ourselves.
+ db.asyncClose(function () {
+ // Reset gDBConn so that later tests will get a new connection object.
+ gDBConn = null;
+ deferred.resolve();
+ });
+
+ yield deferred.promise;
+});
+
+add_task(function* test_clone_optional_param() {
+ let db1 = getService().openUnsharedDatabase(getTestDB());
+ let db2 = db1.clone();
+ do_check_true(db2.connectionReady);
+
+ // A write statement should not fail here.
+ let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)");
+ stmt.params.name = "dwitte";
+ stmt.execute();
+ stmt.finalize();
+
+ // And a read statement should succeed.
+ stmt = db2.createStatement("SELECT * FROM test");
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+
+ // Additionally check that it is a connection on the same database.
+ do_check_true(db1.databaseFile.equals(db2.databaseFile));
+
+ db1.close();
+ db2.close();
+});
+
+function* standardAsyncTest(promisedDB, name, shouldInit = false) {
+ do_print("Performing standard async test " + name);
+
+ let adb = yield promisedDB;
+ do_check_true(adb instanceof Ci.mozIStorageAsyncConnection);
+ do_check_false(adb instanceof Ci.mozIStorageConnection);
+
+ if (shouldInit) {
+ let stmt = adb.createAsyncStatement("CREATE TABLE test(name TEXT)");
+ yield executeAsync(stmt);
+ stmt.finalize();
+ }
+
+ // Generate a name to insert and fetch back
+ name = "worker bee " + Math.random() + " (" + name + ")";
+
+ let stmt = adb.createAsyncStatement("INSERT INTO test (name) VALUES (:name)");
+ stmt.params.name = name;
+ let result = yield executeAsync(stmt);
+ do_print("Request complete");
+ stmt.finalize();
+ do_check_true(Components.isSuccessCode(result));
+ do_print("Extracting data");
+ stmt = adb.createAsyncStatement("SELECT * FROM test");
+ let found = false;
+ yield executeAsync(stmt, function (results) {
+ do_print("Data has been extracted");
+ for (let row = results.getNextRow(); row != null; row = results.getNextRow()) {
+ if (row.getResultByName("name") == name) {
+ found = true;
+ break;
+ }
+ }
+ });
+ do_check_true(found);
+ stmt.finalize();
+ yield asyncClose(adb);
+
+ do_print("Standard async test " + name + " complete");
+}
+
+add_task(function* test_open_async() {
+ yield standardAsyncTest(openAsyncDatabase(getTestDB(), null), "default");
+ yield standardAsyncTest(openAsyncDatabase(getTestDB()), "no optional arg");
+ yield standardAsyncTest(openAsyncDatabase(getTestDB(),
+ {shared: false, growthIncrement: 54}), "non-default options");
+ yield standardAsyncTest(openAsyncDatabase("memory"),
+ "in-memory database", true);
+ yield standardAsyncTest(openAsyncDatabase("memory",
+ {shared: false}),
+ "in-memory database and options", true);
+
+ do_print("Testing async opening with bogus options 0");
+ let raised = false;
+ let adb = null;
+
+ try {
+ adb = yield openAsyncDatabase("memory", {shared: false, growthIncrement: 54});
+ } catch (ex) {
+ raised = true;
+ } finally {
+ if (adb) {
+ yield asyncClose(adb);
+ }
+ }
+ do_check_true(raised);
+
+ do_print("Testing async opening with bogus options 1");
+ raised = false;
+ adb = null;
+ try {
+ adb = yield openAsyncDatabase(getTestDB(), {shared: "forty-two"});
+ } catch (ex) {
+ raised = true;
+ } finally {
+ if (adb) {
+ yield asyncClose(adb);
+ }
+ }
+ do_check_true(raised);
+
+ do_print("Testing async opening with bogus options 2");
+ raised = false;
+ adb = null;
+ try {
+ adb = yield openAsyncDatabase(getTestDB(), {growthIncrement: "forty-two"});
+ } catch (ex) {
+ raised = true;
+ } finally {
+ if (adb) {
+ yield asyncClose(adb);
+ }
+ }
+ do_check_true(raised);
+});
+
+
+add_task(function* test_async_open_with_shared_cache() {
+ do_print("Testing that opening with a shared cache doesn't break stuff");
+ let adb = yield openAsyncDatabase(getTestDB(), {shared: true});
+
+ let stmt = adb.createAsyncStatement("INSERT INTO test (name) VALUES (:name)");
+ stmt.params.name = "clockworker";
+ let result = yield executeAsync(stmt);
+ do_print("Request complete");
+ stmt.finalize();
+ do_check_true(Components.isSuccessCode(result));
+ do_print("Extracting data");
+ stmt = adb.createAsyncStatement("SELECT * FROM test");
+ let found = false;
+ yield executeAsync(stmt, function (results) {
+ do_print("Data has been extracted");
+ for (let row = results.getNextRow(); row != null; row = results.getNextRow()) {
+ if (row.getResultByName("name") == "clockworker") {
+ found = true;
+ break;
+ }
+ }
+ });
+ do_check_true(found);
+ stmt.finalize();
+ yield asyncClose(adb);
+});
+
+add_task(function* test_clone_trivial_async() {
+ do_print("Open connection");
+ let db = getService().openDatabase(getTestDB());
+ do_check_true(db instanceof Ci.mozIStorageAsyncConnection);
+ do_print("AsyncClone connection");
+ let clone = yield asyncClone(db, true);
+ do_check_true(clone instanceof Ci.mozIStorageAsyncConnection);
+ do_print("Close connection");
+ yield asyncClose(db);
+ do_print("Close clone");
+ yield asyncClose(clone);
+});
+
+add_task(function* test_clone_no_optional_param_async() {
+ "use strict";
+ do_print("Testing async cloning");
+ let adb1 = yield openAsyncDatabase(getTestDB(), null);
+ do_check_true(adb1 instanceof Ci.mozIStorageAsyncConnection);
+
+ do_print("Cloning database");
+
+ let adb2 = yield asyncClone(adb1);
+ do_print("Testing that the cloned db is a mozIStorageAsyncConnection " +
+ "and not a mozIStorageConnection");
+ do_check_true(adb2 instanceof Ci.mozIStorageAsyncConnection);
+ do_check_false(adb2 instanceof Ci.mozIStorageConnection);
+
+ do_print("Inserting data into source db");
+ let stmt = adb1.
+ createAsyncStatement("INSERT INTO test (name) VALUES (:name)");
+
+ stmt.params.name = "yoric";
+ let result = yield executeAsync(stmt);
+ do_print("Request complete");
+ stmt.finalize();
+ do_check_true(Components.isSuccessCode(result));
+ do_print("Extracting data from clone db");
+ stmt = adb2.createAsyncStatement("SELECT * FROM test");
+ let found = false;
+ yield executeAsync(stmt, function (results) {
+ do_print("Data has been extracted");
+ for (let row = results.getNextRow(); row != null; row = results.getNextRow()) {
+ if (row.getResultByName("name") == "yoric") {
+ found = true;
+ break;
+ }
+ }
+ });
+ do_check_true(found);
+ stmt.finalize();
+ do_print("Closing databases");
+ yield asyncClose(adb2);
+ do_print("First db closed");
+
+ yield asyncClose(adb1);
+ do_print("Second db closed");
+});
+
+add_task(function* test_clone_readonly() {
+ let db1 = getService().openUnsharedDatabase(getTestDB());
+ let db2 = db1.clone(true);
+ do_check_true(db2.connectionReady);
+
+ // A write statement should fail here.
+ let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)");
+ stmt.params.name = "reed";
+ expectError(Cr.NS_ERROR_FILE_READ_ONLY, () => stmt.execute());
+ stmt.finalize();
+
+ // And a read statement should succeed.
+ stmt = db2.createStatement("SELECT * FROM test");
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+
+ db1.close();
+ db2.close();
+});
+
+add_task(function* test_clone_shared_readonly() {
+ let db1 = getService().openDatabase(getTestDB());
+ let db2 = db1.clone(true);
+ do_check_true(db2.connectionReady);
+
+ let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)");
+ stmt.params.name = "parker";
+ // TODO currently SQLite does not actually work correctly here. The behavior
+ // we want is commented out, and the current behavior is being tested
+ // for. Our IDL comments will have to be updated when this starts to
+ // work again.
+ stmt.execute();
+ // expectError(Components.results.NS_ERROR_FILE_READ_ONLY, () => stmt.execute());
+ stmt.finalize();
+
+ // And a read statement should succeed.
+ stmt = db2.createStatement("SELECT * FROM test");
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+
+ db1.close();
+ db2.close();
+});
+
+add_task(function* test_close_clone_fails() {
+ let calls = [
+ "openDatabase",
+ "openUnsharedDatabase",
+ ];
+ calls.forEach(function (methodName) {
+ let db = getService()[methodName](getTestDB());
+ db.close();
+ expectError(Cr.NS_ERROR_NOT_INITIALIZED, () => db.clone());
+ });
+});
+
+add_task(function* test_memory_clone_fails() {
+ let db = getService().openSpecialDatabase("memory");
+ db.close();
+ expectError(Cr.NS_ERROR_NOT_INITIALIZED, () => db.clone());
+});
+
+add_task(function* test_clone_copies_functions() {
+ const FUNC_NAME = "test_func";
+ let calls = [
+ "openDatabase",
+ "openUnsharedDatabase",
+ ];
+ let functionMethods = [
+ "createFunction",
+ "createAggregateFunction",
+ ];
+ calls.forEach(function (methodName) {
+ [true, false].forEach(function (readOnly) {
+ functionMethods.forEach(function (functionMethod) {
+ let db1 = getService()[methodName](getTestDB());
+ // Create a function for db1.
+ db1[functionMethod](FUNC_NAME, 1, {
+ onFunctionCall: () => 0,
+ onStep: () => 0,
+ onFinal: () => 0,
+ });
+
+ // Clone it, and make sure the function exists still.
+ let db2 = db1.clone(readOnly);
+ // Note: this would fail if the function did not exist.
+ let stmt = db2.createStatement("SELECT " + FUNC_NAME + "(id) FROM test");
+ stmt.finalize();
+ db1.close();
+ db2.close();
+ });
+ });
+ });
+});
+
+add_task(function* test_clone_copies_overridden_functions() {
+ const FUNC_NAME = "lower";
+ function test_func() {
+ this.called = false;
+ }
+ test_func.prototype = {
+ onFunctionCall() {
+ this.called = true;
+ },
+ onStep() {
+ this.called = true;
+ },
+ onFinal: () => 0,
+ };
+
+ let calls = [
+ "openDatabase",
+ "openUnsharedDatabase",
+ ];
+ let functionMethods = [
+ "createFunction",
+ "createAggregateFunction",
+ ];
+ calls.forEach(function (methodName) {
+ [true, false].forEach(function (readOnly) {
+ functionMethods.forEach(function (functionMethod) {
+ let db1 = getService()[methodName](getTestDB());
+ // Create a function for db1.
+ let func = new test_func();
+ db1[functionMethod](FUNC_NAME, 1, func);
+ do_check_false(func.called);
+
+ // Clone it, and make sure the function gets called.
+ let db2 = db1.clone(readOnly);
+ let stmt = db2.createStatement("SELECT " + FUNC_NAME + "(id) FROM test");
+ stmt.executeStep();
+ do_check_true(func.called);
+ stmt.finalize();
+ db1.close();
+ db2.close();
+ });
+ });
+ });
+});
+
+add_task(function* test_clone_copies_pragmas() {
+ const PRAGMAS = [
+ { name: "cache_size", value: 500, copied: true },
+ { name: "temp_store", value: 2, copied: true },
+ { name: "foreign_keys", value: 1, copied: true },
+ { name: "journal_size_limit", value: 524288, copied: true },
+ { name: "synchronous", value: 2, copied: true },
+ { name: "wal_autocheckpoint", value: 16, copied: true },
+ { name: "busy_timeout", value: 50, copied: true },
+ { name: "ignore_check_constraints", value: 1, copied: false },
+ ];
+
+ let db1 = getService().openUnsharedDatabase(getTestDB());
+
+ // Sanity check initial values are different from enforced ones.
+ PRAGMAS.forEach(function (pragma) {
+ let stmt = db1.createStatement("PRAGMA " + pragma.name);
+ do_check_true(stmt.executeStep());
+ do_check_neq(pragma.value, stmt.getInt32(0));
+ stmt.finalize();
+ });
+ // Execute pragmas.
+ PRAGMAS.forEach(function (pragma) {
+ db1.executeSimpleSQL("PRAGMA " + pragma.name + " = " + pragma.value);
+ });
+
+ let db2 = db1.clone();
+ do_check_true(db2.connectionReady);
+
+ // Check cloned connection inherited pragma values.
+ PRAGMAS.forEach(function (pragma) {
+ let stmt = db2.createStatement("PRAGMA " + pragma.name);
+ do_check_true(stmt.executeStep());
+ let validate = pragma.copied ? do_check_eq : do_check_neq;
+ validate(pragma.value, stmt.getInt32(0));
+ stmt.finalize();
+ });
+
+ db1.close();
+ db2.close();
+});
+
+add_task(function* test_readonly_clone_copies_pragmas() {
+ const PRAGMAS = [
+ { name: "cache_size", value: 500, copied: true },
+ { name: "temp_store", value: 2, copied: true },
+ { name: "foreign_keys", value: 1, copied: false },
+ { name: "journal_size_limit", value: 524288, copied: false },
+ { name: "synchronous", value: 2, copied: false },
+ { name: "wal_autocheckpoint", value: 16, copied: false },
+ { name: "busy_timeout", value: 50, copied: false },
+ { name: "ignore_check_constraints", value: 1, copied: false },
+ ];
+
+ let db1 = getService().openUnsharedDatabase(getTestDB());
+
+ // Sanity check initial values are different from enforced ones.
+ PRAGMAS.forEach(function (pragma) {
+ let stmt = db1.createStatement("PRAGMA " + pragma.name);
+ do_check_true(stmt.executeStep());
+ do_check_neq(pragma.value, stmt.getInt32(0));
+ stmt.finalize();
+ });
+ // Execute pragmas.
+ PRAGMAS.forEach(function (pragma) {
+ db1.executeSimpleSQL("PRAGMA " + pragma.name + " = " + pragma.value);
+ });
+
+ let db2 = db1.clone(true);
+ do_check_true(db2.connectionReady);
+
+ // Check cloned connection inherited pragma values.
+ PRAGMAS.forEach(function (pragma) {
+ let stmt = db2.createStatement("PRAGMA " + pragma.name);
+ do_check_true(stmt.executeStep());
+ let validate = pragma.copied ? do_check_eq : do_check_neq;
+ validate(pragma.value, stmt.getInt32(0));
+ stmt.finalize();
+ });
+
+ db1.close();
+ db2.close();
+});
+
+add_task(function* test_clone_attach_database() {
+ let db1 = getService().openUnsharedDatabase(getTestDB());
+
+ let c = 0;
+ function attachDB(conn, name) {
+ let file = dirSvc.get("ProfD", Ci.nsIFile);
+ file.append("test_storage_" + (++c) + ".sqlite");
+ let db = getService().openUnsharedDatabase(file);
+ conn.executeSimpleSQL(`ATTACH DATABASE '${db.databaseFile.path}' AS ${name}`);
+ db.close();
+ }
+ attachDB(db1, "attached_1");
+ attachDB(db1, "attached_2");
+
+ // These should not throw.
+ db1.createStatement("SELECT * FROM attached_1.sqlite_master");
+ db1.createStatement("SELECT * FROM attached_2.sqlite_master");
+
+ // R/W clone.
+ let db2 = db1.clone();
+ do_check_true(db2.connectionReady);
+
+ // These should not throw.
+ db2.createStatement("SELECT * FROM attached_1.sqlite_master");
+ db2.createStatement("SELECT * FROM attached_2.sqlite_master");
+
+ // R/O clone.
+ let db3 = db1.clone(true);
+ do_check_true(db3.connectionReady);
+
+ // These should not throw.
+ db3.createStatement("SELECT * FROM attached_1.sqlite_master");
+ db3.createStatement("SELECT * FROM attached_2.sqlite_master");
+
+ db1.close();
+ db2.close();
+ db3.close();
+});
+
+add_task(function* test_getInterface() {
+ let db = getOpenedDatabase();
+ let target = db.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEventTarget);
+ // Just check that target is non-null. Other tests will ensure that it has
+ // the correct value.
+ do_check_true(target != null);
+
+ yield asyncClose(db);
+ gDBConn = null;
+});
diff --git a/storage/test/unit/test_storage_fulltextindex.js b/storage/test/unit/test_storage_fulltextindex.js
new file mode 100644
index 000000000..1f7614067
--- /dev/null
+++ b/storage/test/unit/test_storage_fulltextindex.js
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file tests support for the fts3 (full-text index) module.
+
+// Example statements in these tests are taken from the Full Text Index page
+// on the SQLite wiki: http://www.sqlite.org/cvstrac/wiki?p=FullTextIndex
+
+function test_table_creation()
+{
+ var msc = getOpenedUnsharedDatabase();
+
+ msc.executeSimpleSQL(
+ "CREATE VIRTUAL TABLE recipe USING fts3(name, ingredients)");
+
+ do_check_true(msc.tableExists("recipe"));
+}
+
+function test_insertion()
+{
+ var msc = getOpenedUnsharedDatabase();
+
+ msc.executeSimpleSQL("INSERT INTO recipe (name, ingredients) VALUES " +
+ "('broccoli stew', 'broccoli peppers cheese tomatoes')");
+ msc.executeSimpleSQL("INSERT INTO recipe (name, ingredients) VALUES " +
+ "('pumpkin stew', 'pumpkin onions garlic celery')");
+ msc.executeSimpleSQL("INSERT INTO recipe (name, ingredients) VALUES " +
+ "('broccoli pie', 'broccoli cheese onions flour')");
+ msc.executeSimpleSQL("INSERT INTO recipe (name, ingredients) VALUES " +
+ "('pumpkin pie', 'pumpkin sugar flour butter')");
+
+ var stmt = msc.createStatement("SELECT COUNT(*) FROM recipe");
+ stmt.executeStep();
+
+ do_check_eq(stmt.getInt32(0), 4);
+
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_selection()
+{
+ var msc = getOpenedUnsharedDatabase();
+
+ var stmt = msc.createStatement(
+ "SELECT rowid, name, ingredients FROM recipe WHERE name MATCH 'pie'");
+
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.getInt32(0), 3);
+ do_check_eq(stmt.getString(1), "broccoli pie");
+ do_check_eq(stmt.getString(2), "broccoli cheese onions flour");
+
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.getInt32(0), 4);
+ do_check_eq(stmt.getString(1), "pumpkin pie");
+ do_check_eq(stmt.getString(2), "pumpkin sugar flour butter");
+
+ do_check_false(stmt.executeStep());
+
+ stmt.reset();
+ stmt.finalize();
+}
+
+var tests = [test_table_creation, test_insertion, test_selection];
+
+function run_test()
+{
+ // It's extra important to start from scratch, since these tests won't work
+ // with an existing shared cache connection, so we do it even though the last
+ // test probably did it already.
+ cleanup();
+
+ try {
+ for (var i = 0; i < tests.length; i++) {
+ tests[i]();
+ }
+ }
+ // It's extra important to clean up afterwards, since later tests that use
+ // a shared cache connection will not be able to read the database we create,
+ // so we do this in a finally block to ensure it happens even if some of our
+ // tests fail.
+ finally {
+ cleanup();
+ }
+}
diff --git a/storage/test/unit/test_storage_function.js b/storage/test/unit/test_storage_function.js
new file mode 100644
index 000000000..6532709ec
--- /dev/null
+++ b/storage/test/unit/test_storage_function.js
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file tests the custom functions
+
+var testNums = [1, 2, 3, 4];
+
+function setup()
+{
+ getOpenedDatabase().createTable("function_tests", "id INTEGER PRIMARY KEY");
+
+ var stmt = createStatement("INSERT INTO function_tests (id) VALUES(?1)");
+ for (let i = 0; i < testNums.length; ++i) {
+ stmt.bindByIndex(0, testNums[i]);
+ stmt.execute();
+ }
+ stmt.reset();
+ stmt.finalize();
+}
+
+var testSquareFunction = {
+ calls: 0,
+
+ onFunctionCall(val) {
+ ++this.calls;
+ return val.getInt32(0) * val.getInt32(0);
+ }
+};
+
+function test_function_registration()
+{
+ var msc = getOpenedDatabase();
+ msc.createFunction("test_square", 1, testSquareFunction);
+}
+
+function test_function_no_double_registration()
+{
+ var msc = getOpenedDatabase();
+ try {
+ msc.createFunction("test_square", 2, testSquareFunction);
+ do_throw("We shouldn't get here!");
+ } catch (e) {
+ do_check_eq(Cr.NS_ERROR_FAILURE, e.result);
+ }
+}
+
+function test_function_removal()
+{
+ var msc = getOpenedDatabase();
+ msc.removeFunction("test_square");
+ // Should be Ok now
+ msc.createFunction("test_square", 1, testSquareFunction);
+}
+
+function test_function_aliases()
+{
+ var msc = getOpenedDatabase();
+ msc.createFunction("test_square2", 1, testSquareFunction);
+}
+
+function test_function_call()
+{
+ var stmt = createStatement("SELECT test_square(id) FROM function_tests");
+ while (stmt.executeStep()) {
+ // Do nothing.
+ }
+ do_check_eq(testNums.length, testSquareFunction.calls);
+ testSquareFunction.calls = 0;
+ stmt.finalize();
+}
+
+function test_function_result()
+{
+ var stmt = createStatement("SELECT test_square(42) FROM function_tests");
+ stmt.executeStep();
+ do_check_eq(42 * 42, stmt.getInt32(0));
+ testSquareFunction.calls = 0;
+ stmt.finalize();
+}
+
+var tests = [test_function_registration, test_function_no_double_registration,
+ test_function_removal, test_function_aliases, test_function_call,
+ test_function_result];
+
+function run_test()
+{
+ setup();
+
+ for (var i = 0; i < tests.length; i++) {
+ tests[i]();
+ }
+
+ cleanup();
+}
diff --git a/storage/test/unit/test_storage_progresshandler.js b/storage/test/unit/test_storage_progresshandler.js
new file mode 100644
index 000000000..c06a57e83
--- /dev/null
+++ b/storage/test/unit/test_storage_progresshandler.js
@@ -0,0 +1,111 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file tests the custom progress handlers
+
+function setup()
+{
+ var msc = getOpenedDatabase();
+ msc.createTable("handler_tests", "id INTEGER PRIMARY KEY, num INTEGER");
+ msc.beginTransaction();
+
+ var stmt = createStatement("INSERT INTO handler_tests (id, num) VALUES(?1, ?2)");
+ for (let i = 0; i < 100; ++i) {
+ stmt.bindByIndex(0, i);
+ stmt.bindByIndex(1, Math.floor(Math.random() * 1000));
+ stmt.execute();
+ }
+ stmt.reset();
+ msc.commitTransaction();
+ stmt.finalize();
+}
+
+var testProgressHandler = {
+ calls: 0,
+ abort: false,
+
+ onProgress(comm) {
+ ++this.calls;
+ return this.abort;
+ }
+};
+
+function test_handler_registration()
+{
+ var msc = getOpenedDatabase();
+ msc.setProgressHandler(10, testProgressHandler);
+}
+
+function test_handler_return()
+{
+ var msc = getOpenedDatabase();
+ var oldH = msc.setProgressHandler(5, testProgressHandler);
+ do_check_true(oldH instanceof Ci.mozIStorageProgressHandler);
+}
+
+function test_handler_removal()
+{
+ var msc = getOpenedDatabase();
+ msc.removeProgressHandler();
+ var oldH = msc.removeProgressHandler();
+ do_check_eq(oldH, null);
+}
+
+function test_handler_call()
+{
+ var msc = getOpenedDatabase();
+ msc.setProgressHandler(50, testProgressHandler);
+ // Some long-executing request
+ var stmt = createStatement(
+ "SELECT SUM(t1.num * t2.num) FROM handler_tests AS t1, handler_tests AS t2");
+ while (stmt.executeStep()) {
+ // Do nothing.
+ }
+ do_check_true(testProgressHandler.calls > 0);
+ stmt.finalize();
+}
+
+function test_handler_abort()
+{
+ var msc = getOpenedDatabase();
+ testProgressHandler.abort = true;
+ msc.setProgressHandler(50, testProgressHandler);
+ // Some long-executing request
+ var stmt = createStatement(
+ "SELECT SUM(t1.num * t2.num) FROM handler_tests AS t1, handler_tests AS t2");
+
+ const SQLITE_INTERRUPT = 9;
+ try {
+ while (stmt.executeStep()) {
+ // Do nothing.
+ }
+ do_throw("We shouldn't get here!");
+ } catch (e) {
+ do_check_eq(Cr.NS_ERROR_ABORT, e.result);
+ do_check_eq(SQLITE_INTERRUPT, msc.lastError);
+ }
+ try {
+ stmt.finalize();
+ do_throw("We shouldn't get here!");
+ } catch (e) {
+ // finalize should return the error code since we encountered an error
+ do_check_eq(Cr.NS_ERROR_ABORT, e.result);
+ do_check_eq(SQLITE_INTERRUPT, msc.lastError);
+ }
+}
+
+var tests = [test_handler_registration, test_handler_return,
+ test_handler_removal, test_handler_call,
+ test_handler_abort];
+
+function run_test()
+{
+ setup();
+
+ for (var i = 0; i < tests.length; i++) {
+ tests[i]();
+ }
+
+ cleanup();
+}
diff --git a/storage/test/unit/test_storage_service.js b/storage/test/unit/test_storage_service.js
new file mode 100644
index 000000000..9cf46620e
--- /dev/null
+++ b/storage/test/unit/test_storage_service.js
@@ -0,0 +1,142 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file tests the functions of mozIStorageService except for
+// openUnsharedDatabase, which is tested by test_storage_service_unshared.js.
+
+const BACKUP_FILE_NAME = "test_storage.sqlite.backup";
+
+function test_openSpecialDatabase_invalid_arg()
+{
+ try {
+ getService().openSpecialDatabase("abcd");
+ do_throw("We should not get here!");
+ } catch (e) {
+ print(e);
+ print("e.result is " + e.result);
+ do_check_eq(Cr.NS_ERROR_INVALID_ARG, e.result);
+ }
+}
+
+function test_openDatabase_null_file()
+{
+ try {
+ getService().openDatabase(null);
+ do_throw("We should not get here!");
+ } catch (e) {
+ print(e);
+ print("e.result is " + e.result);
+ do_check_eq(Cr.NS_ERROR_INVALID_ARG, e.result);
+ }
+}
+
+function test_openUnsharedDatabase_null_file()
+{
+ try {
+ getService().openUnsharedDatabase(null);
+ do_throw("We should not get here!");
+ } catch (e) {
+ print(e);
+ print("e.result is " + e.result);
+ do_check_eq(Cr.NS_ERROR_INVALID_ARG, e.result);
+ }
+}
+
+function test_openDatabase_file_DNE()
+{
+ // the file should be created after calling
+ var db = getTestDB();
+ do_check_false(db.exists());
+ getService().openDatabase(db);
+ do_check_true(db.exists());
+}
+
+function test_openDatabase_file_exists()
+{
+ // it should already exist from our last test
+ var db = getTestDB();
+ do_check_true(db.exists());
+ getService().openDatabase(db);
+ do_check_true(db.exists());
+}
+
+function test_corrupt_db_throws_with_openDatabase()
+{
+ try {
+ getDatabase(getCorruptDB());
+ do_throw("should not be here");
+ }
+ catch (e) {
+ do_check_eq(Cr.NS_ERROR_FILE_CORRUPTED, e.result);
+ }
+}
+
+function test_fake_db_throws_with_openDatabase()
+{
+ try {
+ getDatabase(getFakeDB());
+ do_throw("should not be here");
+ }
+ catch (e) {
+ do_check_eq(Cr.NS_ERROR_FILE_CORRUPTED, e.result);
+ }
+}
+
+function test_backup_not_new_filename()
+{
+ const fname = getTestDB().leafName;
+
+ var backup = getService().backupDatabaseFile(getTestDB(), fname);
+ do_check_neq(fname, backup.leafName);
+
+ backup.remove(false);
+}
+
+function test_backup_new_filename()
+{
+ var backup = getService().backupDatabaseFile(getTestDB(), BACKUP_FILE_NAME);
+ do_check_eq(BACKUP_FILE_NAME, backup.leafName);
+
+ backup.remove(false);
+}
+
+function test_backup_new_folder()
+{
+ var parentDir = getTestDB().parent;
+ parentDir.append("test_storage_temp");
+ if (parentDir.exists())
+ parentDir.remove(true);
+ parentDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ do_check_true(parentDir.exists());
+
+ var backup = getService().backupDatabaseFile(getTestDB(), BACKUP_FILE_NAME,
+ parentDir);
+ do_check_eq(BACKUP_FILE_NAME, backup.leafName);
+ do_check_true(parentDir.equals(backup.parent));
+
+ parentDir.remove(true);
+}
+
+var tests = [
+ test_openSpecialDatabase_invalid_arg,
+ test_openDatabase_null_file,
+ test_openUnsharedDatabase_null_file,
+ test_openDatabase_file_DNE,
+ test_openDatabase_file_exists,
+ test_corrupt_db_throws_with_openDatabase,
+ test_fake_db_throws_with_openDatabase,
+ test_backup_not_new_filename,
+ test_backup_new_filename,
+ test_backup_new_folder,
+];
+
+function run_test()
+{
+ for (var i = 0; i < tests.length; i++) {
+ tests[i]();
+ }
+
+ cleanup();
+}
+
diff --git a/storage/test/unit/test_storage_service_unshared.js b/storage/test/unit/test_storage_service_unshared.js
new file mode 100644
index 000000000..70efb2a43
--- /dev/null
+++ b/storage/test/unit/test_storage_service_unshared.js
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file tests the openUnsharedDatabase function of mozIStorageService.
+
+function test_openUnsharedDatabase_file_DNE()
+{
+ // the file should be created after calling
+ var db = getTestDB();
+ do_check_false(db.exists());
+ getService().openUnsharedDatabase(db);
+ do_check_true(db.exists());
+}
+
+function test_openUnsharedDatabase_file_exists()
+{
+ // it should already exist from our last test
+ var db = getTestDB();
+ do_check_true(db.exists());
+ getService().openUnsharedDatabase(db);
+ do_check_true(db.exists());
+}
+
+var tests = [test_openUnsharedDatabase_file_DNE,
+ test_openUnsharedDatabase_file_exists];
+
+function run_test()
+{
+ for (var i = 0; i < tests.length; i++)
+ tests[i]();
+
+ cleanup();
+}
+
diff --git a/storage/test/unit/test_storage_statement.js b/storage/test/unit/test_storage_statement.js
new file mode 100644
index 000000000..026e271ac
--- /dev/null
+++ b/storage/test/unit/test_storage_statement.js
@@ -0,0 +1,184 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file tests the functions of mozIStorageStatement
+
+function setup()
+{
+ getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, name TEXT");
+}
+
+function test_parameterCount_none()
+{
+ var stmt = createStatement("SELECT * FROM test");
+ do_check_eq(0, stmt.parameterCount);
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_parameterCount_one()
+{
+ var stmt = createStatement("SELECT * FROM test WHERE id = ?1");
+ do_check_eq(1, stmt.parameterCount);
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_getParameterName()
+{
+ var stmt = createStatement("SELECT * FROM test WHERE id = :id");
+ do_check_eq(":id", stmt.getParameterName(0));
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_getParameterIndex_different()
+{
+ var stmt = createStatement("SELECT * FROM test WHERE id = :id OR name = :name");
+ do_check_eq(0, stmt.getParameterIndex("id"));
+ do_check_eq(1, stmt.getParameterIndex("name"));
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_getParameterIndex_same()
+{
+ var stmt = createStatement("SELECT * FROM test WHERE id = :test OR name = :test");
+ do_check_eq(0, stmt.getParameterIndex("test"));
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_columnCount()
+{
+ var stmt = createStatement("SELECT * FROM test WHERE id = ?1 OR name = ?2");
+ do_check_eq(2, stmt.columnCount);
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_getColumnName()
+{
+ var stmt = createStatement("SELECT name, id FROM test");
+ do_check_eq("id", stmt.getColumnName(1));
+ do_check_eq("name", stmt.getColumnName(0));
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_getColumnIndex_same_case()
+{
+ var stmt = createStatement("SELECT name, id FROM test");
+ do_check_eq(0, stmt.getColumnIndex("name"));
+ do_check_eq(1, stmt.getColumnIndex("id"));
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_getColumnIndex_different_case()
+{
+ var stmt = createStatement("SELECT name, id FROM test");
+ try {
+ do_check_eq(0, stmt.getColumnIndex("NaMe"));
+ do_throw("should not get here");
+ } catch (e) {
+ do_check_eq(Cr.NS_ERROR_INVALID_ARG, e.result);
+ }
+ try {
+ do_check_eq(1, stmt.getColumnIndex("Id"));
+ do_throw("should not get here");
+ } catch (e) {
+ do_check_eq(Cr.NS_ERROR_INVALID_ARG, e.result);
+ }
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_state_ready()
+{
+ var stmt = createStatement("SELECT name, id FROM test");
+ do_check_eq(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_READY, stmt.state);
+ stmt.reset();
+ stmt.finalize();
+}
+
+function test_state_executing()
+{
+ var stmt = createStatement("INSERT INTO test (name) VALUES ('foo')");
+ stmt.execute();
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = createStatement("SELECT name, id FROM test");
+ stmt.executeStep();
+ do_check_eq(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_EXECUTING,
+ stmt.state);
+ stmt.executeStep();
+ do_check_eq(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_EXECUTING,
+ stmt.state);
+ stmt.reset();
+ do_check_eq(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_READY, stmt.state);
+ stmt.finalize();
+}
+
+function test_state_after_finalize()
+{
+ var stmt = createStatement("SELECT name, id FROM test");
+ stmt.executeStep();
+ stmt.finalize();
+ do_check_eq(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_INVALID, stmt.state);
+}
+
+function test_failed_execute()
+{
+ var stmt = createStatement("INSERT INTO test (name) VALUES ('foo')");
+ stmt.execute();
+ stmt.finalize();
+ var id = getOpenedDatabase().lastInsertRowID;
+ stmt = createStatement("INSERT INTO test(id, name) VALUES(:id, 'bar')");
+ stmt.params.id = id;
+ try {
+ // Should throw a constraint error
+ stmt.execute();
+ do_throw("Should have seen a constraint error");
+ }
+ catch (e) {
+ do_check_eq(getOpenedDatabase().lastError, Ci.mozIStorageError.CONSTRAINT);
+ }
+ do_check_eq(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_READY, stmt.state);
+ // Should succeed without needing to reset the statement manually
+ stmt.finalize();
+}
+
+function test_bind_undefined()
+{
+ var stmt = createStatement("INSERT INTO test (name) VALUES ('foo')");
+
+ expectError(Cr.NS_ERROR_ILLEGAL_VALUE,
+ () => stmt.bindParameters(undefined));
+
+ stmt.finalize();
+}
+
+var tests = [test_parameterCount_none, test_parameterCount_one,
+ test_getParameterName, test_getParameterIndex_different,
+ test_getParameterIndex_same, test_columnCount,
+ test_getColumnName, test_getColumnIndex_same_case,
+ test_getColumnIndex_different_case, test_state_ready,
+ test_state_executing, test_state_after_finalize,
+ test_failed_execute,
+ test_bind_undefined,
+];
+
+function run_test()
+{
+ setup();
+
+ for (var i = 0; i < tests.length; i++) {
+ tests[i]();
+ }
+
+ cleanup();
+}
+
diff --git a/storage/test/unit/test_storage_value_array.js b/storage/test/unit/test_storage_value_array.js
new file mode 100644
index 000000000..27bd23992
--- /dev/null
+++ b/storage/test/unit/test_storage_value_array.js
@@ -0,0 +1,182 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file tests the functions of mozIStorageValueArray
+
+add_task(function* setup() {
+ getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, name TEXT," +
+ "number REAL, nuller NULL, blobber BLOB");
+
+ var stmt = createStatement("INSERT INTO test (name, number, blobber) " +
+ "VALUES (?1, ?2, ?3)");
+ stmt.bindByIndex(0, "foo");
+ stmt.bindByIndex(1, 2.34);
+ stmt.bindBlobByIndex(2, [], 0);
+ stmt.execute();
+
+ stmt.bindByIndex(0, "");
+ stmt.bindByIndex(1, 1.23);
+ stmt.bindBlobByIndex(2, [1, 2], 2);
+ stmt.execute();
+
+ stmt.reset();
+ stmt.finalize();
+
+ do_register_cleanup(cleanup);
+});
+
+add_task(function* test_getIsNull_for_null() {
+ var stmt = createStatement("SELECT nuller, blobber FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 1);
+ do_check_true(stmt.executeStep());
+
+ do_check_true(stmt.getIsNull(0)); // null field
+ do_check_true(stmt.getIsNull(1)); // data is null if size is 0
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_getIsNull_for_non_null() {
+ var stmt = createStatement("SELECT name, blobber FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ do_check_true(stmt.executeStep());
+
+ do_check_false(stmt.getIsNull(0));
+ do_check_false(stmt.getIsNull(1));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_value_type_null() {
+ var stmt = createStatement("SELECT nuller FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 1);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_NULL,
+ stmt.getTypeOfIndex(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_value_type_integer() {
+ var stmt = createStatement("SELECT id FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 1);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER,
+ stmt.getTypeOfIndex(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_value_type_float() {
+ var stmt = createStatement("SELECT number FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 1);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT,
+ stmt.getTypeOfIndex(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_value_type_text() {
+ var stmt = createStatement("SELECT name FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 1);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_TEXT,
+ stmt.getTypeOfIndex(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_value_type_blob() {
+ var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_BLOB,
+ stmt.getTypeOfIndex(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_numEntries_one() {
+ var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq(1, stmt.numEntries);
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_numEntries_all() {
+ var stmt = createStatement("SELECT * FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq(5, stmt.numEntries);
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_getInt() {
+ var stmt = createStatement("SELECT id FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq(2, stmt.getInt32(0));
+ do_check_eq(2, stmt.getInt64(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_getDouble() {
+ var stmt = createStatement("SELECT number FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq(1.23, stmt.getDouble(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_getUTF8String() {
+ var stmt = createStatement("SELECT name FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 1);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq("foo", stmt.getUTF8String(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_getString() {
+ var stmt = createStatement("SELECT name FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ do_check_true(stmt.executeStep());
+
+ do_check_eq("", stmt.getString(0));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_getBlob() {
+ var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1");
+ stmt.bindByIndex(0, 2);
+ do_check_true(stmt.executeStep());
+
+ var count = { value: 0 };
+ var arr = { value: null };
+ stmt.getBlob(0, count, arr);
+ do_check_eq(2, count.value);
+ do_check_eq(1, arr.value[0]);
+ do_check_eq(2, arr.value[1]);
+ stmt.reset();
+ stmt.finalize();
+});
+
+
diff --git a/storage/test/unit/test_telemetry_vfs.js b/storage/test/unit/test_telemetry_vfs.js
new file mode 100644
index 000000000..0822fe3e7
--- /dev/null
+++ b/storage/test/unit/test_telemetry_vfs.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Make sure that there are telemetry entries created by sqlite io
+
+function run_sql(d, sql) {
+ var stmt = d.createStatement(sql);
+ stmt.execute();
+ stmt.finalize();
+}
+
+function new_file(name)
+{
+ var file = dirSvc.get("ProfD", Ci.nsIFile);
+ file.append(name);
+ return file;
+}
+function run_test()
+{
+ const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
+ let read_hgram = Telemetry.getHistogramById("MOZ_SQLITE_OTHER_READ_B");
+ let old_sum = read_hgram.snapshot().sum;
+ const file = new_file("telemetry.sqlite");
+ var d = getDatabase(file);
+ run_sql(d, "CREATE TABLE bloat(data varchar)");
+ run_sql(d, "DROP TABLE bloat");
+ do_check_true(read_hgram.snapshot().sum > old_sum);
+}
+
diff --git a/storage/test/unit/test_unicode.js b/storage/test/unit/test_unicode.js
new file mode 100644
index 000000000..7753bbfdb
--- /dev/null
+++ b/storage/test/unit/test_unicode.js
@@ -0,0 +1,83 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file tests the unicode functions that we have added
+
+const LATIN1_AE = "\xc6"; // "Æ"
+const LATIN1_ae = "\xe6"; // "æ"
+
+add_task(function* setup() {
+ getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, name TEXT");
+
+ var stmt = createStatement("INSERT INTO test (name, id) VALUES (?1, ?2)");
+ stmt.bindByIndex(0, LATIN1_AE);
+ stmt.bindByIndex(1, 1);
+ stmt.execute();
+ stmt.bindByIndex(0, "A");
+ stmt.bindByIndex(1, 2);
+ stmt.execute();
+ stmt.bindByIndex(0, "b");
+ stmt.bindByIndex(1, 3);
+ stmt.execute();
+ stmt.bindByIndex(0, LATIN1_ae);
+ stmt.bindByIndex(1, 4);
+ stmt.execute();
+ stmt.finalize();
+
+ do_register_cleanup(cleanup);
+});
+
+add_task(function* test_upper_ascii() {
+ var stmt = createStatement("SELECT name, id FROM test WHERE name = upper('a')");
+ do_check_true(stmt.executeStep());
+ do_check_eq("A", stmt.getString(0));
+ do_check_eq(2, stmt.getInt32(1));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_upper_non_ascii() {
+ var stmt = createStatement("SELECT name, id FROM test WHERE name = upper(?1)");
+ stmt.bindByIndex(0, LATIN1_ae);
+ do_check_true(stmt.executeStep());
+ do_check_eq(LATIN1_AE, stmt.getString(0));
+ do_check_eq(1, stmt.getInt32(1));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_lower_ascii() {
+ var stmt = createStatement("SELECT name, id FROM test WHERE name = lower('B')");
+ do_check_true(stmt.executeStep());
+ do_check_eq("b", stmt.getString(0));
+ do_check_eq(3, stmt.getInt32(1));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_lower_non_ascii() {
+ var stmt = createStatement("SELECT name, id FROM test WHERE name = lower(?1)");
+ stmt.bindByIndex(0, LATIN1_AE);
+ do_check_true(stmt.executeStep());
+ do_check_eq(LATIN1_ae, stmt.getString(0));
+ do_check_eq(4, stmt.getInt32(1));
+ stmt.reset();
+ stmt.finalize();
+});
+
+add_task(function* test_like_search_different() {
+ var stmt = createStatement("SELECT COUNT(*) FROM test WHERE name LIKE ?1");
+ stmt.bindByIndex(0, LATIN1_AE);
+ do_check_true(stmt.executeStep());
+ do_check_eq(2, stmt.getInt32(0));
+ stmt.finalize();
+});
+
+add_task(function* test_like_search_same() {
+ var stmt = createStatement("SELECT COUNT(*) FROM test WHERE name LIKE ?1");
+ stmt.bindByIndex(0, LATIN1_ae);
+ do_check_true(stmt.executeStep());
+ do_check_eq(2, stmt.getInt32(0));
+ stmt.finalize();
+});
diff --git a/storage/test/unit/test_vacuum.js b/storage/test/unit/test_vacuum.js
new file mode 100644
index 000000000..e284f78c7
--- /dev/null
+++ b/storage/test/unit/test_vacuum.js
@@ -0,0 +1,335 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This file tests the Vacuum Manager.
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+/**
+ * Loads a test component that will register as a vacuum-participant.
+ * If other participants are found they will be unregistered, to avoid conflicts
+ * with the test itself.
+ */
+function load_test_vacuum_component()
+{
+ const CATEGORY_NAME = "vacuum-participant";
+
+ do_load_manifest("vacuumParticipant.manifest");
+
+ // This is a lazy check, there could be more participants than just this test
+ // we just mind that the test exists though.
+ const EXPECTED_ENTRIES = ["vacuumParticipant"];
+ let catMan = Cc["@mozilla.org/categorymanager;1"].
+ getService(Ci.nsICategoryManager);
+ let found = false;
+ let entries = catMan.enumerateCategory(CATEGORY_NAME);
+ while (entries.hasMoreElements()) {
+ let entry = entries.getNext().QueryInterface(Ci.nsISupportsCString).data;
+ print("Check if the found category entry (" + entry + ") is expected.");
+ if (EXPECTED_ENTRIES.indexOf(entry) != -1) {
+ print("Check that only one test entry exists.");
+ do_check_false(found);
+ found = true;
+ }
+ else {
+ // Temporary unregister other participants for this test.
+ catMan.deleteCategoryEntry("vacuum-participant", entry, false);
+ }
+ }
+ print("Check the test entry exists.");
+ do_check_true(found);
+}
+
+/**
+ * Sends a fake idle-daily notification to the VACUUM Manager.
+ */
+function synthesize_idle_daily()
+{
+ let vm = Cc["@mozilla.org/storage/vacuum;1"].getService(Ci.nsIObserver);
+ vm.observe(null, "idle-daily", null);
+}
+
+/**
+ * Returns a new nsIFile reference for a profile database.
+ * @param filename for the database, excluded the .sqlite extension.
+ */
+function new_db_file(name)
+{
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append(name + ".sqlite");
+ return file;
+}
+
+function run_test()
+{
+ do_test_pending();
+
+ // Change initial page size. Do it immediately since it would require an
+ // additional vacuum op to do it later. As a bonus this makes the page size
+ // change test really fast since it only has to check results.
+ let conn = getDatabase(new_db_file("testVacuum"));
+ conn.executeSimpleSQL("PRAGMA page_size = 1024");
+ print("Check current page size.");
+ let stmt = conn.createStatement("PRAGMA page_size");
+ try {
+ while (stmt.executeStep()) {
+ do_check_eq(stmt.row.page_size, 1024);
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ load_test_vacuum_component();
+
+ run_next_test();
+}
+
+const TESTS = [
+
+ function test_common_vacuum()
+ {
+ print("\n*** Test that a VACUUM correctly happens and all notifications are fired.");
+ // Wait for VACUUM begin.
+ let beginVacuumReceived = false;
+ Services.obs.addObserver(function onVacuum(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(onVacuum, aTopic);
+ beginVacuumReceived = true;
+ }, "test-begin-vacuum", false);
+
+ // Wait for heavy IO notifications.
+ let heavyIOTaskBeginReceived = false;
+ let heavyIOTaskEndReceived = false;
+ Services.obs.addObserver(function onVacuum(aSubject, aTopic, aData) {
+ if (heavyIOTaskBeginReceived && heavyIOTaskEndReceived) {
+ Services.obs.removeObserver(onVacuum, aTopic);
+ }
+
+ if (aData == "vacuum-begin") {
+ heavyIOTaskBeginReceived = true;
+ }
+ else if (aData == "vacuum-end") {
+ heavyIOTaskEndReceived = true;
+ }
+ }, "heavy-io-task", false);
+
+ // Wait for VACUUM end.
+ Services.obs.addObserver(function onVacuum(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(onVacuum, aTopic);
+ print("Check we received onBeginVacuum");
+ do_check_true(beginVacuumReceived);
+ print("Check we received heavy-io-task notifications");
+ do_check_true(heavyIOTaskBeginReceived);
+ do_check_true(heavyIOTaskEndReceived);
+ print("Received onEndVacuum");
+ run_next_test();
+ }, "test-end-vacuum", false);
+
+ synthesize_idle_daily();
+ },
+
+ function test_skipped_if_recent_vacuum()
+ {
+ print("\n*** Test that a VACUUM is skipped if it was run recently.");
+ Services.prefs.setIntPref("storage.vacuum.last.testVacuum.sqlite",
+ parseInt(Date.now() / 1000));
+
+ // Wait for VACUUM begin.
+ let vacuumObserver = {
+ gotNotification: false,
+ observe: function VO_observe(aSubject, aTopic, aData) {
+ this.gotNotification = true;
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+ };
+ Services.obs.addObserver(vacuumObserver, "test-begin-vacuum", false);
+
+ // Check after a couple seconds that no VACUUM has been run.
+ do_timeout(2000, function () {
+ print("Check VACUUM did not run.");
+ do_check_false(vacuumObserver.gotNotification);
+ Services.obs.removeObserver(vacuumObserver, "test-begin-vacuum");
+ run_next_test();
+ });
+
+ synthesize_idle_daily();
+ },
+
+ function test_page_size_change()
+ {
+ print("\n*** Test that a VACUUM changes page_size");
+
+ // We did setup the database with a small page size, the previous vacuum
+ // should have updated it.
+ print("Check that page size was updated.");
+ let conn = getDatabase(new_db_file("testVacuum"));
+ let stmt = conn.createStatement("PRAGMA page_size");
+ try {
+ while (stmt.executeStep()) {
+ do_check_eq(stmt.row.page_size, conn.defaultPageSize);
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ run_next_test();
+ },
+
+ function test_skipped_optout_vacuum()
+ {
+ print("\n*** Test that a VACUUM is skipped if the participant wants to opt-out.");
+ Services.obs.notifyObservers(null, "test-options", "opt-out");
+
+ // Wait for VACUUM begin.
+ let vacuumObserver = {
+ gotNotification: false,
+ observe: function VO_observe(aSubject, aTopic, aData) {
+ this.gotNotification = true;
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+ };
+ Services.obs.addObserver(vacuumObserver, "test-begin-vacuum", false);
+
+ // Check after a couple seconds that no VACUUM has been run.
+ do_timeout(2000, function () {
+ print("Check VACUUM did not run.");
+ do_check_false(vacuumObserver.gotNotification);
+ Services.obs.removeObserver(vacuumObserver, "test-begin-vacuum");
+ run_next_test();
+ });
+
+ synthesize_idle_daily();
+ },
+
+ /* Changing page size on WAL is not supported till Bug 634374 is properly fixed.
+ function test_page_size_change_with_wal()
+ {
+ print("\n*** Test that a VACUUM changes page_size with WAL mode");
+ Services.obs.notifyObservers(null, "test-options", "wal");
+
+ // Set a small page size.
+ let conn = getDatabase(new_db_file("testVacuum2"));
+ conn.executeSimpleSQL("PRAGMA page_size = 1024");
+ let stmt = conn.createStatement("PRAGMA page_size");
+ try {
+ while (stmt.executeStep()) {
+ do_check_eq(stmt.row.page_size, 1024);
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ // Use WAL journal mode.
+ conn.executeSimpleSQL("PRAGMA journal_mode = WAL");
+ stmt = conn.createStatement("PRAGMA journal_mode");
+ try {
+ while (stmt.executeStep()) {
+ do_check_eq(stmt.row.journal_mode, "wal");
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ // Wait for VACUUM end.
+ let vacuumObserver = {
+ observe: function VO_observe(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(this, aTopic);
+ print("Check page size has been updated.");
+ let stmt = conn.createStatement("PRAGMA page_size");
+ try {
+ while (stmt.executeStep()) {
+ do_check_eq(stmt.row.page_size, Ci.mozIStorageConnection.DEFAULT_PAGE_SIZE);
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ print("Check journal mode has been restored.");
+ stmt = conn.createStatement("PRAGMA journal_mode");
+ try {
+ while (stmt.executeStep()) {
+ do_check_eq(stmt.row.journal_mode, "wal");
+ }
+ }
+ finally {
+ stmt.finalize();
+ }
+
+ run_next_test();
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+ }
+ Services.obs.addObserver(vacuumObserver, "test-end-vacuum", false);
+
+ synthesize_idle_daily();
+ },
+ */
+
+ function test_memory_database_crash()
+ {
+ print("\n*** Test that we don't crash trying to vacuum a memory database");
+ Services.obs.notifyObservers(null, "test-options", "memory");
+
+ // Wait for VACUUM begin.
+ let vacuumObserver = {
+ gotNotification: false,
+ observe: function VO_observe(aSubject, aTopic, aData) {
+ this.gotNotification = true;
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+ };
+ Services.obs.addObserver(vacuumObserver, "test-begin-vacuum", false);
+
+ // Check after a couple seconds that no VACUUM has been run.
+ do_timeout(2000, function () {
+ print("Check VACUUM did not run.");
+ do_check_false(vacuumObserver.gotNotification);
+ Services.obs.removeObserver(vacuumObserver, "test-begin-vacuum");
+ run_next_test();
+ });
+
+ synthesize_idle_daily();
+ },
+
+ /* Changing page size on WAL is not supported till Bug 634374 is properly fixed.
+ function test_wal_restore_fail()
+ {
+ print("\n*** Test that a failing WAL restoration notifies failure");
+ Services.obs.notifyObservers(null, "test-options", "wal-fail");
+
+ // Wait for VACUUM end.
+ let vacuumObserver = {
+ observe: function VO_observe(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(vacuumObserver, "test-end-vacuum");
+ print("Check WAL restoration failed.");
+ do_check_false(aData);
+ run_next_test();
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+ }
+ Services.obs.addObserver(vacuumObserver, "test-end-vacuum", false);
+
+ synthesize_idle_daily();
+ },
+ */
+];
+
+function run_next_test()
+{
+ if (TESTS.length == 0) {
+ Services.obs.notifyObservers(null, "test-options", "dispose");
+ do_test_finished();
+ }
+ else {
+ // Set last VACUUM to a date in the past.
+ Services.prefs.setIntPref("storage.vacuum.last.testVacuum.sqlite",
+ parseInt(Date.now() / 1000 - 31 * 86400));
+ do_execute_soon(TESTS.shift());
+ }
+}
diff --git a/storage/test/unit/vacuumParticipant.js b/storage/test/unit/vacuumParticipant.js
new file mode 100644
index 000000000..01b980178
--- /dev/null
+++ b/storage/test/unit/vacuumParticipant.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This testing component is used in test_vacuum* tests.
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+/**
+ * Returns a new nsIFile reference for a profile database.
+ * @param filename for the database, excluded the .sqlite extension.
+ */
+function new_db_file(name)
+{
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append(name + ".sqlite");
+ return file;
+}
+
+/**
+ * Opens and returns a connection to the provided database file.
+ * @param nsIFile interface to the database file.
+ */
+function getDatabase(aFile)
+{
+ return Cc["@mozilla.org/storage/service;1"].getService(Ci.mozIStorageService)
+ .openDatabase(aFile);
+}
+
+function vacuumParticipant()
+{
+ this._dbConn = getDatabase(new_db_file("testVacuum"));
+ Services.obs.addObserver(this, "test-options", false);
+}
+
+vacuumParticipant.prototype =
+{
+ classDescription: "vacuumParticipant",
+ classID: Components.ID("{52aa0b22-b82f-4e38-992a-c3675a3355d2}"),
+ contractID: "@unit.test.com/test-vacuum-participant;1",
+
+ get expectedDatabasePageSize() {
+ return this._dbConn.defaultPageSize;
+ },
+ get databaseConnection() {
+ return this._dbConn;
+ },
+
+ _grant: true,
+ onBeginVacuum: function TVP_onBeginVacuum()
+ {
+ if (!this._grant) {
+ this._grant = true;
+ return false;
+ }
+ Services.obs.notifyObservers(null, "test-begin-vacuum", null);
+ return true;
+ },
+ onEndVacuum: function TVP_EndVacuum(aSucceeded)
+ {
+ if (this._stmt) {
+ this._stmt.finalize();
+ }
+ Services.obs.notifyObservers(null, "test-end-vacuum", aSucceeded);
+ },
+
+ observe: function TVP_observe(aSubject, aTopic, aData)
+ {
+ if (aData == "opt-out") {
+ this._grant = false;
+ }
+ else if (aData == "wal") {
+ try {
+ this._dbConn.close();
+ } catch (e) {
+ // Do nothing.
+ }
+ this._dbConn = getDatabase(new_db_file("testVacuum2"));
+ }
+ else if (aData == "wal-fail") {
+ try {
+ this._dbConn.close();
+ } catch (e) {
+ // Do nothing.
+ }
+ this._dbConn = getDatabase(new_db_file("testVacuum3"));
+ // Use WAL journal mode.
+ this._dbConn.executeSimpleSQL("PRAGMA journal_mode = WAL");
+ // Create a not finalized statement.
+ this._stmt = this._dbConn.createStatement("SELECT :test");
+ this._stmt.params.test = 1;
+ this._stmt.executeStep();
+ }
+ else if (aData == "memory") {
+ try {
+ this._dbConn.asyncClose();
+ } catch (e) {
+ // Do nothing.
+ }
+ this._dbConn = Cc["@mozilla.org/storage/service;1"].
+ getService(Ci.mozIStorageService).
+ openSpecialDatabase("memory");
+ }
+ else if (aData == "dispose") {
+ Services.obs.removeObserver(this, "test-options");
+ try {
+ this._dbConn.asyncClose();
+ } catch (e) {
+ // Do nothing.
+ }
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.mozIStorageVacuumParticipant,
+ Ci.nsIObserver,
+ ])
+};
+
+var gComponentsArray = [vacuumParticipant];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(gComponentsArray);
diff --git a/storage/test/unit/vacuumParticipant.manifest b/storage/test/unit/vacuumParticipant.manifest
new file mode 100644
index 000000000..cf359a80f
--- /dev/null
+++ b/storage/test/unit/vacuumParticipant.manifest
@@ -0,0 +1,3 @@
+component {52aa0b22-b82f-4e38-992a-c3675a3355d2} vacuumParticipant.js
+contract @unit.test.com/test-vacuum-participant;1 {52aa0b22-b82f-4e38-992a-c3675a3355d2}
+category vacuum-participant vacuumParticipant @unit.test.com/test-vacuum-participant;1
diff --git a/storage/test/unit/xpcshell.ini b/storage/test/unit/xpcshell.ini
new file mode 100644
index 000000000..e93c7d5b9
--- /dev/null
+++ b/storage/test/unit/xpcshell.ini
@@ -0,0 +1,46 @@
+[DEFAULT]
+head = head_storage.js
+tail =
+support-files =
+ corruptDB.sqlite
+ fakeDB.sqlite
+ locale_collation.txt
+ vacuumParticipant.js
+ vacuumParticipant.manifest
+
+[test_bug-365166.js]
+[test_bug-393952.js]
+[test_bug-429521.js]
+[test_bug-444233.js]
+[test_cache_size.js]
+[test_chunk_growth.js]
+# Bug 676981: test fails consistently on Android
+fail-if = os == "android"
+[test_connection_asyncClose.js]
+[test_connection_executeAsync.js]
+[test_connection_executeSimpleSQLAsync.js]
+[test_js_helpers.js]
+[test_levenshtein.js]
+[test_like.js]
+[test_like_escape.js]
+[test_locale_collation.js]
+[test_page_size_is_32k.js]
+[test_sqlite_secure_delete.js]
+[test_statement_executeAsync.js]
+[test_statement_wrapper_automatically.js]
+[test_storage_aggregates.js]
+[test_storage_connection.js]
+# Bug 676981: test fails consistently on Android
+fail-if = os == "android"
+[test_storage_fulltextindex.js]
+[test_storage_function.js]
+[test_storage_progresshandler.js]
+[test_storage_service.js]
+[test_storage_service_unshared.js]
+[test_storage_statement.js]
+[test_storage_value_array.js]
+[test_unicode.js]
+[test_vacuum.js]
+[test_telemetry_vfs.js]
+# Bug 676981: test fails consistently on Android
+# fail-if = os == "android"