summaryrefslogtreecommitdiffstats
path: root/storage/test/unit/test_statement_executeAsync.js
diff options
context:
space:
mode:
Diffstat (limited to 'storage/test/unit/test_statement_executeAsync.js')
-rw-r--r--storage/test/unit/test_statement_executeAsync.js1045
1 files changed, 1045 insertions, 0 deletions
diff --git a/storage/test/unit/test_statement_executeAsync.js b/storage/test/unit/test_statement_executeAsync.js
new file mode 100644
index 0000000000..dab15121a5
--- /dev/null
+++ b/storage/test/unit/test_statement_executeAsync.js
@@ -0,0 +1,1045 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * This file tests the functionality of mozIStorageBaseStatement::executeAsync
+ * for both mozIStorageStatement and mozIStorageAsyncStatement.
+ */
+
+// This file uses the internal _quit from testing/xpcshell/head.js */
+/* global _quit */
+
+const INTEGER = 1;
+const TEXT = "this is test text";
+const REAL = 3.23;
+const BLOB = [1, 2];
+
+/**
+ * Execute the given statement asynchronously, spinning an event loop until the
+ * async statement completes.
+ *
+ * @param aStmt
+ * The statement to execute.
+ * @param [aOptions={}]
+ * @param [aOptions.error=false]
+ * If true we should expect an error whose code we do not care about. If
+ * a numeric value, that's the error code we expect and require. If we
+ * are expecting an error, we expect a completion reason of REASON_ERROR.
+ * Otherwise we expect no error notification and a completion reason of
+ * REASON_FINISHED.
+ * @param [aOptions.cancel]
+ * If true we cancel the pending statement and additionally return the
+ * pending statement in case you want to further manipulate it.
+ * @param [aOptions.returnPending=false]
+ * If true we keep the pending statement around and return it to you. We
+ * normally avoid doing this to try and minimize the amount of time a
+ * reference is held to the returned pending statement.
+ * @param [aResults]
+ * If omitted, we assume no results rows are expected. If it is a
+ * number, we assume it is the number of results rows expected. If it is
+ * a function, we assume it is a function that takes the 1) result row
+ * number, 2) result tuple, 3) call stack for the original call to
+ * execAsync as arguments. If it is a list, we currently assume it is a
+ * list of functions where each function is intended to evaluate the
+ * result row at that ordinal position and takes the result tuple and
+ * the call stack for the original call.
+ */
+function execAsync(aStmt, aOptions, aResults) {
+ let caller = Components.stack.caller;
+ if (aOptions == null) {
+ aOptions = {};
+ }
+
+ let resultsExpected;
+ let resultsChecker;
+ if (aResults == null) {
+ resultsExpected = 0;
+ } else if (typeof aResults == "number") {
+ resultsExpected = aResults;
+ } else if (typeof aResults == "function") {
+ resultsChecker = aResults;
+ } else {
+ // array
+ resultsExpected = aResults.length;
+ resultsChecker = function (aResultNum, aTup, aCaller) {
+ aResults[aResultNum](aTup, aCaller);
+ };
+ }
+ let resultsSeen = 0;
+
+ let errorCodeExpected = false;
+ let reasonExpected = Ci.mozIStorageStatementCallback.REASON_FINISHED;
+ let altReasonExpected = null;
+ if ("error" in aOptions) {
+ errorCodeExpected = aOptions.error;
+ if (errorCodeExpected) {
+ reasonExpected = Ci.mozIStorageStatementCallback.REASON_ERROR;
+ }
+ }
+ let errorCodeSeen = false;
+
+ if ("cancel" in aOptions && aOptions.cancel) {
+ altReasonExpected = Ci.mozIStorageStatementCallback.REASON_CANCELED;
+ }
+
+ let completed = false;
+
+ let listener = {
+ handleResult(aResultSet) {
+ let row,
+ resultsSeenThisCall = 0;
+ while ((row = aResultSet.getNextRow()) != null) {
+ if (resultsChecker) {
+ resultsChecker(resultsSeen, row, caller);
+ }
+ resultsSeen++;
+ resultsSeenThisCall++;
+ }
+
+ if (!resultsSeenThisCall) {
+ do_throw("handleResult invoked with 0 result rows!");
+ }
+ },
+ handleError(aError) {
+ if (errorCodeSeen) {
+ do_throw("handleError called when we already had an error!");
+ }
+ errorCodeSeen = aError.result;
+ },
+ handleCompletion(aReason) {
+ if (completed) {
+ // paranoia check
+ do_throw("Received a second handleCompletion notification!", caller);
+ }
+
+ if (resultsSeen != resultsExpected) {
+ do_throw(
+ "Expected " +
+ resultsExpected +
+ " rows of results but " +
+ "got " +
+ resultsSeen +
+ " rows!",
+ caller
+ );
+ }
+
+ if (errorCodeExpected && !errorCodeSeen) {
+ do_throw("Expected an error, but did not see one.", caller);
+ } else if (errorCodeExpected != errorCodeSeen) {
+ do_throw(
+ "Expected error code " +
+ errorCodeExpected +
+ " but got " +
+ errorCodeSeen,
+ caller
+ );
+ }
+
+ if (aReason != reasonExpected && aReason != altReasonExpected) {
+ do_throw(
+ "Expected reason " +
+ reasonExpected +
+ (altReasonExpected ? " or " + altReasonExpected : "") +
+ " but got " +
+ aReason,
+ caller
+ );
+ }
+
+ completed = true;
+ },
+ };
+
+ let pending;
+ // Only get a pending reference if we're supposed to do.
+ // (note: This does not stop XPConnect from holding onto one currently.)
+ if (
+ ("cancel" in aOptions && aOptions.cancel) ||
+ ("returnPending" in aOptions && aOptions.returnPending)
+ ) {
+ pending = aStmt.executeAsync(listener);
+ } else {
+ aStmt.executeAsync(listener);
+ }
+
+ if ("cancel" in aOptions && aOptions.cancel) {
+ pending.cancel();
+ }
+
+ Services.tm.spinEventLoopUntil(
+ "Test(test_statement_executeAsync.js:execAsync)",
+ () => completed || _quit
+ );
+
+ return pending;
+}
+
+/**
+ * Make sure that illegal SQL generates the expected runtime error and does not
+ * result in any crashes. Async-only since the synchronous case generates the
+ * error synchronously (and is tested elsewhere).
+ */
+function test_illegal_sql_async_deferred() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);
+
+ // gibberish
+ let stmt = makeTestStatement("I AM A ROBOT. DO AS I SAY.");
+ execAsync(stmt, { error: Ci.mozIStorageError.ERROR });
+ stmt.finalize();
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ TEST_DB_NAME,
+ TELEMETRY_VALUES.failure,
+ 1
+ );
+ histogram.clear();
+
+ // legal SQL syntax, but with semantics issues.
+ stmt = makeTestStatement("SELECT destination FROM funkytown");
+ execAsync(stmt, { error: Ci.mozIStorageError.ERROR });
+ stmt.finalize();
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ TEST_DB_NAME,
+ TELEMETRY_VALUES.failure,
+ 1
+ );
+ histogram.clear();
+
+ run_next_test();
+}
+test_illegal_sql_async_deferred.asyncOnly = true;
+
+function test_create_table() {
+ // Ensure our table doesn't exist
+ Assert.ok(!getOpenedDatabase().tableExists("test"));
+
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);
+
+ var stmt = makeTestStatement(
+ "CREATE TABLE test (" +
+ "id INTEGER, " +
+ "string TEXT, " +
+ "number REAL, " +
+ "nuller NULL, " +
+ "blober BLOB" +
+ ")"
+ );
+ execAsync(stmt);
+ stmt.finalize();
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ TEST_DB_NAME,
+ TELEMETRY_VALUES.success,
+ 1
+ );
+ histogram.clear();
+
+ // Check that the table has been created
+ Assert.ok(getOpenedDatabase().tableExists("test"));
+
+ histogram.clear();
+
+ // Verify that it's created correctly (this will throw if it wasn't)
+ let checkStmt = getOpenedDatabase().createStatement(
+ "SELECT id, string, number, nuller, blober FROM test"
+ );
+ checkStmt.finalize();
+
+ // Nothing has executed so the histogram should be empty.
+ Assert.ok(!histogram.snapshot().values);
+
+ run_next_test();
+}
+
+function test_add_data() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);
+
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (?, ?, ?, ?, ?)"
+ );
+ stmt.bindBlobByIndex(4, BLOB, BLOB.length);
+ stmt.bindByIndex(3, null);
+ stmt.bindByIndex(2, REAL);
+ stmt.bindByIndex(1, TEXT);
+ stmt.bindByIndex(0, INTEGER);
+
+ execAsync(stmt);
+ stmt.finalize();
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ TEST_DB_NAME,
+ TELEMETRY_VALUES.success,
+ 1
+ );
+ histogram.clear();
+
+ // Check that the result is in the table
+ verifyQuery(
+ "SELECT string, number, nuller, blober FROM test WHERE id = ?",
+ INTEGER,
+ [TEXT, REAL, null, BLOB]
+ );
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ TEST_DB_NAME,
+ TELEMETRY_VALUES.success,
+ 1
+ );
+ histogram.clear();
+
+ run_next_test();
+}
+
+function test_get_data() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);
+
+ var stmt = makeTestStatement(
+ "SELECT string, number, nuller, blober, id FROM test WHERE id = ?"
+ );
+ stmt.bindByIndex(0, INTEGER);
+ execAsync(stmt, {}, [
+ function (tuple) {
+ Assert.notEqual(null, tuple);
+
+ // Check that it's what we expect
+ Assert.ok(!tuple.getIsNull(0));
+ Assert.equal(tuple.getResultByName("string"), tuple.getResultByIndex(0));
+ Assert.equal(TEXT, tuple.getResultByName("string"));
+ Assert.equal(
+ Ci.mozIStorageValueArray.VALUE_TYPE_TEXT,
+ tuple.getTypeOfIndex(0)
+ );
+
+ Assert.ok(!tuple.getIsNull(1));
+ Assert.equal(tuple.getResultByName("number"), tuple.getResultByIndex(1));
+ Assert.equal(REAL, tuple.getResultByName("number"));
+ Assert.equal(
+ Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT,
+ tuple.getTypeOfIndex(1)
+ );
+
+ Assert.ok(tuple.getIsNull(2));
+ Assert.equal(tuple.getResultByName("nuller"), tuple.getResultByIndex(2));
+ Assert.equal(null, tuple.getResultByName("nuller"));
+ Assert.equal(
+ Ci.mozIStorageValueArray.VALUE_TYPE_NULL,
+ tuple.getTypeOfIndex(2)
+ );
+
+ Assert.ok(!tuple.getIsNull(3));
+ var blobByName = tuple.getResultByName("blober");
+ Assert.equal(BLOB.length, blobByName.length);
+ var blobByIndex = tuple.getResultByIndex(3);
+ Assert.equal(BLOB.length, blobByIndex.length);
+ for (let i = 0; i < BLOB.length; i++) {
+ Assert.equal(BLOB[i], blobByName[i]);
+ Assert.equal(BLOB[i], blobByIndex[i]);
+ }
+ var count = { value: 0 };
+ var blob = { value: null };
+ tuple.getBlob(3, count, blob);
+ Assert.equal(BLOB.length, count.value);
+ for (let i = 0; i < BLOB.length; i++) {
+ Assert.equal(BLOB[i], blob.value[i]);
+ }
+ Assert.equal(
+ Ci.mozIStorageValueArray.VALUE_TYPE_BLOB,
+ tuple.getTypeOfIndex(3)
+ );
+
+ Assert.ok(!tuple.getIsNull(4));
+ Assert.equal(tuple.getResultByName("id"), tuple.getResultByIndex(4));
+ Assert.equal(INTEGER, tuple.getResultByName("id"));
+ Assert.equal(
+ Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER,
+ tuple.getTypeOfIndex(4)
+ );
+ },
+ ]);
+ stmt.finalize();
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ TEST_DB_NAME,
+ TELEMETRY_VALUES.success,
+ 1
+ );
+ histogram.clear();
+
+ run_next_test();
+}
+
+function test_tuple_out_of_bounds() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);
+
+ var stmt = makeTestStatement("SELECT string FROM test");
+ execAsync(stmt, {}, [
+ function (tuple) {
+ Assert.notEqual(null, tuple);
+
+ // Check all out of bounds - should throw
+ var methods = [
+ "getTypeOfIndex",
+ "getInt32",
+ "getInt64",
+ "getDouble",
+ "getUTF8String",
+ "getString",
+ "getIsNull",
+ ];
+ for (var i in methods) {
+ try {
+ tuple[methods[i]](tuple.numEntries);
+ do_throw("did not throw :(");
+ } catch (e) {
+ Assert.equal(Cr.NS_ERROR_ILLEGAL_VALUE, e.result);
+ }
+ }
+
+ // getBlob requires more args...
+ try {
+ var blob = { value: null };
+ var size = { value: 0 };
+ tuple.getBlob(tuple.numEntries, blob, size);
+ do_throw("did not throw :(");
+ } catch (e) {
+ Assert.equal(Cr.NS_ERROR_ILLEGAL_VALUE, e.result);
+ }
+ },
+ ]);
+ stmt.finalize();
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ TEST_DB_NAME,
+ TELEMETRY_VALUES.success,
+ 1
+ );
+ histogram.clear();
+
+ run_next_test();
+}
+
+function test_no_listener_works_on_success() {
+ var stmt = makeTestStatement("DELETE FROM test WHERE id = ?");
+ stmt.bindByIndex(0, 0);
+ stmt.executeAsync();
+ stmt.finalize();
+
+ // Run the next test.
+ run_next_test();
+}
+
+function test_no_listener_works_on_results() {
+ var stmt = makeTestStatement("SELECT ?");
+ stmt.bindByIndex(0, 1);
+ stmt.executeAsync();
+ stmt.finalize();
+
+ // Run the next test.
+ run_next_test();
+}
+
+function test_no_listener_works_on_error() {
+ // commit without a transaction will trigger an error
+ var stmt = makeTestStatement("COMMIT");
+ stmt.executeAsync();
+ stmt.finalize();
+
+ // Run the next test.
+ run_next_test();
+}
+
+function test_partial_listener_works() {
+ var stmt = makeTestStatement("DELETE FROM test WHERE id = ?");
+ stmt.bindByIndex(0, 0);
+ stmt.executeAsync({
+ handleResult(aResultSet) {},
+ });
+ stmt.executeAsync({
+ handleError(aError) {},
+ });
+ stmt.executeAsync({
+ handleCompletion(aReason) {},
+ });
+ stmt.finalize();
+
+ // Run the next test.
+ run_next_test();
+}
+
+/**
+ * Dubious cancellation test that depends on system loading may or may not
+ * succeed in canceling things. It does at least test if calling cancel blows
+ * up. test_AsyncCancellation in test_true_async.cpp is our test that canceling
+ * actually works correctly.
+ */
+function test_immediate_cancellation() {
+ var stmt = makeTestStatement("DELETE FROM test WHERE id = ?");
+ stmt.bindByIndex(0, 0);
+ execAsync(stmt, { cancel: true });
+ stmt.finalize();
+ run_next_test();
+}
+
+/**
+ * Test that calling cancel twice throws the second time.
+ */
+function test_double_cancellation() {
+ var stmt = makeTestStatement("DELETE FROM test WHERE id = ?");
+ stmt.bindByIndex(0, 0);
+ let pendingStatement = execAsync(stmt, { cancel: true });
+ // And cancel again - expect an exception
+ expectError(Cr.NS_ERROR_UNEXPECTED, () => pendingStatement.cancel());
+
+ stmt.finalize();
+ run_next_test();
+}
+
+/**
+ * Verify that nothing untoward happens if we try and cancel something after it
+ * has fully run to completion.
+ */
+function test_cancellation_after_execution() {
+ var stmt = makeTestStatement("DELETE FROM test WHERE id = ?");
+ stmt.bindByIndex(0, 0);
+ let pendingStatement = execAsync(stmt, { returnPending: true });
+ // (the statement has fully executed at this point)
+ // canceling after the statement has run to completion should not throw!
+ pendingStatement.cancel();
+
+ stmt.finalize();
+ run_next_test();
+}
+
+/**
+ * Verifies that a single statement can be executed more than once. Might once
+ * have been intended to also ensure that callback notifications were not
+ * incorrectly interleaved, but that part was brittle (it's totally fine for
+ * handleResult to get called multiple times) and not comprehensive.
+ */
+function test_double_execute() {
+ let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);
+
+ var stmt = makeTestStatement("SELECT 1");
+ execAsync(stmt, null, 1);
+ execAsync(stmt, null, 1);
+ stmt.finalize();
+
+ TelemetryTestUtils.assertKeyedHistogramValue(
+ histogram,
+ TEST_DB_NAME,
+ TELEMETRY_VALUES.success,
+ 2
+ );
+ histogram.clear();
+
+ run_next_test();
+}
+
+function test_finalized_statement_does_not_crash() {
+ var stmt = makeTestStatement("SELECT * FROM TEST");
+ stmt.finalize();
+ // we are concerned about a crash here; an error is fine.
+ try {
+ stmt.executeAsync();
+ } catch (ex) {
+ // Do nothing.
+ }
+
+ // Run the next test.
+ run_next_test();
+}
+
+/**
+ * Bind by mozIStorageBindingParams on the mozIStorageBaseStatement by index.
+ */
+function test_bind_direct_binding_params_by_index() {
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (?, ?, ?, ?, ?)"
+ );
+ let insertId = nextUniqueId++;
+ stmt.bindByIndex(0, insertId);
+ stmt.bindByIndex(1, TEXT);
+ stmt.bindByIndex(2, REAL);
+ stmt.bindByIndex(3, null);
+ stmt.bindBlobByIndex(4, BLOB, BLOB.length);
+ execAsync(stmt);
+ stmt.finalize();
+ verifyQuery(
+ "SELECT string, number, nuller, blober FROM test WHERE id = ?",
+ insertId,
+ [TEXT, REAL, null, BLOB]
+ );
+ run_next_test();
+}
+
+/**
+ * Bind by mozIStorageBindingParams on the mozIStorageBaseStatement by name.
+ */
+function test_bind_direct_binding_params_by_name() {
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (:int, :text, :real, :null, :blob)"
+ );
+ let insertId = nextUniqueId++;
+ stmt.bindByName("int", insertId);
+ stmt.bindByName("text", TEXT);
+ stmt.bindByName("real", REAL);
+ stmt.bindByName("null", null);
+ stmt.bindBlobByName("blob", BLOB);
+ execAsync(stmt);
+ stmt.finalize();
+ verifyQuery(
+ "SELECT string, number, nuller, blober FROM test WHERE id = ?",
+ insertId,
+ [TEXT, REAL, null, BLOB]
+ );
+ run_next_test();
+}
+
+function test_bind_js_params_helper_by_index() {
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (?, ?, ?, ?, NULL)"
+ );
+ let insertId = nextUniqueId++;
+ // we cannot bind blobs this way; no blober
+ stmt.params[3] = null;
+ stmt.params[2] = REAL;
+ stmt.params[1] = TEXT;
+ stmt.params[0] = insertId;
+ execAsync(stmt);
+ stmt.finalize();
+ verifyQuery(
+ "SELECT string, number, nuller FROM test WHERE id = ?",
+ insertId,
+ [TEXT, REAL, null]
+ );
+ run_next_test();
+}
+
+function test_bind_js_params_helper_by_name() {
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (:int, :text, :real, :null, NULL)"
+ );
+ let insertId = nextUniqueId++;
+ // we cannot bind blobs this way; no blober
+ stmt.params.null = null;
+ stmt.params.real = REAL;
+ stmt.params.text = TEXT;
+ stmt.params.int = insertId;
+ execAsync(stmt);
+ stmt.finalize();
+ verifyQuery(
+ "SELECT string, number, nuller FROM test WHERE id = ?",
+ insertId,
+ [TEXT, REAL, null]
+ );
+ run_next_test();
+}
+
+function test_bind_multiple_rows_by_index() {
+ const AMOUNT_TO_ADD = 5;
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (?, ?, ?, ?, ?)"
+ );
+ var array = stmt.newBindingParamsArray();
+ for (let i = 0; i < AMOUNT_TO_ADD; i++) {
+ let bp = array.newBindingParams();
+ bp.bindByIndex(0, INTEGER);
+ bp.bindByIndex(1, TEXT);
+ bp.bindByIndex(2, REAL);
+ bp.bindByIndex(3, null);
+ bp.bindBlobByIndex(4, BLOB, BLOB.length);
+ array.addParams(bp);
+ Assert.equal(array.length, i + 1);
+ }
+ stmt.bindParameters(array);
+
+ let rowCount = getTableRowCount("test");
+ execAsync(stmt);
+ Assert.equal(rowCount + AMOUNT_TO_ADD, getTableRowCount("test"));
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_bind_multiple_rows_by_name() {
+ const AMOUNT_TO_ADD = 5;
+ var stmt = makeTestStatement(
+ "INSERT INTO test (id, string, number, nuller, blober) " +
+ "VALUES (:int, :text, :real, :null, :blob)"
+ );
+ var array = stmt.newBindingParamsArray();
+ for (let i = 0; i < AMOUNT_TO_ADD; i++) {
+ let bp = array.newBindingParams();
+ bp.bindByName("int", INTEGER);
+ bp.bindByName("text", TEXT);
+ bp.bindByName("real", REAL);
+ bp.bindByName("null", null);
+ bp.bindBlobByName("blob", BLOB);
+ array.addParams(bp);
+ Assert.equal(array.length, i + 1);
+ }
+ stmt.bindParameters(array);
+
+ let rowCount = getTableRowCount("test");
+ execAsync(stmt);
+ Assert.equal(rowCount + AMOUNT_TO_ADD, getTableRowCount("test"));
+ stmt.finalize();
+ run_next_test();
+}
+
+/**
+ * Verify that a mozIStorageStatement instance throws immediately when we
+ * try and bind to an illegal index.
+ */
+function test_bind_out_of_bounds_sync_immediate() {
+ let stmt = makeTestStatement("INSERT INTO test (id) VALUES (?)");
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+
+ // Check variant binding.
+ expectError(Cr.NS_ERROR_INVALID_ARG, () => bp.bindByIndex(1, INTEGER));
+ // Check blob binding.
+ expectError(Cr.NS_ERROR_INVALID_ARG, () =>
+ bp.bindBlobByIndex(1, BLOB, BLOB.length)
+ );
+
+ stmt.finalize();
+ run_next_test();
+}
+test_bind_out_of_bounds_sync_immediate.syncOnly = true;
+
+/**
+ * Verify that a mozIStorageAsyncStatement reports an error asynchronously when
+ * we bind to an illegal index.
+ */
+function test_bind_out_of_bounds_async_deferred() {
+ let stmt = makeTestStatement("INSERT INTO test (id) VALUES (?)");
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+
+ // There is no difference between variant and blob binding for async purposes.
+ bp.bindByIndex(1, INTEGER);
+ array.addParams(bp);
+ stmt.bindParameters(array);
+ execAsync(stmt, { error: Ci.mozIStorageError.RANGE });
+
+ stmt.finalize();
+ run_next_test();
+}
+test_bind_out_of_bounds_async_deferred.asyncOnly = true;
+
+function test_bind_no_such_name_sync_immediate() {
+ let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:foo)");
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+
+ // Check variant binding.
+ expectError(Cr.NS_ERROR_INVALID_ARG, () =>
+ bp.bindByName("doesnotexist", INTEGER)
+ );
+ // Check blob binding.
+ expectError(Cr.NS_ERROR_INVALID_ARG, () =>
+ bp.bindBlobByName("doesnotexist", BLOB)
+ );
+
+ stmt.finalize();
+ run_next_test();
+}
+test_bind_no_such_name_sync_immediate.syncOnly = true;
+
+function test_bind_no_such_name_async_deferred() {
+ let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:foo)");
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+
+ bp.bindByName("doesnotexist", INTEGER);
+ array.addParams(bp);
+ stmt.bindParameters(array);
+ execAsync(stmt, { error: Ci.mozIStorageError.RANGE });
+
+ stmt.finalize();
+ run_next_test();
+}
+test_bind_no_such_name_async_deferred.asyncOnly = true;
+
+function test_bind_bogus_type_by_index() {
+ // We try to bind a JS Object here that should fail to bind.
+ let stmt = makeTestStatement("INSERT INTO test (blober) VALUES (?)");
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+ Assert.throws(() => bp.bindByIndex(0, run_test), /NS_ERROR_UNEXPECTED/);
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_bind_bogus_type_by_name() {
+ // We try to bind a JS Object here that should fail to bind.
+ let stmt = makeTestStatement("INSERT INTO test (blober) VALUES (:blob)");
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+ Assert.throws(() => bp.bindByName("blob", run_test), /NS_ERROR_UNEXPECTED/);
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_bind_params_already_locked() {
+ let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)");
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+ bp.bindByName("int", INTEGER);
+ array.addParams(bp);
+
+ // We should get an error after we call addParams and try to bind again.
+ expectError(Cr.NS_ERROR_UNEXPECTED, () => bp.bindByName("int", INTEGER));
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_bind_params_array_already_locked() {
+ let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)");
+
+ let array = stmt.newBindingParamsArray();
+ let bp1 = array.newBindingParams();
+ bp1.bindByName("int", INTEGER);
+ array.addParams(bp1);
+ let bp2 = array.newBindingParams();
+ stmt.bindParameters(array);
+ bp2.bindByName("int", INTEGER);
+
+ // We should get an error after we have bound the array to the statement.
+ expectError(Cr.NS_ERROR_UNEXPECTED, () => array.addParams(bp2));
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_no_binding_params_from_locked_array() {
+ let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)");
+
+ let array = stmt.newBindingParamsArray();
+ let bp = array.newBindingParams();
+ bp.bindByName("int", INTEGER);
+ array.addParams(bp);
+ stmt.bindParameters(array);
+
+ // We should not be able to get a new BindingParams object after we have bound
+ // to the statement.
+ expectError(Cr.NS_ERROR_UNEXPECTED, () => array.newBindingParams());
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_not_right_owning_array() {
+ let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)");
+
+ let array1 = stmt.newBindingParamsArray();
+ let array2 = stmt.newBindingParamsArray();
+ let bp = array1.newBindingParams();
+ bp.bindByName("int", INTEGER);
+
+ // We should not be able to add bp to array2 since it was created from array1.
+ expectError(Cr.NS_ERROR_UNEXPECTED, () => array2.addParams(bp));
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_not_right_owning_statement() {
+ let stmt1 = makeTestStatement("INSERT INTO test (id) VALUES (:int)");
+ let stmt2 = makeTestStatement("INSERT INTO test (id) VALUES (:int)");
+
+ let array1 = stmt1.newBindingParamsArray();
+ stmt2.newBindingParamsArray();
+ let bp = array1.newBindingParams();
+ bp.bindByName("int", INTEGER);
+ array1.addParams(bp);
+
+ // We should not be able to bind array1 since it was created from stmt1.
+ expectError(Cr.NS_ERROR_UNEXPECTED, () => stmt2.bindParameters(array1));
+
+ stmt1.finalize();
+ stmt2.finalize();
+ run_next_test();
+}
+
+function test_bind_empty_array() {
+ let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)");
+
+ let paramsArray = stmt.newBindingParamsArray();
+
+ // We should not be able to bind this array to the statement because it is
+ // empty.
+ expectError(Cr.NS_ERROR_UNEXPECTED, () => stmt.bindParameters(paramsArray));
+
+ stmt.finalize();
+ run_next_test();
+}
+
+function test_multiple_results() {
+ let expectedResults = getTableRowCount("test");
+ // Sanity check - we should have more than one result, but let's be sure.
+ Assert.ok(expectedResults > 1);
+
+ // Now check that we get back two rows of data from our async query.
+ let stmt = makeTestStatement("SELECT * FROM test");
+ execAsync(stmt, {}, expectedResults);
+
+ stmt.finalize();
+ run_next_test();
+}
+
+// Test Runner
+
+const TEST_PASS_SYNC = 0;
+const TEST_PASS_ASYNC = 1;
+/**
+ * We run 2 passes against the test. One where makeTestStatement generates
+ * synchronous (mozIStorageStatement) statements and one where it generates
+ * asynchronous (mozIStorageAsyncStatement) statements.
+ *
+ * Because of differences in the ability to know the number of parameters before
+ * dispatching, some tests are sync/async specific. These functions are marked
+ * with 'syncOnly' or 'asyncOnly' attributes and run_next_test knows what to do.
+ */
+var testPass = TEST_PASS_SYNC;
+
+/**
+ * Create a statement of the type under test per testPass.
+ *
+ * @param aSQL
+ * The SQL string from which to build a statement.
+ * @return a statement of the type under test per testPass.
+ */
+function makeTestStatement(aSQL) {
+ if (testPass == TEST_PASS_SYNC) {
+ return getOpenedDatabase().createStatement(aSQL);
+ }
+ return getOpenedDatabase().createAsyncStatement(aSQL);
+}
+
+var tests = [
+ test_illegal_sql_async_deferred,
+ test_create_table,
+ test_add_data,
+ test_get_data,
+ test_tuple_out_of_bounds,
+ test_no_listener_works_on_success,
+ test_no_listener_works_on_results,
+ test_no_listener_works_on_error,
+ test_partial_listener_works,
+ test_immediate_cancellation,
+ test_double_cancellation,
+ test_cancellation_after_execution,
+ test_double_execute,
+ test_finalized_statement_does_not_crash,
+ test_bind_direct_binding_params_by_index,
+ test_bind_direct_binding_params_by_name,
+ test_bind_js_params_helper_by_index,
+ test_bind_js_params_helper_by_name,
+ test_bind_multiple_rows_by_index,
+ test_bind_multiple_rows_by_name,
+ test_bind_out_of_bounds_sync_immediate,
+ test_bind_out_of_bounds_async_deferred,
+ test_bind_no_such_name_sync_immediate,
+ test_bind_no_such_name_async_deferred,
+ test_bind_bogus_type_by_index,
+ test_bind_bogus_type_by_name,
+ test_bind_params_already_locked,
+ test_bind_params_array_already_locked,
+ test_bind_empty_array,
+ test_no_binding_params_from_locked_array,
+ test_not_right_owning_array,
+ test_not_right_owning_statement,
+ test_multiple_results,
+];
+var index = 0;
+
+const STARTING_UNIQUE_ID = 2;
+var nextUniqueId = STARTING_UNIQUE_ID;
+
+function run_next_test() {
+ function _run_next_test() {
+ // use a loop so we can skip tests...
+ while (index < tests.length) {
+ let test = tests[index++];
+ // skip tests not appropriate to the current test pass
+ if (
+ (testPass == TEST_PASS_SYNC && "asyncOnly" in test) ||
+ (testPass == TEST_PASS_ASYNC && "syncOnly" in test)
+ ) {
+ continue;
+ }
+
+ // Asynchronous tests means that exceptions don't kill the test.
+ try {
+ print("****** Running the next test: " + test.name);
+ test();
+ return;
+ } catch (e) {
+ do_throw(e);
+ }
+ }
+
+ // if we only completed the first pass, move to the next pass
+ if (testPass == TEST_PASS_SYNC) {
+ print("********* Beginning mozIStorageAsyncStatement pass.");
+ testPass++;
+ index = 0;
+ // a new pass demands a new database
+ asyncCleanup();
+ nextUniqueId = STARTING_UNIQUE_ID;
+ _run_next_test();
+ return;
+ }
+
+ // we did some async stuff; we need to clean up.
+ asyncCleanup();
+ do_test_finished();
+ }
+
+ // Don't actually schedule another test if we're quitting.
+ if (!_quit) {
+ // For saner stacks, we execute this code RSN.
+ executeSoon(_run_next_test);
+ }
+}
+
+function run_test() {
+ // Thunderbird doesn't have one or more of the probes used in this test.
+ // Ensure the data is collected anyway.
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+
+ cleanup();
+
+ do_test_pending();
+ run_next_test();
+}