diff options
Diffstat (limited to 'toolkit/modules/tests/xpcshell/test_sqlite.js')
-rw-r--r-- | toolkit/modules/tests/xpcshell/test_sqlite.js | 1094 |
1 files changed, 1094 insertions, 0 deletions
diff --git a/toolkit/modules/tests/xpcshell/test_sqlite.js b/toolkit/modules/tests/xpcshell/test_sqlite.js new file mode 100644 index 000000000..edd39d977 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_sqlite.js @@ -0,0 +1,1094 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +do_get_profile(); + +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/PromiseUtils.jsm"); +Cu.import("resource://gre/modules/osfile.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Sqlite.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +// To spin the event loop in test. +Cu.import("resource://services-common/async.js"); + +function sleep(ms) { + let deferred = Promise.defer(); + + let timer = Cc["@mozilla.org/timer;1"] + .createInstance(Ci.nsITimer); + + timer.initWithCallback({ + notify: function () { + deferred.resolve(); + }, + }, ms, timer.TYPE_ONE_SHOT); + + return deferred.promise; +} + +// When testing finalization, use this to tell Sqlite.jsm to not throw +// an uncatchable `Promise.reject` +function failTestsOnAutoClose(enabled) { + Cu.getGlobalForObject(Sqlite).Debugging.failTestsOnAutoClose = enabled; +} + +function getConnection(dbName, extraOptions={}) { + let path = dbName + ".sqlite"; + let options = {path: path}; + for (let [k, v] of Object.entries(extraOptions)) { + options[k] = v; + } + + return Sqlite.openConnection(options); +} + +function* getDummyDatabase(name, extraOptions={}) { + const TABLES = { + dirs: "id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT", + files: "id INTEGER PRIMARY KEY AUTOINCREMENT, dir_id INTEGER, path TEXT", + }; + + let c = yield getConnection(name, extraOptions); + c._initialStatementCount = 0; + + for (let [k, v] of Object.entries(TABLES)) { + yield c.execute("CREATE TABLE " + k + "(" + v + ")"); + c._initialStatementCount++; + } + + return c; +} + +function* getDummyTempDatabase(name, extraOptions={}) { + const TABLES = { + dirs: "id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT", + files: "id INTEGER PRIMARY KEY AUTOINCREMENT, dir_id INTEGER, path TEXT", + }; + + let c = yield getConnection(name, extraOptions); + c._initialStatementCount = 0; + + for (let [k, v] of Object.entries(TABLES)) { + yield c.execute("CREATE TEMP TABLE " + k + "(" + v + ")"); + c._initialStatementCount++; + } + + return c; +} + +function run_test() { + Cu.import("resource://testing-common/services/common/logging.js"); + initTestLogging("Trace"); + + run_next_test(); +} + +add_task(function* test_open_normal() { + let c = yield Sqlite.openConnection({path: "test_open_normal.sqlite"}); + yield c.close(); +}); + +add_task(function* test_open_unshared() { + let path = OS.Path.join(OS.Constants.Path.profileDir, "test_open_unshared.sqlite"); + + let c = yield Sqlite.openConnection({path: path, sharedMemoryCache: false}); + yield c.close(); +}); + +add_task(function* test_get_dummy_database() { + let db = yield getDummyDatabase("get_dummy_database"); + + do_check_eq(typeof(db), "object"); + yield db.close(); +}); + +add_task(function* test_schema_version() { + let db = yield getDummyDatabase("schema_version"); + + let version = yield db.getSchemaVersion(); + do_check_eq(version, 0); + + db.setSchemaVersion(14); + version = yield db.getSchemaVersion(); + do_check_eq(version, 14); + + for (let v of [0.5, "foobar", NaN]) { + let success; + try { + yield db.setSchemaVersion(v); + do_print("Schema version " + v + " should have been rejected"); + success = false; + } catch (ex) { + if (!ex.message.startsWith("Schema version must be an integer.")) + throw ex; + success = true; + } + do_check_true(success); + + version = yield db.getSchemaVersion(); + do_check_eq(version, 14); + } + + yield db.close(); +}); + +add_task(function* test_simple_insert() { + let c = yield getDummyDatabase("simple_insert"); + + let result = yield c.execute("INSERT INTO dirs VALUES (NULL, 'foo')"); + do_check_true(Array.isArray(result)); + do_check_eq(result.length, 0); + yield c.close(); +}); + +add_task(function* test_simple_bound_array() { + let c = yield getDummyDatabase("simple_bound_array"); + + let result = yield c.execute("INSERT INTO dirs VALUES (?, ?)", [1, "foo"]); + do_check_eq(result.length, 0); + yield c.close(); +}); + +add_task(function* test_simple_bound_object() { + let c = yield getDummyDatabase("simple_bound_object"); + let result = yield c.execute("INSERT INTO dirs VALUES (:id, :path)", + {id: 1, path: "foo"}); + do_check_eq(result.length, 0); + result = yield c.execute("SELECT id, path FROM dirs"); + do_check_eq(result.length, 1); + do_check_eq(result[0].getResultByName("id"), 1); + do_check_eq(result[0].getResultByName("path"), "foo"); + yield c.close(); +}); + +// This is mostly a sanity test to ensure simple executions work. +add_task(function* test_simple_insert_then_select() { + let c = yield getDummyDatabase("simple_insert_then_select"); + + yield c.execute("INSERT INTO dirs VALUES (NULL, 'foo')"); + yield c.execute("INSERT INTO dirs (path) VALUES (?)", ["bar"]); + + let result = yield c.execute("SELECT * FROM dirs"); + do_check_eq(result.length, 2); + + let i = 0; + for (let row of result) { + i++; + + do_check_eq(row.numEntries, 2); + do_check_eq(row.getResultByIndex(0), i); + + let expected = {1: "foo", 2: "bar"}[i]; + do_check_eq(row.getResultByName("path"), expected); + } + + yield c.close(); +}); + +add_task(function* test_repeat_execution() { + let c = yield getDummyDatabase("repeat_execution"); + + let sql = "INSERT INTO dirs (path) VALUES (:path)"; + yield c.executeCached(sql, {path: "foo"}); + yield c.executeCached(sql); + + let result = yield c.execute("SELECT * FROM dirs"); + + do_check_eq(result.length, 2); + + yield c.close(); +}); + +add_task(function* test_table_exists() { + let c = yield getDummyDatabase("table_exists"); + + do_check_false(yield c.tableExists("does_not_exist")); + do_check_true(yield c.tableExists("dirs")); + do_check_true(yield c.tableExists("files")); + + yield c.close(); +}); + +add_task(function* test_index_exists() { + let c = yield getDummyDatabase("index_exists"); + + do_check_false(yield c.indexExists("does_not_exist")); + + yield c.execute("CREATE INDEX my_index ON dirs (path)"); + do_check_true(yield c.indexExists("my_index")); + + yield c.close(); +}); + +add_task(function* test_temp_table_exists() { + let c = yield getDummyTempDatabase("temp_table_exists"); + + do_check_false(yield c.tableExists("temp_does_not_exist")); + do_check_true(yield c.tableExists("dirs")); + do_check_true(yield c.tableExists("files")); + + yield c.close(); +}); + +add_task(function* test_temp_index_exists() { + let c = yield getDummyTempDatabase("temp_index_exists"); + + do_check_false(yield c.indexExists("temp_does_not_exist")); + + yield c.execute("CREATE INDEX my_index ON dirs (path)"); + do_check_true(yield c.indexExists("my_index")); + + yield c.close(); +}); + +add_task(function* test_close_cached() { + let c = yield getDummyDatabase("close_cached"); + + yield c.executeCached("SELECT * FROM dirs"); + yield c.executeCached("SELECT * FROM files"); + + yield c.close(); +}); + +add_task(function* test_execute_invalid_statement() { + let c = yield getDummyDatabase("invalid_statement"); + + let deferred = Promise.defer(); + + do_check_eq(c._connectionData._anonymousStatements.size, 0); + + c.execute("SELECT invalid FROM unknown").then(do_throw, function onError(error) { + deferred.resolve(); + }); + + yield deferred.promise; + + // Ensure we don't leak the statement instance. + do_check_eq(c._connectionData._anonymousStatements.size, 0); + + yield c.close(); +}); + +add_task(function* test_incorrect_like_bindings() { + let c = yield getDummyDatabase("incorrect_like_bindings"); + + let sql = "select * from dirs where path LIKE 'non%'"; + Assert.throws(() => c.execute(sql), /Please enter a LIKE clause/); + Assert.throws(() => c.executeCached(sql), /Please enter a LIKE clause/); + + yield c.close(); +}); +add_task(function* test_on_row_exception_ignored() { + let c = yield getDummyDatabase("on_row_exception_ignored"); + + let sql = "INSERT INTO dirs (path) VALUES (?)"; + for (let i = 0; i < 10; i++) { + yield c.executeCached(sql, ["dir" + i]); + } + + let i = 0; + let hasResult = yield c.execute("SELECT * FROM DIRS", null, function onRow(row) { + i++; + + throw new Error("Some silly error."); + }); + + do_check_eq(hasResult, true); + do_check_eq(i, 10); + + yield c.close(); +}); + +// Ensure StopIteration during onRow causes processing to stop. +add_task(function* test_on_row_stop_iteration() { + let c = yield getDummyDatabase("on_row_stop_iteration"); + + let sql = "INSERT INTO dirs (path) VALUES (?)"; + for (let i = 0; i < 10; i++) { + yield c.executeCached(sql, ["dir" + i]); + } + + let i = 0; + let hasResult = yield c.execute("SELECT * FROM dirs", null, function onRow(row) { + i++; + + if (i == 5) { + throw StopIteration; + } + }); + + do_check_eq(hasResult, true); + do_check_eq(i, 5); + + yield c.close(); +}); + +// Ensure execute resolves to false when no rows are selected. +add_task(function* test_on_row_stop_iteration() { + let c = yield getDummyDatabase("no_on_row"); + + let i = 0; + let hasResult = yield c.execute(`SELECT * FROM dirs WHERE path="nonexistent"`, null, function onRow(row) { + i++; + }); + + do_check_eq(hasResult, false); + do_check_eq(i, 0); + + yield c.close(); +}); + +add_task(function* test_invalid_transaction_type() { + let c = yield getDummyDatabase("invalid_transaction_type"); + + Assert.throws(() => c.executeTransaction(function* () {}, "foobar"), + /Unknown transaction type/, + "Unknown transaction type should throw"); + + yield c.close(); +}); + +add_task(function* test_execute_transaction_success() { + let c = yield getDummyDatabase("execute_transaction_success"); + + do_check_false(c.transactionInProgress); + + yield c.executeTransaction(function* transaction(conn) { + do_check_eq(c, conn); + do_check_true(conn.transactionInProgress); + + yield conn.execute("INSERT INTO dirs (path) VALUES ('foo')"); + }); + + do_check_false(c.transactionInProgress); + let rows = yield c.execute("SELECT * FROM dirs"); + do_check_true(Array.isArray(rows)); + do_check_eq(rows.length, 1); + + yield c.close(); +}); + +add_task(function* test_execute_transaction_rollback() { + let c = yield getDummyDatabase("execute_transaction_rollback"); + + let deferred = Promise.defer(); + + c.executeTransaction(function* transaction(conn) { + yield conn.execute("INSERT INTO dirs (path) VALUES ('foo')"); + print("Expecting error with next statement."); + yield conn.execute("INSERT INTO invalid VALUES ('foo')"); + + // We should never get here. + do_throw(); + }).then(do_throw, function onError(error) { + deferred.resolve(); + }); + + yield deferred.promise; + + let rows = yield c.execute("SELECT * FROM dirs"); + do_check_eq(rows.length, 0); + + yield c.close(); +}); + +add_task(function* test_close_during_transaction() { + let c = yield getDummyDatabase("close_during_transaction"); + + yield c.execute("INSERT INTO dirs (path) VALUES ('foo')"); + + let promise = c.executeTransaction(function* transaction(conn) { + yield c.execute("INSERT INTO dirs (path) VALUES ('bar')"); + }); + yield c.close(); + + yield Assert.rejects(promise, + /Transaction canceled due to a closed connection/, + "closing a connection in the middle of a transaction should reject it"); + + let c2 = yield getConnection("close_during_transaction"); + let rows = yield c2.execute("SELECT * FROM dirs"); + do_check_eq(rows.length, 1); + + yield c2.close(); +}); + +// Verify that we support concurrent transactions. +add_task(function* test_multiple_transactions() { + let c = yield getDummyDatabase("detect_multiple_transactions"); + + for (let i = 0; i < 10; ++i) { + // We don't wait for these transactions. + c.executeTransaction(function* () { + yield c.execute("INSERT INTO dirs (path) VALUES (:path)", + { path: `foo${i}` }); + yield c.execute("SELECT * FROM dirs"); + }); + } + for (let i = 0; i < 10; ++i) { + yield c.executeTransaction(function* () { + yield c.execute("INSERT INTO dirs (path) VALUES (:path)", + { path: `bar${i}` }); + yield c.execute("SELECT * FROM dirs"); + }); + } + + let rows = yield c.execute("SELECT * FROM dirs"); + do_check_eq(rows.length, 20); + + yield c.close(); +}); + +// Verify that wrapped transactions ignore a BEGIN TRANSACTION failure, when +// an externally opened transaction exists. +add_task(function* test_wrapped_connection_transaction() { + let file = new FileUtils.File(OS.Path.join(OS.Constants.Path.profileDir, + "test_wrapStorageConnection.sqlite")); + let c = yield new Promise((resolve, reject) => { + Services.storage.openAsyncDatabase(file, null, (status, db) => { + if (Components.isSuccessCode(status)) { + resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection)); + } else { + reject(new Error(status)); + } + }); + }); + + let wrapper = yield Sqlite.wrapStorageConnection({ connection: c }); + // Start a transaction on the raw connection. + yield c.executeSimpleSQLAsync("BEGIN"); + // Now use executeTransaction, it will be executed, but not in a transaction. + yield wrapper.executeTransaction(function* () { + yield wrapper.execute("CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT)"); + }); + // This should not fail cause the internal transaction has not been created. + yield c.executeSimpleSQLAsync("COMMIT"); + + yield wrapper.execute("SELECT * FROM test"); + + // Closing the wrapper should just finalize statements but not close the + // database. + yield wrapper.close(); + yield c.asyncClose(); +}); + +add_task(function* test_shrink_memory() { + let c = yield getDummyDatabase("shrink_memory"); + + // It's just a simple sanity test. We have no way of measuring whether this + // actually does anything. + + yield c.shrinkMemory(); + yield c.close(); +}); + +add_task(function* test_no_shrink_on_init() { + let c = yield getConnection("no_shrink_on_init", + {shrinkMemoryOnConnectionIdleMS: 200}); + + let oldShrink = c._connectionData.shrinkMemory; + let count = 0; + Object.defineProperty(c._connectionData, "shrinkMemory", { + value: function () { + count++; + }, + }); + + // We should not shrink until a statement has been executed. + yield sleep(220); + do_check_eq(count, 0); + + yield c.execute("SELECT 1"); + yield sleep(220); + do_check_eq(count, 1); + + yield c.close(); +}); + +add_task(function* test_idle_shrink_fires() { + let c = yield getDummyDatabase("idle_shrink_fires", + {shrinkMemoryOnConnectionIdleMS: 200}); + c._connectionData._clearIdleShrinkTimer(); + + let oldShrink = c._connectionData.shrinkMemory; + let shrinkPromises = []; + + let count = 0; + Object.defineProperty(c._connectionData, "shrinkMemory", { + value: function () { + count++; + let promise = oldShrink.call(c._connectionData); + shrinkPromises.push(promise); + return promise; + }, + }); + + // We reset the idle shrink timer after monkeypatching because otherwise the + // installed timer callback will reference the non-monkeypatched function. + c._connectionData._startIdleShrinkTimer(); + + yield sleep(220); + do_check_eq(count, 1); + do_check_eq(shrinkPromises.length, 1); + yield shrinkPromises[0]; + shrinkPromises.shift(); + + // We shouldn't shrink again unless a statement was executed. + yield sleep(300); + do_check_eq(count, 1); + + yield c.execute("SELECT 1"); + yield sleep(300); + + do_check_eq(count, 2); + do_check_eq(shrinkPromises.length, 1); + yield shrinkPromises[0]; + + yield c.close(); +}); + +add_task(function* test_idle_shrink_reset_on_operation() { + const INTERVAL = 500; + let c = yield getDummyDatabase("idle_shrink_reset_on_operation", + {shrinkMemoryOnConnectionIdleMS: INTERVAL}); + + c._connectionData._clearIdleShrinkTimer(); + + let oldShrink = c._connectionData.shrinkMemory; + let shrinkPromises = []; + let count = 0; + + Object.defineProperty(c._connectionData, "shrinkMemory", { + value: function () { + count++; + let promise = oldShrink.call(c._connectionData); + shrinkPromises.push(promise); + return promise; + }, + }); + + let now = new Date(); + c._connectionData._startIdleShrinkTimer(); + + let initialIdle = new Date(now.getTime() + INTERVAL); + + // Perform database operations until initial scheduled time has been passed. + let i = 0; + while (new Date() < initialIdle) { + yield c.execute("INSERT INTO dirs (path) VALUES (?)", ["" + i]); + i++; + } + + do_check_true(i > 0); + + // We should not have performed an idle while doing operations. + do_check_eq(count, 0); + + // Wait for idle timer. + yield sleep(INTERVAL); + + // Ensure we fired. + do_check_eq(count, 1); + do_check_eq(shrinkPromises.length, 1); + yield shrinkPromises[0]; + + yield c.close(); +}); + +add_task(function* test_in_progress_counts() { + let c = yield getDummyDatabase("in_progress_counts"); + do_check_eq(c._connectionData._statementCounter, c._initialStatementCount); + do_check_eq(c._connectionData._pendingStatements.size, 0); + yield c.executeCached("INSERT INTO dirs (path) VALUES ('foo')"); + do_check_eq(c._connectionData._statementCounter, c._initialStatementCount + 1); + do_check_eq(c._connectionData._pendingStatements.size, 0); + + let expectOne; + let expectTwo; + + // Please forgive me. + let inner = Async.makeSpinningCallback(); + let outer = Async.makeSpinningCallback(); + + // We want to make sure that two queries executing simultaneously + // result in `_pendingStatements.size` reaching 2, then dropping back to 0. + // + // To do so, we kick off a second statement within the row handler + // of the first, then wait for both to finish. + + yield c.executeCached("SELECT * from dirs", null, function onRow() { + // In the onRow handler, we're still an outstanding query. + // Expect a single in-progress entry. + expectOne = c._connectionData._pendingStatements.size; + + // Start another query, checking that after its statement has been created + // there are two statements in progress. + let p = c.executeCached("SELECT 10, path from dirs"); + expectTwo = c._connectionData._pendingStatements.size; + + // Now wait for it to be done before we return from the row handler … + p.then(function onInner() { + inner(); + }); + }).then(function onOuter() { + // … and wait for the inner to be done before we finish … + inner.wait(); + outer(); + }); + + // … and wait for both queries to have finished before we go on and + // test postconditions. + outer.wait(); + + do_check_eq(expectOne, 1); + do_check_eq(expectTwo, 2); + do_check_eq(c._connectionData._statementCounter, c._initialStatementCount + 3); + do_check_eq(c._connectionData._pendingStatements.size, 0); + + yield c.close(); +}); + +add_task(function* test_discard_while_active() { + let c = yield getDummyDatabase("discard_while_active"); + + yield c.executeCached("INSERT INTO dirs (path) VALUES ('foo')"); + yield c.executeCached("INSERT INTO dirs (path) VALUES ('bar')"); + + let discarded = -1; + let first = true; + let sql = "SELECT * FROM dirs"; + yield c.executeCached(sql, null, function onRow(row) { + if (!first) { + return; + } + first = false; + discarded = c.discardCachedStatements(); + }); + + // We discarded everything, because the SELECT had already started to run. + do_check_eq(3, discarded); + + // And again is safe. + do_check_eq(0, c.discardCachedStatements()); + + yield c.close(); +}); + +add_task(function* test_discard_cached() { + let c = yield getDummyDatabase("discard_cached"); + + yield c.executeCached("SELECT * from dirs"); + do_check_eq(1, c._connectionData._cachedStatements.size); + + yield c.executeCached("SELECT * from files"); + do_check_eq(2, c._connectionData._cachedStatements.size); + + yield c.executeCached("SELECT * from dirs"); + do_check_eq(2, c._connectionData._cachedStatements.size); + + c.discardCachedStatements(); + do_check_eq(0, c._connectionData._cachedStatements.size); + + yield c.close(); +}); + +add_task(function* test_programmatic_binding() { + let c = yield getDummyDatabase("programmatic_binding"); + + let bindings = [ + {id: 1, path: "foobar"}, + {id: null, path: "baznoo"}, + {id: 5, path: "toofoo"}, + ]; + + let sql = "INSERT INTO dirs VALUES (:id, :path)"; + let result = yield c.execute(sql, bindings); + do_check_eq(result.length, 0); + + let rows = yield c.executeCached("SELECT * from dirs"); + do_check_eq(rows.length, 3); + yield c.close(); +}); + +add_task(function* test_programmatic_binding_transaction() { + let c = yield getDummyDatabase("programmatic_binding_transaction"); + + let bindings = [ + {id: 1, path: "foobar"}, + {id: null, path: "baznoo"}, + {id: 5, path: "toofoo"}, + ]; + + let sql = "INSERT INTO dirs VALUES (:id, :path)"; + yield c.executeTransaction(function* transaction() { + let result = yield c.execute(sql, bindings); + do_check_eq(result.length, 0); + + let rows = yield c.executeCached("SELECT * from dirs"); + do_check_eq(rows.length, 3); + }); + + // Transaction committed. + let rows = yield c.executeCached("SELECT * from dirs"); + do_check_eq(rows.length, 3); + yield c.close(); +}); + +add_task(function* test_programmatic_binding_transaction_partial_rollback() { + let c = yield getDummyDatabase("programmatic_binding_transaction_partial_rollback"); + + let bindings = [ + {id: 2, path: "foobar"}, + {id: 3, path: "toofoo"}, + ]; + + let sql = "INSERT INTO dirs VALUES (:id, :path)"; + + // Add some data in an implicit transaction before beginning the batch insert. + yield c.execute(sql, {id: 1, path: "works"}); + + let secondSucceeded = false; + try { + yield c.executeTransaction(function* transaction() { + // Insert one row. This won't implicitly start a transaction. + let result = yield c.execute(sql, bindings[0]); + + // Insert multiple rows. mozStorage will want to start a transaction. + // One of the inserts will fail, so the transaction should be rolled back. + result = yield c.execute(sql, bindings); + secondSucceeded = true; + }); + } catch (ex) { + print("Caught expected exception: " + ex); + } + + // We did not get to the end of our in-transaction block. + do_check_false(secondSucceeded); + + // Everything that happened in *our* transaction, not mozStorage's, got + // rolled back, but the first row still exists. + let rows = yield c.executeCached("SELECT * from dirs"); + do_check_eq(rows.length, 1); + do_check_eq(rows[0].getResultByName("path"), "works"); + yield c.close(); +}); + +// Just like the previous test, but relying on the implicit +// transaction established by mozStorage. +add_task(function* test_programmatic_binding_implicit_transaction() { + let c = yield getDummyDatabase("programmatic_binding_implicit_transaction"); + + let bindings = [ + {id: 2, path: "foobar"}, + {id: 1, path: "toofoo"}, + ]; + + let sql = "INSERT INTO dirs VALUES (:id, :path)"; + let secondSucceeded = false; + yield c.execute(sql, {id: 1, path: "works"}); + try { + let result = yield c.execute(sql, bindings); + secondSucceeded = true; + } catch (ex) { + print("Caught expected exception: " + ex); + } + + do_check_false(secondSucceeded); + + // The entire batch failed. + let rows = yield c.executeCached("SELECT * from dirs"); + do_check_eq(rows.length, 1); + do_check_eq(rows[0].getResultByName("path"), "works"); + yield c.close(); +}); + +// Test that direct binding of params and execution through mozStorage doesn't +// error when we manually create a transaction. See Bug 856925. +add_task(function* test_direct() { + let file = FileUtils.getFile("TmpD", ["test_direct.sqlite"]); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); + print("Opening " + file.path); + + let db = Services.storage.openDatabase(file); + print("Opened " + db); + + db.executeSimpleSQL("CREATE TABLE types (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, UNIQUE (name))"); + print("Executed setup."); + + let statement = db.createAsyncStatement("INSERT INTO types (name) VALUES (:name)"); + let params = statement.newBindingParamsArray(); + let one = params.newBindingParams(); + one.bindByName("name", null); + params.addParams(one); + let two = params.newBindingParams(); + two.bindByName("name", "bar"); + params.addParams(two); + + print("Beginning transaction."); + let begin = db.createAsyncStatement("BEGIN DEFERRED TRANSACTION"); + let end = db.createAsyncStatement("COMMIT TRANSACTION"); + + let deferred = Promise.defer(); + begin.executeAsync({ + handleCompletion: function (reason) { + deferred.resolve(); + } + }); + yield deferred.promise; + + statement.bindParameters(params); + + deferred = Promise.defer(); + print("Executing async."); + statement.executeAsync({ + handleResult: function (resultSet) { + }, + + handleError: function (error) { + print("Error when executing SQL (" + error.result + "): " + + error.message); + print("Original error: " + error.error); + errors.push(error); + deferred.reject(); + }, + + handleCompletion: function (reason) { + print("Completed."); + deferred.resolve(); + } + }); + + yield deferred.promise; + + deferred = Promise.defer(); + end.executeAsync({ + handleCompletion: function (reason) { + deferred.resolve(); + } + }); + yield deferred.promise; + + statement.finalize(); + begin.finalize(); + end.finalize(); + + deferred = Promise.defer(); + db.asyncClose(function () { + deferred.resolve() + }); + yield deferred.promise; +}); + +// Test Sqlite.cloneStorageConnection. +add_task(function* test_cloneStorageConnection() { + let file = new FileUtils.File(OS.Path.join(OS.Constants.Path.profileDir, + "test_cloneStorageConnection.sqlite")); + let c = yield new Promise((resolve, reject) => { + Services.storage.openAsyncDatabase(file, null, (status, db) => { + if (Components.isSuccessCode(status)) { + resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection)); + } else { + reject(new Error(status)); + } + }); + }); + + let clone = yield Sqlite.cloneStorageConnection({ connection: c, readOnly: true }); + // Just check that it works. + yield clone.execute("SELECT 1"); + + let clone2 = yield Sqlite.cloneStorageConnection({ connection: c, readOnly: false }); + // Just check that it works. + yield clone2.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)"); + + // Closing order should not matter. + yield c.asyncClose(); + yield clone2.close(); + yield clone.close(); +}); + +// Test Sqlite.cloneStorageConnection invalid argument. +add_task(function* test_cloneStorageConnection() { + try { + let clone = yield Sqlite.cloneStorageConnection({ connection: null }); + do_throw(new Error("Should throw on invalid connection")); + } catch (ex) { + if (ex.name != "TypeError") { + throw ex; + } + } +}); + +// Test clone() method. +add_task(function* test_clone() { + let c = yield getDummyDatabase("clone"); + + let clone = yield c.clone(); + // Just check that it works. + yield clone.execute("SELECT 1"); + // Closing order should not matter. + yield c.close(); + yield clone.close(); +}); + +// Test clone(readOnly) method. +add_task(function* test_readOnly_clone() { + let path = OS.Path.join(OS.Constants.Path.profileDir, "test_readOnly_clone.sqlite"); + let c = yield Sqlite.openConnection({path: path, sharedMemoryCache: false}); + + let clone = yield c.clone(true); + // Just check that it works. + yield clone.execute("SELECT 1"); + // But should not be able to write. + + yield Assert.rejects(clone.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)"), + /readonly/); + // Closing order should not matter. + yield c.close(); + yield clone.close(); +}); + +// Test Sqlite.wrapStorageConnection. +add_task(function* test_wrapStorageConnection() { + let file = new FileUtils.File(OS.Path.join(OS.Constants.Path.profileDir, + "test_wrapStorageConnection.sqlite")); + let c = yield new Promise((resolve, reject) => { + Services.storage.openAsyncDatabase(file, null, (status, db) => { + if (Components.isSuccessCode(status)) { + resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection)); + } else { + reject(new Error(status)); + } + }); + }); + + let wrapper = yield Sqlite.wrapStorageConnection({ connection: c }); + // Just check that it works. + yield wrapper.execute("SELECT 1"); + yield wrapper.executeCached("SELECT 1"); + + // Closing the wrapper should just finalize statements but not close the + // database. + yield wrapper.close(); + yield c.asyncClose(); +}); + +// Test finalization +add_task(function* test_closed_by_witness() { + failTestsOnAutoClose(false); + let c = yield getDummyDatabase("closed_by_witness"); + + Services.obs.notifyObservers(null, "sqlite-finalization-witness", + c._connectionData._identifier); + // Since we triggered finalization ourselves, tell the witness to + // forget the connection so it does not trigger a finalization again + c._witness.forget(); + yield c._connectionData._deferredClose.promise; + do_check_false(c._connectionData._open); + failTestsOnAutoClose(true); +}); + +add_task(function* test_warning_message_on_finalization() { + failTestsOnAutoClose(false); + let c = yield getDummyDatabase("warning_message_on_finalization"); + let identifier = c._connectionData._identifier; + let deferred = Promise.defer(); + + let listener = { + observe: function(msg) { + let messageText = msg.message; + // Make sure the message starts with a warning containing the + // connection identifier + if (messageText.indexOf("Warning: Sqlite connection '" + identifier + "'") !== -1) { + deferred.resolve(); + } + } + }; + Services.console.registerListener(listener); + + Services.obs.notifyObservers(null, "sqlite-finalization-witness", identifier); + // Since we triggered finalization ourselves, tell the witness to + // forget the connection so it does not trigger a finalization again + c._witness.forget(); + + yield deferred.promise; + Services.console.unregisterListener(listener); + failTestsOnAutoClose(true); +}); + +add_task(function* test_error_message_on_unknown_finalization() { + failTestsOnAutoClose(false); + let deferred = Promise.defer(); + + let listener = { + observe: function(msg) { + let messageText = msg.message; + if (messageText.indexOf("Error: Attempt to finalize unknown " + + "Sqlite connection: foo") !== -1) { + deferred.resolve(); + } + } + }; + Services.console.registerListener(listener); + Services.obs.notifyObservers(null, "sqlite-finalization-witness", "foo"); + + yield deferred.promise; + Services.console.unregisterListener(listener); + failTestsOnAutoClose(true); +}); + +add_task(function* test_forget_witness_on_close() { + let c = yield getDummyDatabase("forget_witness_on_close"); + + let forgetCalled = false; + let oldWitness = c._witness; + c._witness = { + forget: function () { + forgetCalled = true; + oldWitness.forget(); + }, + }; + + yield c.close(); + // After close, witness should have forgotten the connection + do_check_true(forgetCalled); +}); + +add_task(function* test_close_database_on_gc() { + failTestsOnAutoClose(false); + let finalPromise; + + { + let collectedPromises = []; + for (let i = 0; i < 100; ++i) { + let deferred = PromiseUtils.defer(); + let c = yield getDummyDatabase("gc_" + i); + c._connectionData._deferredClose.promise.then(deferred.resolve); + collectedPromises.push(deferred.promise); + } + finalPromise = Promise.all(collectedPromises); + } + + // Call getDummyDatabase once more to clear any remaining + // references. This is needed at the moment, otherwise + // garbage-collection takes place after the shutdown barrier and the + // test will timeout. Once that is fixed, we can remove this line + // and be fine as long as the connections are garbage-collected. + let last = yield getDummyDatabase("gc_last"); + yield last.close(); + + Components.utils.forceGC(); + Components.utils.forceCC(); + Components.utils.forceShrinkingGC(); + + yield finalPromise; + failTestsOnAutoClose(true); +}); |