summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/tests/xpcshell
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /toolkit/modules/tests/xpcshell
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'toolkit/modules/tests/xpcshell')
-rw-r--r--toolkit/modules/tests/xpcshell/.eslintrc.js7
-rw-r--r--toolkit/modules/tests/xpcshell/TestIntegration.jsm42
-rw-r--r--toolkit/modules/tests/xpcshell/chromeappsstore.sqlitebin0 -> 262144 bytes
-rw-r--r--toolkit/modules/tests/xpcshell/propertyLists/bug710259_propertyListBinary.plistbin0 -> 3277 bytes
-rw-r--r--toolkit/modules/tests/xpcshell/propertyLists/bug710259_propertyListXML.plist28
-rw-r--r--toolkit/modules/tests/xpcshell/test_BinarySearch.js81
-rw-r--r--toolkit/modules/tests/xpcshell/test_CanonicalJSON.js146
-rw-r--r--toolkit/modules/tests/xpcshell/test_Color.js53
-rw-r--r--toolkit/modules/tests/xpcshell/test_DeferredTask.js390
-rw-r--r--toolkit/modules/tests/xpcshell/test_FileUtils.js226
-rw-r--r--toolkit/modules/tests/xpcshell/test_FinderIterator.js265
-rw-r--r--toolkit/modules/tests/xpcshell/test_GMPInstallManager.js794
-rw-r--r--toolkit/modules/tests/xpcshell/test_Http.js257
-rw-r--r--toolkit/modules/tests/xpcshell/test_Integration.js238
-rw-r--r--toolkit/modules/tests/xpcshell/test_JSONFile.js242
-rw-r--r--toolkit/modules/tests/xpcshell/test_Log.js592
-rw-r--r--toolkit/modules/tests/xpcshell/test_Log_stackTrace.js30
-rw-r--r--toolkit/modules/tests/xpcshell/test_MatchGlobs.js58
-rw-r--r--toolkit/modules/tests/xpcshell/test_MatchPattern.js95
-rw-r--r--toolkit/modules/tests/xpcshell/test_MatchURLFilters.js396
-rw-r--r--toolkit/modules/tests/xpcshell/test_NewTabUtils.js378
-rw-r--r--toolkit/modules/tests/xpcshell/test_ObjectUtils.js96
-rw-r--r--toolkit/modules/tests/xpcshell/test_ObjectUtils_strict.js29
-rw-r--r--toolkit/modules/tests/xpcshell/test_PermissionsUtils.js85
-rw-r--r--toolkit/modules/tests/xpcshell/test_Preferences.js378
-rw-r--r--toolkit/modules/tests/xpcshell/test_Promise.js1105
-rw-r--r--toolkit/modules/tests/xpcshell/test_PromiseUtils.js105
-rw-r--r--toolkit/modules/tests/xpcshell/test_Services.js90
-rw-r--r--toolkit/modules/tests/xpcshell/test_UpdateUtils_updatechannel.js38
-rw-r--r--toolkit/modules/tests/xpcshell/test_UpdateUtils_url.js292
-rw-r--r--toolkit/modules/tests/xpcshell/test_ZipUtils.js79
-rw-r--r--toolkit/modules/tests/xpcshell/test_client_id.js95
-rw-r--r--toolkit/modules/tests/xpcshell/test_jsesc.js9
-rw-r--r--toolkit/modules/tests/xpcshell/test_propertyListsUtils.js106
-rw-r--r--toolkit/modules/tests/xpcshell/test_readCertPrefs.js97
-rw-r--r--toolkit/modules/tests/xpcshell/test_servicerequest_xhr.js25
-rw-r--r--toolkit/modules/tests/xpcshell/test_session_recorder.js306
-rw-r--r--toolkit/modules/tests/xpcshell/test_sqlite.js1094
-rw-r--r--toolkit/modules/tests/xpcshell/test_sqlite_shutdown.js122
-rw-r--r--toolkit/modules/tests/xpcshell/test_task.js642
-rw-r--r--toolkit/modules/tests/xpcshell/test_timer.js57
-rw-r--r--toolkit/modules/tests/xpcshell/test_web_channel.js149
-rw-r--r--toolkit/modules/tests/xpcshell/test_web_channel_broker.js88
-rw-r--r--toolkit/modules/tests/xpcshell/xpcshell.ini75
-rw-r--r--toolkit/modules/tests/xpcshell/zips/zen.zipbin0 -> 1226 bytes
45 files changed, 9480 insertions, 0 deletions
diff --git a/toolkit/modules/tests/xpcshell/.eslintrc.js b/toolkit/modules/tests/xpcshell/.eslintrc.js
new file mode 100644
index 000000000..fee088c17
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/modules/tests/xpcshell/TestIntegration.jsm b/toolkit/modules/tests/xpcshell/TestIntegration.jsm
new file mode 100644
index 000000000..78a0b7267
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/TestIntegration.jsm
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Internal module used to test the generation of Integration.jsm getters.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "TestIntegration",
+];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Task.jsm");
+
+this.TestIntegration = {
+ value: "value",
+
+ get valueFromThis() {
+ return this.value;
+ },
+
+ get property() {
+ return this._property;
+ },
+
+ set property(value) {
+ this._property = value;
+ },
+
+ method(argument) {
+ this.methodArgument = argument;
+ return "method" + argument;
+ },
+
+ asyncMethod: Task.async(function* (argument) {
+ this.asyncMethodArgument = argument;
+ return "asyncMethod" + argument;
+ }),
+};
diff --git a/toolkit/modules/tests/xpcshell/chromeappsstore.sqlite b/toolkit/modules/tests/xpcshell/chromeappsstore.sqlite
new file mode 100644
index 000000000..15d309df5
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/chromeappsstore.sqlite
Binary files differ
diff --git a/toolkit/modules/tests/xpcshell/propertyLists/bug710259_propertyListBinary.plist b/toolkit/modules/tests/xpcshell/propertyLists/bug710259_propertyListBinary.plist
new file mode 100644
index 000000000..5888c9c9c
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/propertyLists/bug710259_propertyListBinary.plist
Binary files differ
diff --git a/toolkit/modules/tests/xpcshell/propertyLists/bug710259_propertyListXML.plist b/toolkit/modules/tests/xpcshell/propertyLists/bug710259_propertyListXML.plist
new file mode 100644
index 000000000..9b6decc1e
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/propertyLists/bug710259_propertyListXML.plist
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>Boolean</key>
+ <false/>
+ <key>Array</key>
+ <array>
+ <string>abc</string>
+ <string>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</string>
+ <string>אאא</string>
+ <string>אאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאא</string>
+ <string>𐀀𐀀𐀀</string>
+ <date>2011-12-31T11:15:23Z</date>
+ <data>MjAxMS0xMi0zMVQxMToxNTozM1o=</data>
+ <dict>
+ <key>Negative Number</key>
+ <integer>-400</integer>
+ <key>Real Number</key>
+ <real>2.71828183</real>
+ <key>Big Int</key>
+ <integer>9007199254740993</integer>
+ <key>Negative Big Int</key>
+ <integer>-9007199254740993</integer>
+ </dict>
+ </array>
+</dict>
+</plist>
diff --git a/toolkit/modules/tests/xpcshell/test_BinarySearch.js b/toolkit/modules/tests/xpcshell/test_BinarySearch.js
new file mode 100644
index 000000000..f48b0bccf
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_BinarySearch.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/BinarySearch.jsm");
+
+function run_test() {
+ // empty array
+ ok([], 1, false, 0);
+
+ // one-element array
+ ok([2], 2, true, 0);
+ ok([2], 1, false, 0);
+ ok([2], 3, false, 1);
+
+ // two-element array
+ ok([2, 4], 2, true, 0);
+ ok([2, 4], 4, true, 1);
+ ok([2, 4], 1, false, 0);
+ ok([2, 4], 3, false, 1);
+ ok([2, 4], 5, false, 2);
+
+ // three-element array
+ ok([2, 4, 6], 2, true, 0);
+ ok([2, 4, 6], 4, true, 1);
+ ok([2, 4, 6], 6, true, 2);
+ ok([2, 4, 6], 1, false, 0);
+ ok([2, 4, 6], 3, false, 1);
+ ok([2, 4, 6], 5, false, 2);
+ ok([2, 4, 6], 7, false, 3);
+
+ // duplicates
+ ok([2, 2], 2, true, 0);
+ ok([2, 2], 1, false, 0);
+ ok([2, 2], 3, false, 2);
+
+ // duplicates on the left
+ ok([2, 2, 4], 2, true, 1);
+ ok([2, 2, 4], 4, true, 2);
+ ok([2, 2, 4], 1, false, 0);
+ ok([2, 2, 4], 3, false, 2);
+ ok([2, 2, 4], 5, false, 3);
+
+ // duplicates on the right
+ ok([2, 4, 4], 2, true, 0);
+ ok([2, 4, 4], 4, true, 1);
+ ok([2, 4, 4], 1, false, 0);
+ ok([2, 4, 4], 3, false, 1);
+ ok([2, 4, 4], 5, false, 3);
+
+ // duplicates in the middle
+ ok([2, 4, 4, 6], 2, true, 0);
+ ok([2, 4, 4, 6], 4, true, 1);
+ ok([2, 4, 4, 6], 6, true, 3);
+ ok([2, 4, 4, 6], 1, false, 0);
+ ok([2, 4, 4, 6], 3, false, 1);
+ ok([2, 4, 4, 6], 5, false, 3);
+ ok([2, 4, 4, 6], 7, false, 4);
+
+ // duplicates all around
+ ok([2, 2, 4, 4, 6, 6], 2, true, 0);
+ ok([2, 2, 4, 4, 6, 6], 4, true, 2);
+ ok([2, 2, 4, 4, 6, 6], 6, true, 4);
+ ok([2, 2, 4, 4, 6, 6], 1, false, 0);
+ ok([2, 2, 4, 4, 6, 6], 3, false, 2);
+ ok([2, 2, 4, 4, 6, 6], 5, false, 4);
+ ok([2, 2, 4, 4, 6, 6], 7, false, 6);
+}
+
+function ok(array, target, expectedFound, expectedIdx) {
+ let [found, idx] = BinarySearch.search(cmp, array, target);
+ do_check_eq(found, expectedFound);
+ do_check_eq(idx, expectedIdx);
+
+ idx = expectedFound ? expectedIdx : -1;
+ do_check_eq(BinarySearch.indexOf(cmp, array, target), idx);
+ do_check_eq(BinarySearch.insertionIndexOf(cmp, array, target), expectedIdx);
+}
+
+function cmp(num1, num2) {
+ return num1 - num2;
+}
diff --git a/toolkit/modules/tests/xpcshell/test_CanonicalJSON.js b/toolkit/modules/tests/xpcshell/test_CanonicalJSON.js
new file mode 100644
index 000000000..fa61f5a01
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_CanonicalJSON.js
@@ -0,0 +1,146 @@
+const { CanonicalJSON } = Components.utils.import("resource://gre/modules/CanonicalJSON.jsm");
+
+function stringRepresentation(obj) {
+ const clone = JSON.parse(JSON.stringify(obj));
+ return JSON.stringify(clone);
+}
+
+add_task(function* test_canonicalJSON_should_preserve_array_order() {
+ const input = ['one', 'two', 'three'];
+ // No sorting should be done on arrays.
+ do_check_eq(CanonicalJSON.stringify(input), '["one","two","three"]');
+});
+
+add_task(function* test_canonicalJSON_orders_object_keys() {
+ const input = [{
+ b: ['two', 'three'],
+ a: ['zero', 'one']
+ }];
+ do_check_eq(
+ CanonicalJSON.stringify(input),
+ '[{"a":["zero","one"],"b":["two","three"]}]'
+ );
+});
+
+add_task(function* test_canonicalJSON_orders_nested_object_keys() {
+ const input = [{
+ b: {d: 'd', c: 'c'},
+ a: {b: 'b', a: 'a'}
+ }];
+ do_check_eq(
+ CanonicalJSON.stringify(input),
+ '[{"a":{"a":"a","b":"b"},"b":{"c":"c","d":"d"}}]'
+ );
+});
+
+add_task(function* test_canonicalJSON_escapes_unicode_values() {
+ do_check_eq(
+ CanonicalJSON.stringify([{key: '✓'}]),
+ '[{"key":"\\u2713"}]'
+ );
+ // Unicode codepoints should be output in lowercase.
+ do_check_eq(
+ CanonicalJSON.stringify([{key: 'é'}]),
+ '[{"key":"\\u00e9"}]'
+ );
+});
+
+add_task(function* test_canonicalJSON_escapes_unicode_object_keys() {
+ do_check_eq(
+ CanonicalJSON.stringify([{'é': 'check'}]),
+ '[{"\\u00e9":"check"}]'
+ );
+});
+
+
+add_task(function* test_canonicalJSON_does_not_alter_input() {
+ const records = [
+ {'foo': 'bar', 'last_modified': '12345', 'id': '1'},
+ {'bar': 'baz', 'last_modified': '45678', 'id': '2'}
+ ];
+ const serializedJSON = JSON.stringify(records);
+ CanonicalJSON.stringify(records);
+ do_check_eq(JSON.stringify(records), serializedJSON);
+});
+
+
+add_task(function* test_canonicalJSON_preserves_data() {
+ const records = [
+ {'foo': 'bar', 'last_modified': '12345', 'id': '1'},
+ {'bar': 'baz', 'last_modified': '45678', 'id': '2'},
+ ]
+ const serialized = CanonicalJSON.stringify(records);
+ const expected = '[{"foo":"bar","id":"1","last_modified":"12345"},' +
+ '{"bar":"baz","id":"2","last_modified":"45678"}]';
+ do_check_eq(CanonicalJSON.stringify(records), expected);
+});
+
+add_task(function* test_canonicalJSON_does_not_add_space_separators() {
+ const records = [
+ {'foo': 'bar', 'last_modified': '12345', 'id': '1'},
+ {'bar': 'baz', 'last_modified': '45678', 'id': '2'},
+ ]
+ const serialized = CanonicalJSON.stringify(records);
+ do_check_false(serialized.includes(" "));
+});
+
+add_task(function* test_canonicalJSON_serializes_empty_object() {
+ do_check_eq(CanonicalJSON.stringify({}), "{}");
+});
+
+add_task(function* test_canonicalJSON_serializes_empty_array() {
+ do_check_eq(CanonicalJSON.stringify([]), "[]");
+});
+
+add_task(function* test_canonicalJSON_serializes_NaN() {
+ do_check_eq(CanonicalJSON.stringify(NaN), "null");
+});
+
+add_task(function* test_canonicalJSON_serializes_inf() {
+ // This isn't part of the JSON standard.
+ do_check_eq(CanonicalJSON.stringify(Infinity), "null");
+});
+
+
+add_task(function* test_canonicalJSON_serializes_empty_string() {
+ do_check_eq(CanonicalJSON.stringify(""), '""');
+});
+
+add_task(function* test_canonicalJSON_escapes_backslashes() {
+ do_check_eq(CanonicalJSON.stringify("This\\and this"), '"This\\\\and this"');
+});
+
+add_task(function* test_canonicalJSON_handles_signed_zeros() {
+ // do_check_eq doesn't support comparison of -0 and 0 properly.
+ do_check_true(CanonicalJSON.stringify(-0) === '-0');
+ do_check_true(CanonicalJSON.stringify(0) === '0');
+});
+
+
+add_task(function* test_canonicalJSON_with_deeply_nested_dicts() {
+ const records = [{
+ 'a': {
+ 'b': 'b',
+ 'a': 'a',
+ 'c': {
+ 'b': 'b',
+ 'a': 'a',
+ 'c': ['b', 'a', 'c'],
+ 'd': {'b': 'b', 'a': 'a'},
+ 'id': '1',
+ 'e': 1,
+ 'f': [2, 3, 1],
+ 'g': {2: 2, 3: 3, 1: {
+ 'b': 'b', 'a': 'a', 'c': 'c'}}}},
+ 'id': '1'}]
+ const expected =
+ '[{"a":{"a":"a","b":"b","c":{"a":"a","b":"b","c":["b","a","c"],' +
+ '"d":{"a":"a","b":"b"},"e":1,"f":[2,3,1],"g":{' +
+ '"1":{"a":"a","b":"b","c":"c"},"2":2,"3":3},"id":"1"}},"id":"1"}]';
+
+ do_check_eq(CanonicalJSON.stringify(records), expected);
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/modules/tests/xpcshell/test_Color.js b/toolkit/modules/tests/xpcshell/test_Color.js
new file mode 100644
index 000000000..9bf9bf861
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_Color.js
@@ -0,0 +1,53 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/Color.jsm");
+
+function run_test() {
+ testRelativeLuminance();
+ testIsBright();
+ testContrastRatio();
+ testIsContrastRatioAcceptable();
+}
+
+function testRelativeLuminance() {
+ let c = new Color(0, 0, 0);
+ Assert.equal(c.relativeLuminance, 0, "Black is not illuminating");
+
+ c = new Color(255, 255, 255);
+ Assert.equal(c.relativeLuminance, 1, "White is quite the luminant one");
+
+ c = new Color(142, 42, 142);
+ Assert.equal(c.relativeLuminance, 0.25263952353998204,
+ "This purple is not that luminant");
+}
+
+function testIsBright() {
+ let c = new Color(0, 0, 0);
+ Assert.equal(c.isBright, 0, "Black is bright");
+
+ c = new Color(255, 255, 255);
+ Assert.equal(c.isBright, 1, "White is bright");
+}
+
+function testContrastRatio() {
+ let c = new Color(0, 0, 0);
+ let c2 = new Color(255, 255, 255);
+ Assert.equal(c.contrastRatio(c2), 21, "Contrast between black and white is max");
+ Assert.equal(c.contrastRatio(c), 1, "Contrast between equals is min");
+
+ let c3 = new Color(142, 42, 142);
+ Assert.equal(c.contrastRatio(c3), 6.05279047079964, "Contrast between black and purple");
+ Assert.equal(c2.contrastRatio(c3), 3.469474137806338, "Contrast between white and purple");
+}
+
+function testIsContrastRatioAcceptable() {
+ // Let's assert what browser.js is doing for window frames.
+ let c = new Color(...[55, 156, 152]);
+ let c2 = new Color(0, 0, 0);
+ Assert.equal(c.r, 55, "Reds should match");
+ Assert.equal(c.g, 156, "Greens should match");
+ Assert.equal(c.b, 152, "Blues should match");
+ Assert.ok(c.isContrastRatioAcceptable(c2), "The blue is high contrast enough");
+ c = new Color(...[35, 65, 100]);
+ Assert.ok(!c.isContrastRatioAcceptable(c2), "The blue is not high contrast enough");
+}
diff --git a/toolkit/modules/tests/xpcshell/test_DeferredTask.js b/toolkit/modules/tests/xpcshell/test_DeferredTask.js
new file mode 100644
index 000000000..441f9054c
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_DeferredTask.js
@@ -0,0 +1,390 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the DeferredTask.jsm module.
+ */
+
+// Globals
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
+ "resource://gre/modules/DeferredTask.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+
+/**
+ * Due to the nature of this module, most of the tests are time-dependent. All
+ * the timeouts are designed to occur at multiples of this granularity value,
+ * in milliseconds, that should be high enough to prevent intermittent failures,
+ * but low enough to prevent an excessive overall test execution time.
+ */
+const T = 100;
+
+/**
+ * Waits for the specified timeout before resolving the returned promise.
+ */
+function promiseTimeout(aTimeoutMs)
+{
+ let deferred = Promise.defer();
+ do_timeout(aTimeoutMs, deferred.resolve);
+ return deferred.promise;
+}
+
+function run_test()
+{
+ run_next_test();
+}
+
+// Tests
+
+/**
+ * Creates a simple DeferredTask and executes it once.
+ */
+add_test(function test_arm_simple()
+{
+ new DeferredTask(run_next_test, 10).arm();
+});
+
+/**
+ * Checks that the delay set for the task is respected.
+ */
+add_test(function test_arm_delay_respected()
+{
+ let executed1 = false;
+ let executed2 = false;
+
+ new DeferredTask(function () {
+ executed1 = true;
+ do_check_false(executed2);
+ }, 1*T).arm();
+
+ new DeferredTask(function () {
+ executed2 = true;
+ do_check_true(executed1);
+ run_next_test();
+ }, 2*T).arm();
+});
+
+/**
+ * Checks that calling "arm" again does not introduce further delay.
+ */
+add_test(function test_arm_delay_notrestarted()
+{
+ let executed = false;
+
+ // Create a task that will run later.
+ let deferredTask = new DeferredTask(() => { executed = true; }, 4*T);
+ deferredTask.arm();
+
+ // Before the task starts, call "arm" again.
+ do_timeout(2*T, () => deferredTask.arm());
+
+ // The "arm" call should not have introduced further delays.
+ do_timeout(5*T, function () {
+ do_check_true(executed);
+ run_next_test();
+ });
+});
+
+/**
+ * Checks that a task runs only once when armed multiple times synchronously.
+ */
+add_test(function test_arm_coalesced()
+{
+ let executed = false;
+
+ let deferredTask = new DeferredTask(function () {
+ do_check_false(executed);
+ executed = true;
+ run_next_test();
+ }, 50);
+
+ deferredTask.arm();
+ deferredTask.arm();
+});
+
+/**
+ * Checks that a task runs only once when armed multiple times synchronously,
+ * even when it has been created with a delay of zero milliseconds.
+ */
+add_test(function test_arm_coalesced_nodelay()
+{
+ let executed = false;
+
+ let deferredTask = new DeferredTask(function () {
+ do_check_false(executed);
+ executed = true;
+ run_next_test();
+ }, 0);
+
+ deferredTask.arm();
+ deferredTask.arm();
+});
+
+/**
+ * Checks that a task can be armed again while running.
+ */
+add_test(function test_arm_recursive()
+{
+ let executed = false;
+
+ let deferredTask = new DeferredTask(function () {
+ if (!executed) {
+ executed = true;
+ deferredTask.arm();
+ } else {
+ run_next_test();
+ }
+ }, 50);
+
+ deferredTask.arm();
+});
+
+/**
+ * Checks that calling "arm" while an asynchronous task is running waits until
+ * the task is finished before restarting the delay.
+ */
+add_test(function test_arm_async()
+{
+ let finishedExecution = false;
+ let finishedExecutionAgain = false;
+
+ // Create a task that will run later.
+ let deferredTask = new DeferredTask(function* () {
+ yield promiseTimeout(4*T);
+ if (!finishedExecution) {
+ finishedExecution = true;
+ } else if (!finishedExecutionAgain) {
+ finishedExecutionAgain = true;
+ }
+ }, 2*T);
+ deferredTask.arm();
+
+ // While the task is running, call "arm" again. This will result in a wait
+ // of 2*T until the task finishes, then another 2*T for the normal task delay
+ // specified on construction.
+ do_timeout(4*T, function () {
+ do_check_true(deferredTask.isRunning);
+ do_check_false(finishedExecution);
+ deferredTask.arm();
+ });
+
+ // This will fail in case the task was started without waiting 2*T after it
+ // has finished.
+ do_timeout(7*T, function () {
+ do_check_false(deferredTask.isRunning);
+ do_check_true(finishedExecution);
+ });
+
+ // This is in the middle of the second execution.
+ do_timeout(10*T, function () {
+ do_check_true(deferredTask.isRunning);
+ do_check_false(finishedExecutionAgain);
+ });
+
+ // Wait enough time to verify that the task was executed as expected.
+ do_timeout(13*T, function () {
+ do_check_false(deferredTask.isRunning);
+ do_check_true(finishedExecutionAgain);
+ run_next_test();
+ });
+});
+
+/**
+ * Checks that an armed task can be disarmed.
+ */
+add_test(function test_disarm()
+{
+ // Create a task that will run later.
+ let deferredTask = new DeferredTask(function () {
+ do_throw("This task should not run.");
+ }, 2*T);
+ deferredTask.arm();
+
+ // Disable execution later, but before the task starts.
+ do_timeout(1*T, () => deferredTask.disarm());
+
+ // Wait enough time to verify that the task did not run.
+ do_timeout(3*T, run_next_test);
+});
+
+/**
+ * Checks that calling "disarm" allows the delay to be restarted.
+ */
+add_test(function test_disarm_delay_restarted()
+{
+ let executed = false;
+
+ let deferredTask = new DeferredTask(() => { executed = true; }, 4*T);
+ deferredTask.arm();
+
+ do_timeout(2*T, function () {
+ deferredTask.disarm();
+ deferredTask.arm();
+ });
+
+ do_timeout(5*T, function () {
+ do_check_false(executed);
+ });
+
+ do_timeout(7*T, function () {
+ do_check_true(executed);
+ run_next_test();
+ });
+});
+
+/**
+ * Checks that calling "disarm" while an asynchronous task is running does not
+ * prevent the task to finish.
+ */
+add_test(function test_disarm_async()
+{
+ let finishedExecution = false;
+
+ let deferredTask = new DeferredTask(function* () {
+ deferredTask.arm();
+ yield promiseTimeout(2*T);
+ finishedExecution = true;
+ }, 1*T);
+ deferredTask.arm();
+
+ do_timeout(2*T, function () {
+ do_check_true(deferredTask.isRunning);
+ do_check_true(deferredTask.isArmed);
+ do_check_false(finishedExecution);
+ deferredTask.disarm();
+ });
+
+ do_timeout(4*T, function () {
+ do_check_false(deferredTask.isRunning);
+ do_check_false(deferredTask.isArmed);
+ do_check_true(finishedExecution);
+ run_next_test();
+ });
+});
+
+/**
+ * Checks that calling "arm" immediately followed by "disarm" while an
+ * asynchronous task is running does not cause it to run again.
+ */
+add_test(function test_disarm_immediate_async()
+{
+ let executed = false;
+
+ let deferredTask = new DeferredTask(function* () {
+ do_check_false(executed);
+ executed = true;
+ yield promiseTimeout(2*T);
+ }, 1*T);
+ deferredTask.arm();
+
+ do_timeout(2*T, function () {
+ do_check_true(deferredTask.isRunning);
+ do_check_false(deferredTask.isArmed);
+ deferredTask.arm();
+ deferredTask.disarm();
+ });
+
+ do_timeout(4*T, function () {
+ do_check_true(executed);
+ do_check_false(deferredTask.isRunning);
+ do_check_false(deferredTask.isArmed);
+ run_next_test();
+ });
+});
+
+/**
+ * Checks the isArmed and isRunning properties with a synchronous task.
+ */
+add_test(function test_isArmed_isRunning()
+{
+ let deferredTask = new DeferredTask(function () {
+ do_check_true(deferredTask.isRunning);
+ do_check_false(deferredTask.isArmed);
+ deferredTask.arm();
+ do_check_true(deferredTask.isArmed);
+ deferredTask.disarm();
+ do_check_false(deferredTask.isArmed);
+ run_next_test();
+ }, 50);
+
+ do_check_false(deferredTask.isArmed);
+ deferredTask.arm();
+ do_check_true(deferredTask.isArmed);
+ do_check_false(deferredTask.isRunning);
+});
+
+/**
+ * Checks that the "finalize" method executes a synchronous task.
+ */
+add_test(function test_finalize()
+{
+ let executed = false;
+ let timePassed = false;
+
+ let deferredTask = new DeferredTask(function () {
+ do_check_false(timePassed);
+ executed = true;
+ }, 2*T);
+ deferredTask.arm();
+
+ do_timeout(1*T, () => { timePassed = true; });
+
+ // This should trigger the immediate execution of the task.
+ deferredTask.finalize().then(function () {
+ do_check_true(executed);
+ run_next_test();
+ });
+});
+
+/**
+ * Checks that the "finalize" method executes the task again from start to
+ * finish in case it is already running.
+ */
+add_test(function test_finalize_executes_entirely()
+{
+ let executed = false;
+ let executedAgain = false;
+ let timePassed = false;
+
+ let deferredTask = new DeferredTask(function* () {
+ // The first time, we arm the timer again and set up the finalization.
+ if (!executed) {
+ deferredTask.arm();
+ do_check_true(deferredTask.isArmed);
+ do_check_true(deferredTask.isRunning);
+
+ deferredTask.finalize().then(function () {
+ // When we reach this point, the task must be finished.
+ do_check_true(executedAgain);
+ do_check_false(timePassed);
+ do_check_false(deferredTask.isArmed);
+ do_check_false(deferredTask.isRunning);
+ run_next_test();
+ });
+
+ // The second execution triggered by the finalization waits 1*T for the
+ // current task to finish (see the timeout below), but then it must not
+ // wait for the 2*T specified on construction as normal task delay. The
+ // second execution will finish after the timeout below has passed again,
+ // for a total of 2*T of wait time.
+ do_timeout(3*T, () => { timePassed = true; });
+ }
+
+ yield promiseTimeout(1*T);
+
+ // Just before finishing, indicate if we completed the second execution.
+ if (executed) {
+ do_check_true(deferredTask.isRunning);
+ executedAgain = true;
+ } else {
+ executed = true;
+ }
+ }, 2*T);
+
+ deferredTask.arm();
+});
diff --git a/toolkit/modules/tests/xpcshell/test_FileUtils.js b/toolkit/modules/tests/xpcshell/test_FileUtils.js
new file mode 100644
index 000000000..86ac74389
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_FileUtils.js
@@ -0,0 +1,226 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/FileUtils.jsm");
+
+function do_check_throws(f, result, stack) {
+ if (!stack)
+ stack = Components.stack.caller;
+
+ try {
+ f();
+ } catch (exc) {
+ if (exc.result == result)
+ return;
+ do_throw("expected result " + result + ", caught " + exc, stack);
+ }
+ do_throw("expected result " + result + ", none thrown", stack);
+}
+
+const gProfD = do_get_profile();
+
+add_test(function test_getFile() {
+ let file = FileUtils.getFile("ProfD", ["foobar"]);
+ do_check_true(file instanceof Components.interfaces.nsIFile);
+ do_check_false(file.exists());
+
+ let other = gProfD.clone();
+ other.append("foobar");
+ do_check_true(file.equals(other));
+
+ run_next_test();
+});
+
+add_test(function test_getFile_nonexistentDir() {
+ do_check_throws(function () {
+ let file = FileUtils.getFile("NonexistentD", ["foobar"]);
+ }, Components.results.NS_ERROR_FAILURE);
+
+ run_next_test();
+});
+
+add_test(function test_getFile_createDirs() {
+ let file = FileUtils.getFile("ProfD", ["a", "b", "foobar"]);
+ do_check_true(file instanceof Components.interfaces.nsIFile);
+ do_check_false(file.exists());
+
+ let other = gProfD.clone();
+ other.append("a");
+ do_check_true(other.isDirectory());
+ other.append("b");
+ do_check_true(other.isDirectory());
+ other.append("foobar");
+ do_check_true(file.equals(other));
+
+ run_next_test();
+});
+
+add_test(function test_getDir() {
+ let dir = FileUtils.getDir("ProfD", ["foodir"]);
+ do_check_true(dir instanceof Components.interfaces.nsIFile);
+ do_check_false(dir.exists());
+
+ let other = gProfD.clone();
+ other.append("foodir");
+ do_check_true(dir.equals(other));
+
+ run_next_test();
+});
+
+add_test(function test_getDir_nonexistentDir() {
+ do_check_throws(function () {
+ let file = FileUtils.getDir("NonexistentD", ["foodir"]);
+ }, Components.results.NS_ERROR_FAILURE);
+
+ run_next_test();
+});
+
+add_test(function test_getDir_shouldCreate() {
+ let dir = FileUtils.getDir("ProfD", ["c", "d", "foodir"], true);
+ do_check_true(dir instanceof Components.interfaces.nsIFile);
+ do_check_true(dir.exists());
+
+ let other = gProfD.clone();
+ other.append("c");
+ do_check_true(other.isDirectory());
+ other.append("d");
+ do_check_true(other.isDirectory());
+ other.append("foodir");
+ do_check_true(dir.equals(other));
+
+ run_next_test();
+});
+
+var openFileOutputStream_defaultFlags = function (aKind, aFileName) {
+ let file = FileUtils.getFile("ProfD", [aFileName]);
+ let fos;
+ do_check_true(aKind == "atomic" || aKind == "safe" || aKind == "");
+ if (aKind == "atomic") {
+ fos = FileUtils.openAtomicFileOutputStream(file);
+ } else if (aKind == "safe") {
+ fos = FileUtils.openSafeFileOutputStream(file);
+ } else {
+ fos = FileUtils.openFileOutputStream(file);
+ }
+ do_check_true(fos instanceof Components.interfaces.nsIFileOutputStream);
+ if (aKind == "atomic" || aKind == "safe") {
+ do_check_true(fos instanceof Components.interfaces.nsISafeOutputStream);
+ }
+
+ // FileUtils.openFileOutputStream or FileUtils.openAtomicFileOutputStream()
+ // or FileUtils.openSafeFileOutputStream() opens the stream with DEFER_OPEN
+ // which means the file will not be open until we write to it.
+ do_check_false(file.exists());
+
+ let data = "test_default_flags";
+ fos.write(data, data.length);
+ do_check_true(file.exists());
+
+ // No nsIXULRuntime in xpcshell, so use this trick to determine whether we're
+ // on Windows.
+ if ("@mozilla.org/windows-registry-key;1" in Components.classes) {
+ do_check_eq(file.permissions, 0o666);
+ } else {
+ do_check_eq(file.permissions, FileUtils.PERMS_FILE);
+ }
+
+ run_next_test();
+};
+
+var openFileOutputStream_modeFlags = function(aKind, aFileName) {
+ let file = FileUtils.getFile("ProfD", [aFileName]);
+ let fos;
+ do_check_true(aKind == "atomic" || aKind == "safe" || aKind == "");
+ if (aKind == "atomic") {
+ fos = FileUtils.openAtomicFileOutputStream(file, FileUtils.MODE_WRONLY);
+ } else if (aKind == "safe") {
+ fos = FileUtils.openSafeFileOutputStream(file, FileUtils.MODE_WRONLY);
+ } else {
+ fos = FileUtils.openFileOutputStream(file, FileUtils.MODE_WRONLY);
+ }
+ let data = "test_modeFlags";
+ do_check_throws(function () {
+ fos.write(data, data.length);
+ }, Components.results.NS_ERROR_FILE_NOT_FOUND);
+ do_check_false(file.exists());
+
+ run_next_test();
+};
+
+var closeFileOutputStream = function(aKind, aFileName) {
+ let file = FileUtils.getFile("ProfD", [aFileName]);
+ let fos;
+ do_check_true(aKind == "atomic" || aKind == "safe");
+ if (aKind == "atomic") {
+ fos = FileUtils.openAtomicFileOutputStream(file);
+ } else if (aKind == "safe") {
+ fos = FileUtils.openSafeFileOutputStream(file);
+ }
+
+ // We can write data to the stream just fine while it's open.
+ let data = "testClose";
+ fos.write(data, data.length);
+
+ // But once we close it, we can't anymore.
+ if (aKind == "atomic") {
+ FileUtils.closeAtomicFileOutputStream(fos);
+ } else if (aKind == "safe") {
+ FileUtils.closeSafeFileOutputStream(fos);
+ }
+ do_check_throws(function () {
+ fos.write(data, data.length);
+ }, Components.results.NS_BASE_STREAM_CLOSED);
+ run_next_test();
+};
+
+add_test(function test_openFileOutputStream_defaultFlags() {
+ openFileOutputStream_defaultFlags("", "george");
+});
+
+// openFileOutputStream will uses MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE
+// as the default mode flags, but we can pass in our own if we want to.
+add_test(function test_openFileOutputStream_modeFlags() {
+ openFileOutputStream_modeFlags("", "ringo");
+});
+
+add_test(function test_openAtomicFileOutputStream_defaultFlags() {
+ openFileOutputStream_defaultFlags("atomic", "peiyong");
+});
+
+// openAtomicFileOutputStream will uses MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE
+// as the default mode flags, but we can pass in our own if we want to.
+add_test(function test_openAtomicFileOutputStream_modeFlags() {
+ openFileOutputStream_modeFlags("atomic", "lin");
+});
+
+add_test(function test_closeAtomicFileOutputStream() {
+ closeFileOutputStream("atomic", "peiyonglin");
+});
+
+add_test(function test_openSafeFileOutputStream_defaultFlags() {
+ openFileOutputStream_defaultFlags("safe", "john");
+});
+
+// openSafeFileOutputStream will uses MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE
+// as the default mode flags, but we can pass in our own if we want to.
+add_test(function test_openSafeFileOutputStream_modeFlags() {
+ openFileOutputStream_modeFlags("safe", "paul");
+});
+
+add_test(function test_closeSafeFileOutputStream() {
+ closeFileOutputStream("safe", "georgee");
+});
+
+add_test(function test_newFile() {
+ let testfile = FileUtils.getFile("ProfD", ["test"]);
+ let testpath = testfile.path;
+ let file = new FileUtils.File(testpath);
+ do_check_true(file instanceof Components.interfaces.nsILocalFile);
+ do_check_true(file.equals(testfile));
+ do_check_eq(file.path, testpath);
+ run_next_test();
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/modules/tests/xpcshell/test_FinderIterator.js b/toolkit/modules/tests/xpcshell/test_FinderIterator.js
new file mode 100644
index 000000000..02c923a00
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_FinderIterator.js
@@ -0,0 +1,265 @@
+const { interfaces: Ci, classes: Cc, utils: Cu } = Components;
+const { FinderIterator } = Cu.import("resource://gre/modules/FinderIterator.jsm", {});
+Cu.import("resource://gre/modules/Promise.jsm");
+
+var gFindResults = [];
+// Stub the method that instantiates nsIFind and does all the interaction with
+// the docShell to be searched through.
+FinderIterator._iterateDocument = function* (word, window, finder) {
+ for (let range of gFindResults)
+ yield range;
+};
+
+FinderIterator._rangeStartsInLink = fakeRange => fakeRange.startsInLink;
+
+function FakeRange(textContent, startsInLink = false) {
+ this.startContainer = {};
+ this.startsInLink = startsInLink;
+ this.toString = () => textContent;
+}
+
+var gMockWindow = {
+ setTimeout(cb, delay) {
+ Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer)
+ .initWithCallback(cb, delay, Ci.nsITimer.TYPE_ONE_SHOT);
+ }
+};
+
+var gMockFinder = {
+ _getWindow() { return gMockWindow; }
+};
+
+function prepareIterator(findText, rangeCount) {
+ gFindResults = [];
+ for (let i = rangeCount; --i >= 0;)
+ gFindResults.push(new FakeRange(findText));
+}
+
+add_task(function* test_start() {
+ let findText = "test";
+ let rangeCount = 300;
+ prepareIterator(findText, rangeCount);
+
+ let count = 0;
+ yield FinderIterator.start({
+ caseSensitive: false,
+ entireWord: false,
+ finder: gMockFinder,
+ listener: {
+ onIteratorRangeFound(range) {
+ ++count;
+ Assert.equal(range.toString(), findText, "Text content should match");
+ }
+ },
+ word: findText
+ });
+
+ Assert.equal(rangeCount, count, "Amount of ranges yielded should match!");
+ Assert.ok(!FinderIterator.running, "Running state should match");
+ Assert.equal(FinderIterator._previousRanges.length, rangeCount, "Ranges cache should match");
+
+ FinderIterator.reset();
+});
+
+add_task(function* test_valid_arguments() {
+ let findText = "foo";
+ let rangeCount = 20;
+ prepareIterator(findText, rangeCount);
+
+ let count = 0;
+
+ yield FinderIterator.start({
+ caseSensitive: false,
+ entireWord: false,
+ finder: gMockFinder,
+ listener: { onIteratorRangeFound(range) { ++count; } },
+ word: findText
+ });
+
+ let params = FinderIterator._previousParams;
+ Assert.ok(!params.linksOnly, "Default for linksOnly is false");
+ Assert.ok(!params.useCache, "Default for useCache is false");
+ Assert.equal(params.word, findText, "Words should match");
+
+ count = 0;
+ Assert.throws(() => FinderIterator.start({
+ entireWord: false,
+ listener: { onIteratorRangeFound(range) { ++count; } },
+ word: findText
+ }), /Missing required option 'caseSensitive'/, "Should throw when missing an argument");
+ FinderIterator.reset();
+
+ Assert.throws(() => FinderIterator.start({
+ caseSensitive: false,
+ listener: { onIteratorRangeFound(range) { ++count; } },
+ word: findText
+ }), /Missing required option 'entireWord'/, "Should throw when missing an argument");
+ FinderIterator.reset();
+
+ Assert.throws(() => FinderIterator.start({
+ caseSensitive: false,
+ entireWord: false,
+ listener: { onIteratorRangeFound(range) { ++count; } },
+ word: findText
+ }), /Missing required option 'finder'/, "Should throw when missing an argument");
+ FinderIterator.reset();
+
+ Assert.throws(() => FinderIterator.start({
+ caseSensitive: true,
+ entireWord: false,
+ finder: gMockFinder,
+ word: findText
+ }), /Missing valid, required option 'listener'/, "Should throw when missing an argument");
+ FinderIterator.reset();
+
+ Assert.throws(() => FinderIterator.start({
+ caseSensitive: false,
+ entireWord: true,
+ finder: gMockFinder,
+ listener: { onIteratorRangeFound(range) { ++count; } },
+ }), /Missing required option 'word'/, "Should throw when missing an argument");
+ FinderIterator.reset();
+
+ Assert.equal(count, 0, "No ranges should've been counted");
+});
+
+add_task(function* test_stop() {
+ let findText = "bar";
+ let rangeCount = 120;
+ prepareIterator(findText, rangeCount);
+
+ let count = 0;
+ let whenDone = FinderIterator.start({
+ caseSensitive: false,
+ entireWord: false,
+ finder: gMockFinder,
+ listener: { onIteratorRangeFound(range) { ++count; } },
+ word: findText
+ });
+
+ FinderIterator.stop();
+
+ yield whenDone;
+
+ Assert.equal(count, 0, "Number of ranges should be 0");
+
+ FinderIterator.reset();
+});
+
+add_task(function* test_reset() {
+ let findText = "tik";
+ let rangeCount = 142;
+ prepareIterator(findText, rangeCount);
+
+ let count = 0;
+ let whenDone = FinderIterator.start({
+ caseSensitive: false,
+ entireWord: false,
+ finder: gMockFinder,
+ listener: { onIteratorRangeFound(range) { ++count; } },
+ word: findText
+ });
+
+ Assert.ok(FinderIterator.running, "Yup, running we are");
+ Assert.equal(count, 0, "Number of ranges should match 0");
+ Assert.equal(FinderIterator.ranges.length, 0, "Number of ranges should match 0");
+
+ FinderIterator.reset();
+
+ Assert.ok(!FinderIterator.running, "Nope, running we are not");
+ Assert.equal(FinderIterator.ranges.length, 0, "No ranges after reset");
+ Assert.equal(FinderIterator._previousRanges.length, 0, "No ranges after reset");
+
+ yield whenDone;
+
+ Assert.equal(count, 0, "Number of ranges should match 0");
+});
+
+add_task(function* test_parallel_starts() {
+ let findText = "tak";
+ let rangeCount = 2143;
+ prepareIterator(findText, rangeCount);
+
+ // Start off the iterator.
+ let count = 0;
+ let whenDone = FinderIterator.start({
+ caseSensitive: false,
+ entireWord: false,
+ finder: gMockFinder,
+ listener: { onIteratorRangeFound(range) { ++count; } },
+ word: findText
+ });
+
+ yield new Promise(resolve => gMockWindow.setTimeout(resolve, 120));
+ Assert.ok(FinderIterator.running, "We ought to be running here");
+
+ let count2 = 0;
+ let whenDone2 = FinderIterator.start({
+ caseSensitive: false,
+ entireWord: false,
+ finder: gMockFinder,
+ listener: { onIteratorRangeFound(range) { ++count2; } },
+ word: findText
+ });
+
+ // Let the iterator run for a little while longer before we assert the world.
+ yield new Promise(resolve => gMockWindow.setTimeout(resolve, 10));
+ FinderIterator.stop();
+
+ Assert.ok(!FinderIterator.running, "Stop means stop");
+
+ yield whenDone;
+ yield whenDone2;
+
+ Assert.greater(count, FinderIterator.kIterationSizeMax, "At least one range should've been found");
+ Assert.less(count, rangeCount, "Not all ranges should've been found");
+ Assert.greater(count2, FinderIterator.kIterationSizeMax, "At least one range should've been found");
+ Assert.less(count2, rangeCount, "Not all ranges should've been found");
+
+ Assert.equal(count2, count, "The second start was later, but should have caught up");
+
+ FinderIterator.reset();
+});
+
+add_task(function* test_allowDistance() {
+ let findText = "gup";
+ let rangeCount = 20;
+ prepareIterator(findText, rangeCount);
+
+ // Start off the iterator.
+ let count = 0;
+ let whenDone = FinderIterator.start({
+ caseSensitive: false,
+ entireWord: false,
+ finder: gMockFinder,
+ listener: { onIteratorRangeFound(range) { ++count; } },
+ word: findText
+ });
+
+ let count2 = 0;
+ let whenDone2 = FinderIterator.start({
+ caseSensitive: false,
+ entireWord: false,
+ finder: gMockFinder,
+ listener: { onIteratorRangeFound(range) { ++count2; } },
+ word: "gu"
+ });
+
+ let count3 = 0;
+ let whenDone3 = FinderIterator.start({
+ allowDistance: 1,
+ caseSensitive: false,
+ entireWord: false,
+ finder: gMockFinder,
+ listener: { onIteratorRangeFound(range) { ++count3; } },
+ word: "gu"
+ });
+
+ yield Promise.all([whenDone, whenDone2, whenDone3]);
+
+ Assert.equal(count, rangeCount, "The first iterator invocation should yield all results");
+ Assert.equal(count2, 0, "The second iterator invocation should yield _no_ results");
+ Assert.equal(count3, rangeCount, "The first iterator invocation should yield all results");
+
+ FinderIterator.reset();
+});
diff --git a/toolkit/modules/tests/xpcshell/test_GMPInstallManager.js b/toolkit/modules/tests/xpcshell/test_GMPInstallManager.js
new file mode 100644
index 000000000..74d5ad43d
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_GMPInstallManager.js
@@ -0,0 +1,794 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} = Components;
+const URL_HOST = "http://localhost";
+
+var GMPScope = Cu.import("resource://gre/modules/GMPInstallManager.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm")
+Cu.import("resource://gre/modules/UpdateUtils.jsm");
+
+var { computeHash } = Cu.import("resource://gre/modules/addons/ProductAddonChecker.jsm");
+var ProductAddonCheckerScope = Cu.import("resource://gre/modules/addons/ProductAddonChecker.jsm");
+
+do_get_profile();
+
+function run_test() { Cu.import("resource://gre/modules/Preferences.jsm")
+ Preferences.set("media.gmp.log.dump", true);
+ Preferences.set("media.gmp.log.level", 0);
+ run_next_test();
+}
+
+/**
+ * Tests that the helper used for preferences works correctly
+ */
+add_task(function* test_prefs() {
+ let addon1 = "addon1", addon2 = "addon2";
+
+ GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_URL, "http://not-really-used");
+ GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_URL_OVERRIDE, "http://not-really-used-2");
+ GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "1", addon1);
+ GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, "2", addon1);
+ GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "3", addon2);
+ GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, "4", addon2);
+ GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, false, addon2);
+ GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_CERT_CHECKATTRS, true);
+
+ do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_URL), "http://not-really-used");
+ do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_URL_OVERRIDE),
+ "http://not-really-used-2");
+ do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "", addon1), "1");
+ do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, "", addon1), "2");
+ do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "", addon2), "3");
+ do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, "", addon2), "4");
+ do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, undefined, addon2),
+ false);
+ do_check_true(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_CERT_CHECKATTRS));
+ GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, addon2);
+});
+
+/**
+ * Tests that an uninit without a check works fine
+ */
+add_task(function* test_checkForAddons_uninitWithoutCheck() {
+ let installManager = new GMPInstallManager();
+ installManager.uninit();
+});
+
+/**
+ * Tests that an uninit without an install works fine
+ */
+add_test(function test_checkForAddons_uninitWithoutInstall() {
+ overrideXHR(200, "");
+ let installManager = new GMPInstallManager();
+ let promise = installManager.checkForAddons();
+ promise.then(res => {
+ do_check_true(res.usedFallback);
+ installManager.uninit();
+ run_next_test();
+ });
+});
+
+/**
+ * Tests that no response returned rejects
+ */
+add_test(function test_checkForAddons_noResponse() {
+ overrideXHR(200, "");
+ let installManager = new GMPInstallManager();
+ let promise = installManager.checkForAddons();
+ promise.then(res => {
+ do_check_true(res.usedFallback);
+ installManager.uninit();
+ run_next_test();
+ });
+});
+
+/**
+ * Tests that no addons element returned resolves with no addons
+ */
+add_task(function* test_checkForAddons_noAddonsElement() {
+ overrideXHR(200, "<updates></updates>");
+ let installManager = new GMPInstallManager();
+ let res = yield installManager.checkForAddons();
+ do_check_eq(res.gmpAddons.length, 0);
+ installManager.uninit();
+});
+
+/**
+ * Tests that empty addons element returned resolves with no addons
+ */
+add_task(function* test_checkForAddons_emptyAddonsElement() {
+ overrideXHR(200, "<updates><addons/></updates>");
+ let installManager = new GMPInstallManager();
+ let res = yield installManager.checkForAddons();
+ do_check_eq(res.gmpAddons.length, 0);
+ installManager.uninit();
+});
+
+/**
+ * Tests that a response with the wrong root element rejects
+ */
+add_test(function test_checkForAddons_wrongResponseXML() {
+ overrideXHR(200, "<digits_of_pi>3.141592653589793....</digits_of_pi>");
+ let installManager = new GMPInstallManager();
+ let promise = installManager.checkForAddons();
+ promise.then(res => {
+ do_check_true(res.usedFallback);
+ installManager.uninit();
+ run_next_test();
+ });
+});
+
+/**
+ * Tests that a 404 error works as expected
+ */
+add_test(function test_checkForAddons_404Error() {
+ overrideXHR(404, "");
+ let installManager = new GMPInstallManager();
+ let promise = installManager.checkForAddons();
+ promise.then(res => {
+ do_check_true(res.usedFallback);
+ installManager.uninit();
+ run_next_test();
+ });
+});
+
+/**
+ * Tests that a xhr abort() works as expected
+ */
+add_test(function test_checkForAddons_abort() {
+ let overriddenXhr = overrideXHR(200, "", { dropRequest: true} );
+ let installManager = new GMPInstallManager();
+ let promise = installManager.checkForAddons();
+ overriddenXhr.abort();
+ promise.then(res => {
+ do_check_true(res.usedFallback);
+ installManager.uninit();
+ run_next_test();
+ });
+});
+
+/**
+ * Tests that a defensive timeout works as expected
+ */
+add_test(function test_checkForAddons_timeout() {
+ overrideXHR(200, "", { dropRequest: true, timeout: true });
+ let installManager = new GMPInstallManager();
+ let promise = installManager.checkForAddons();
+ promise.then(res => {
+ do_check_true(res.usedFallback);
+ installManager.uninit();
+ run_next_test();
+ });
+});
+
+/**
+ * Tests that we throw correctly in case of ssl certification error.
+ */
+add_test(function test_checkForAddons_bad_ssl() {
+ //
+ // Add random stuff that cause CertUtil to require https.
+ //
+ let PREF_KEY_URL_OVERRIDE_BACKUP =
+ Preferences.get(GMPScope.GMPPrefs.KEY_URL_OVERRIDE, undefined);
+ Preferences.reset(GMPScope.GMPPrefs.KEY_URL_OVERRIDE);
+
+ let CERTS_BRANCH_DOT_ONE = GMPScope.GMPPrefs.KEY_CERTS_BRANCH + ".1";
+ let PREF_CERTS_BRANCH_DOT_ONE_BACKUP =
+ Preferences.get(CERTS_BRANCH_DOT_ONE, undefined);
+ Services.prefs.setCharPref(CERTS_BRANCH_DOT_ONE, "funky value");
+
+
+ overrideXHR(200, "");
+ let installManager = new GMPInstallManager();
+ let promise = installManager.checkForAddons();
+ promise.then(res => {
+ do_check_true(res.usedFallback);
+ installManager.uninit();
+ if (PREF_KEY_URL_OVERRIDE_BACKUP) {
+ Preferences.set(GMPScope.GMPPrefs.KEY_URL_OVERRIDE,
+ PREF_KEY_URL_OVERRIDE_BACKUP);
+ }
+ if (PREF_CERTS_BRANCH_DOT_ONE_BACKUP) {
+ Preferences.set(CERTS_BRANCH_DOT_ONE,
+ PREF_CERTS_BRANCH_DOT_ONE_BACKUP);
+ }
+ run_next_test();
+ });
+});
+
+/**
+ * Tests that gettinga a funky non XML response works as expected
+ */
+add_test(function test_checkForAddons_notXML() {
+ overrideXHR(200, "3.141592653589793....");
+ let installManager = new GMPInstallManager();
+ let promise = installManager.checkForAddons();
+
+ promise.then(res => {
+ do_check_true(res.usedFallback);
+ installManager.uninit();
+ run_next_test();
+ });
+});
+
+/**
+ * Tests that getting a response with a single addon works as expected
+ */
+add_task(function* test_checkForAddons_singleAddon() {
+ let responseXML =
+ "<?xml version=\"1.0\"?>" +
+ "<updates>" +
+ " <addons>" +
+ " <addon id=\"gmp-gmpopenh264\"" +
+ " URL=\"http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip\"" +
+ " hashFunction=\"sha256\"" +
+ " hashValue=\"1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+ " version=\"1.1\"/>" +
+ " </addons>" +
+ "</updates>"
+ overrideXHR(200, responseXML);
+ let installManager = new GMPInstallManager();
+ let res = yield installManager.checkForAddons();
+ do_check_eq(res.gmpAddons.length, 1);
+ let gmpAddon = res.gmpAddons[0];
+ do_check_eq(gmpAddon.id, "gmp-gmpopenh264");
+ do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip");
+ do_check_eq(gmpAddon.hashFunction, "sha256");
+ do_check_eq(gmpAddon.hashValue, "1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee");
+ do_check_eq(gmpAddon.version, "1.1");
+ do_check_eq(gmpAddon.size, undefined);
+ do_check_true(gmpAddon.isValid);
+ do_check_false(gmpAddon.isInstalled);
+ installManager.uninit();
+});
+
+/**
+ * Tests that getting a response with a single addon with the optional size
+ * attribute parses as expected.
+ */
+add_task(function* test_checkForAddons_singleAddonWithSize() {
+ let responseXML =
+ "<?xml version=\"1.0\"?>" +
+ "<updates>" +
+ " <addons>" +
+ " <addon id=\"openh264-plugin-no-at-symbol\"" +
+ " URL=\"http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip\"" +
+ " hashFunction=\"sha256\"" +
+ " size=\"42\"" +
+ " hashValue=\"1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+ " version=\"1.1\"/>" +
+ " </addons>" +
+ "</updates>"
+ overrideXHR(200, responseXML);
+ let installManager = new GMPInstallManager();
+ let res = yield installManager.checkForAddons();
+ do_check_eq(res.gmpAddons.length, 1);
+ let gmpAddon = res.gmpAddons[0];
+ do_check_eq(gmpAddon.id, "openh264-plugin-no-at-symbol");
+ do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip");
+ do_check_eq(gmpAddon.hashFunction, "sha256");
+ do_check_eq(gmpAddon.hashValue, "1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee");
+ do_check_eq(gmpAddon.size, 42);
+ do_check_eq(gmpAddon.version, "1.1");
+ do_check_true(gmpAddon.isValid);
+ do_check_false(gmpAddon.isInstalled);
+ installManager.uninit();
+});
+
+/**
+ * Tests that checking for multiple addons work correctly.
+ * Also tests that invalid addons work correctly.
+ */
+add_task(function* test_checkForAddons_multipleAddonNoUpdatesSomeInvalid() {
+ let responseXML =
+ "<?xml version=\"1.0\"?>" +
+ "<updates>" +
+ " <addons>" +
+ // valid openh264
+ " <addon id=\"gmp-gmpopenh264\"" +
+ " URL=\"http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip\"" +
+ " hashFunction=\"sha256\"" +
+ " hashValue=\"1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+ " version=\"1.1\"/>" +
+ // valid not openh264
+ " <addon id=\"NOT-gmp-gmpopenh264\"" +
+ " URL=\"http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip\"" +
+ " hashFunction=\"sha512\"" +
+ " hashValue=\"141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+ " version=\"9.1\"/>" +
+ // noid
+ " <addon notid=\"NOT-gmp-gmpopenh264\"" +
+ " URL=\"http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip\"" +
+ " hashFunction=\"sha512\"" +
+ " hashValue=\"141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+ " version=\"9.1\"/>" +
+ // no URL
+ " <addon id=\"NOT-gmp-gmpopenh264\"" +
+ " notURL=\"http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip\"" +
+ " hashFunction=\"sha512\"" +
+ " hashValue=\"141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+ " version=\"9.1\"/>" +
+ // no hash function
+ " <addon id=\"NOT-gmp-gmpopenh264\"" +
+ " URL=\"http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip\"" +
+ " nothashFunction=\"sha512\"" +
+ " hashValue=\"141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+ " version=\"9.1\"/>" +
+ // no hash function
+ " <addon id=\"NOT-gmp-gmpopenh264\"" +
+ " URL=\"http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip\"" +
+ " hashFunction=\"sha512\"" +
+ " nothashValue=\"141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+ " version=\"9.1\"/>" +
+ // not version
+ " <addon id=\"NOT-gmp-gmpopenh264\"" +
+ " URL=\"http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip\"" +
+ " hashFunction=\"sha512\"" +
+ " hashValue=\"141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+ " notversion=\"9.1\"/>" +
+ " </addons>" +
+ "</updates>"
+ overrideXHR(200, responseXML);
+ let installManager = new GMPInstallManager();
+ let res = yield installManager.checkForAddons();
+ do_check_eq(res.gmpAddons.length, 7);
+ let gmpAddon = res.gmpAddons[0];
+ do_check_eq(gmpAddon.id, "gmp-gmpopenh264");
+ do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip");
+ do_check_eq(gmpAddon.hashFunction, "sha256");
+ do_check_eq(gmpAddon.hashValue, "1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee");
+ do_check_eq(gmpAddon.version, "1.1");
+ do_check_true(gmpAddon.isValid);
+ do_check_false(gmpAddon.isInstalled);
+
+ gmpAddon = res.gmpAddons[1];
+ do_check_eq(gmpAddon.id, "NOT-gmp-gmpopenh264");
+ do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip");
+ do_check_eq(gmpAddon.hashFunction, "sha512");
+ do_check_eq(gmpAddon.hashValue, "141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee");
+ do_check_eq(gmpAddon.version, "9.1");
+ do_check_true(gmpAddon.isValid);
+ do_check_false(gmpAddon.isInstalled);
+
+ for (let i = 2; i < res.gmpAddons.length; i++) {
+ do_check_false(res.gmpAddons[i].isValid);
+ do_check_false(res.gmpAddons[i].isInstalled);
+ }
+ installManager.uninit();
+});
+
+/**
+ * Tests that checking for addons when there are also updates available
+ * works as expected.
+ */
+add_task(function* test_checkForAddons_updatesWithAddons() {
+ let responseXML =
+ "<?xml version=\"1.0\"?>" +
+ " <updates>" +
+ " <update type=\"minor\" displayVersion=\"33.0a1\" appVersion=\"33.0a1\" platformVersion=\"33.0a1\" buildID=\"20140628030201\">" +
+ " <patch type=\"complete\" URL=\"http://ftp.mozilla.org/pub/mozilla.org/firefox/nightly/2014/06/2014-06-28-03-02-01-mozilla-central/firefox-33.0a1.en-US.mac.complete.mar\" hashFunction=\"sha512\" hashValue=\"f3f90d71dff03ae81def80e64bba3e4569da99c9e15269f731c2b167c4fc30b3aed9f5fee81c19614120230ca333e73a5e7def1b8e45d03135b2069c26736219\" size=\"85249896\"/>" +
+ " </update>" +
+ " <addons>" +
+ " <addon id=\"gmp-gmpopenh264\"" +
+ " URL=\"http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip\"" +
+ " hashFunction=\"sha256\"" +
+ " hashValue=\"1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+ " version=\"1.1\"/>" +
+ " </addons>" +
+ "</updates>"
+ overrideXHR(200, responseXML);
+ let installManager = new GMPInstallManager();
+ let res = yield installManager.checkForAddons();
+ do_check_eq(res.gmpAddons.length, 1);
+ let gmpAddon = res.gmpAddons[0];
+ do_check_eq(gmpAddon.id, "gmp-gmpopenh264");
+ do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip");
+ do_check_eq(gmpAddon.hashFunction, "sha256");
+ do_check_eq(gmpAddon.hashValue, "1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee");
+ do_check_eq(gmpAddon.version, "1.1");
+ do_check_true(gmpAddon.isValid);
+ do_check_false(gmpAddon.isInstalled);
+ installManager.uninit();
+});
+
+/**
+ * Tests that installing found addons works as expected
+ */
+function* test_checkForAddons_installAddon(id, includeSize, wantInstallReject) {
+ do_print("Running installAddon for id: " + id +
+ ", includeSize: " + includeSize +
+ " and wantInstallReject: " + wantInstallReject);
+ let httpServer = new HttpServer();
+ let dir = FileUtils.getDir("TmpD", [], true);
+ httpServer.registerDirectory("/", dir);
+ httpServer.start(-1);
+ let testserverPort = httpServer.identity.primaryPort;
+ let zipFileName = "test_" + id + "_GMP.zip";
+
+ let zipURL = URL_HOST + ":" + testserverPort + "/" + zipFileName;
+ do_print("zipURL: " + zipURL);
+
+ let data = "e~=0.5772156649";
+ let zipFile = createNewZipFile(zipFileName, data);
+ let hashFunc = "sha256";
+ let expectedDigest = yield computeHash(hashFunc, zipFile.path);
+ let fileSize = zipFile.fileSize;
+ if (wantInstallReject) {
+ fileSize = 1;
+ }
+
+ let responseXML =
+ "<?xml version=\"1.0\"?>" +
+ "<updates>" +
+ " <addons>" +
+ " <addon id=\"" + id + "-gmp-gmpopenh264\"" +
+ " URL=\"" + zipURL + "\"" +
+ " hashFunction=\"" + hashFunc + "\"" +
+ " hashValue=\"" + expectedDigest + "\"" +
+ (includeSize ? " size=\"" + fileSize + "\"" : "") +
+ " version=\"1.1\"/>" +
+ " </addons>" +
+ "</updates>"
+
+ overrideXHR(200, responseXML);
+ let installManager = new GMPInstallManager();
+ let res = yield installManager.checkForAddons();
+ do_check_eq(res.gmpAddons.length, 1);
+ let gmpAddon = res.gmpAddons[0];
+ do_check_false(gmpAddon.isInstalled);
+
+ try {
+ let extractedPaths = yield installManager.installAddon(gmpAddon);
+ if (wantInstallReject) {
+ do_check_true(false); // installAddon() should have thrown.
+ }
+ do_check_eq(extractedPaths.length, 1);
+ let extractedPath = extractedPaths[0];
+
+ do_print("Extracted path: " + extractedPath);
+
+ let extractedFile = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsIFile);
+ extractedFile.initWithPath(extractedPath);
+ do_check_true(extractedFile.exists());
+ let readData = readStringFromFile(extractedFile);
+ do_check_eq(readData, data);
+
+ // Make sure the prefs are set correctly
+ do_check_true(!!GMPScope.GMPPrefs.get(
+ GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "", gmpAddon.id));
+ do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, "",
+ gmpAddon.id),
+ "1.1");
+ do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_ABI, "",
+ gmpAddon.id),
+ UpdateUtils.ABI);
+ // Make sure it reports as being installed
+ do_check_true(gmpAddon.isInstalled);
+
+ // Cleanup
+ extractedFile.parent.remove(true);
+ zipFile.remove(false);
+ httpServer.stop(function() {});
+ installManager.uninit();
+ } catch (ex) {
+ zipFile.remove(false);
+ if (!wantInstallReject) {
+ do_throw("install update should not reject " + ex.message);
+ }
+ }
+}
+
+add_task(test_checkForAddons_installAddon.bind(null, "1", true, false));
+add_task(test_checkForAddons_installAddon.bind(null, "2", false, false));
+add_task(test_checkForAddons_installAddon.bind(null, "3", true, true));
+
+/**
+ * Tests simpleCheckAndInstall when autoupdate is disabled for a GMP
+ */
+add_task(function* test_simpleCheckAndInstall_autoUpdateDisabled() {
+ GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, false, GMPScope.OPEN_H264_ID);
+ let responseXML =
+ "<?xml version=\"1.0\"?>" +
+ "<updates>" +
+ " <addons>" +
+ // valid openh264
+ " <addon id=\"gmp-gmpopenh264\"" +
+ " URL=\"http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip\"" +
+ " hashFunction=\"sha256\"" +
+ " hashValue=\"1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee\"" +
+ " version=\"1.1\"/>" +
+ " </addons>" +
+ "</updates>"
+
+ overrideXHR(200, responseXML);
+ let installManager = new GMPInstallManager();
+ let result = yield installManager.simpleCheckAndInstall();
+ do_check_eq(result.status, "nothing-new-to-install");
+ Preferences.reset(GMPScope.GMPPrefs.KEY_UPDATE_LAST_CHECK);
+ GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, GMPScope.OPEN_H264_ID);
+});
+
+/**
+ * Tests simpleCheckAndInstall nothing to install
+ */
+add_task(function* test_simpleCheckAndInstall_nothingToInstall() {
+ let responseXML =
+ "<?xml version=\"1.0\"?>" +
+ "<updates>" +
+ "</updates>"
+
+ overrideXHR(200, responseXML);
+ let installManager = new GMPInstallManager();
+ let result = yield installManager.simpleCheckAndInstall();
+ do_check_eq(result.status, "nothing-new-to-install");
+});
+
+/**
+ * Tests simpleCheckAndInstall too frequent
+ */
+add_task(function* test_simpleCheckAndInstall_tooFrequent() {
+ let responseXML =
+ "<?xml version=\"1.0\"?>" +
+ "<updates>" +
+ "</updates>"
+
+ overrideXHR(200, responseXML);
+ let installManager = new GMPInstallManager();
+ let result = yield installManager.simpleCheckAndInstall();
+ do_check_eq(result.status, "too-frequent-no-check");
+});
+
+/**
+ * Tests that installing addons when there is no server works as expected
+ */
+add_test(function test_installAddon_noServer() {
+ let dir = FileUtils.getDir("TmpD", [], true);
+ let zipFileName = "test_GMP.zip";
+ let zipURL = URL_HOST + ":0/" + zipFileName;
+
+ let data = "e~=0.5772156649";
+ let zipFile = createNewZipFile(zipFileName, data);
+
+ let responseXML =
+ "<?xml version=\"1.0\"?>" +
+ "<updates>" +
+ " <addons>" +
+ " <addon id=\"gmp-gmpopenh264\"" +
+ " URL=\"" + zipURL + "\"" +
+ " hashFunction=\"sha256\"" +
+ " hashValue=\"11221cbda000347b054028b527a60e578f919cb10f322ef8077d3491c6fcb474\"" +
+ " version=\"1.1\"/>" +
+ " </addons>" +
+ "</updates>"
+
+ overrideXHR(200, responseXML);
+ let installManager = new GMPInstallManager();
+ let checkPromise = installManager.checkForAddons();
+ checkPromise.then(res => {
+ do_check_eq(res.gmpAddons.length, 1);
+ let gmpAddon = res.gmpAddons[0];
+
+ GMPInstallManager.overrideLeaveDownloadedZip = true;
+ let installPromise = installManager.installAddon(gmpAddon);
+ installPromise.then(extractedPaths => {
+ do_throw("No server for install should reject");
+ }, err => {
+ do_check_true(!!err);
+ installManager.uninit();
+ run_next_test();
+ });
+ }, () => {
+ do_throw("check should not reject for install no server");
+ });
+});
+
+/**
+ * Returns the read stream into a string
+ */
+function readStringFromInputStream(inputStream) {
+ let sis = Cc["@mozilla.org/scriptableinputstream;1"].
+ createInstance(Ci.nsIScriptableInputStream);
+ sis.init(inputStream);
+ let text = sis.read(sis.available());
+ sis.close();
+ return text;
+}
+
+/**
+ * Reads a string of text from a file.
+ * This function only works with ASCII text.
+ */
+function readStringFromFile(file) {
+ if (!file.exists()) {
+ do_print("readStringFromFile - file doesn't exist: " + file.path);
+ return null;
+ }
+ let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ fis.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+ return readStringFromInputStream(fis);
+}
+
+/**
+ * Bare bones XMLHttpRequest implementation for testing onprogress, onerror,
+ * and onload nsIDomEventListener handleEvent.
+ */
+function makeHandler(aVal) {
+ if (typeof aVal == "function")
+ return { handleEvent: aVal };
+ return aVal;
+}
+/**
+ * Constructs a mock xhr which is used for testing different aspects
+ * of responses.
+ */
+function xhr(inputStatus, inputResponse, options) {
+ this.inputStatus = inputStatus;
+ this.inputResponse = inputResponse;
+ this.status = 0;
+ this.responseXML = null;
+ this._aborted = false;
+ this._onabort = null;
+ this._onprogress = null;
+ this._onerror = null;
+ this._onload = null;
+ this._onloadend = null;
+ this._ontimeout = null;
+ this._url = null;
+ this._method = null;
+ this._timeout = 0;
+ this._notified = false;
+ this._options = options || {};
+}
+xhr.prototype = {
+ overrideMimeType: function(aMimetype) { },
+ setRequestHeader: function(aHeader, aValue) { },
+ status: null,
+ channel: { set notificationCallbacks(aVal) { } },
+ open: function(aMethod, aUrl) {
+ this.channel.originalURI = Services.io.newURI(aUrl, null, null);
+ this._method = aMethod; this._url = aUrl;
+ },
+ abort: function() {
+ this._dropRequest = true;
+ this._notify(["abort", "loadend"]);
+ },
+ responseXML: null,
+ responseText: null,
+ send: function(aBody) {
+ do_execute_soon(function() {
+ try {
+ if (this._options.dropRequest) {
+ if (this._timeout > 0 && this._options.timeout) {
+ this._notify(["timeout", "loadend"]);
+ }
+ return;
+ }
+ this.status = this.inputStatus;
+ this.responseText = this.inputResponse;
+ try {
+ let parser = Cc["@mozilla.org/xmlextras/domparser;1"].
+ createInstance(Ci.nsIDOMParser);
+ this.responseXML = parser.parseFromString(this.inputResponse,
+ "application/xml");
+ } catch (e) {
+ this.responseXML = null;
+ }
+ if (this.inputStatus === 200) {
+ this._notify(["load", "loadend"]);
+ } else {
+ this._notify(["error", "loadend"]);
+ }
+ } catch (ex) {
+ do_throw(ex);
+ }
+ }.bind(this));
+ },
+ set onabort(aValue) { this._onabort = makeHandler(aValue); },
+ get onabort() { return this._onabort; },
+ set onprogress(aValue) { this._onprogress = makeHandler(aValue); },
+ get onprogress() { return this._onprogress; },
+ set onerror(aValue) { this._onerror = makeHandler(aValue); },
+ get onerror() { return this._onerror; },
+ set onload(aValue) { this._onload = makeHandler(aValue); },
+ get onload() { return this._onload; },
+ set onloadend(aValue) { this._onloadend = makeHandler(aValue); },
+ get onloadend() { return this._onloadend; },
+ set ontimeout(aValue) { this._ontimeout = makeHandler(aValue); },
+ get ontimeout() { return this._ontimeout; },
+ set timeout(aValue) { this._timeout = aValue; },
+ _notify: function(events) {
+ if (this._notified) {
+ return;
+ }
+ this._notified = true;
+ for (let item of events) {
+ let k = "on" + item;
+ if (this[k]) {
+ do_print("Notifying " + item);
+ let e = {
+ target: this,
+ type: item,
+ };
+ this[k](e);
+ } else {
+ do_print("Notifying " + item + ", but there are no listeners");
+ }
+ }
+ },
+ addEventListener: function(aEvent, aValue, aCapturing) {
+ eval("this._on" + aEvent + " = aValue");
+ },
+ flags: Ci.nsIClassInfo.SINGLETON,
+ getScriptableHelper: () => null,
+ getInterfaces: function(aCount) {
+ let interfaces = [Ci.nsISupports];
+ aCount.value = interfaces.length;
+ return interfaces;
+ },
+ classDescription: "XMLHttpRequest",
+ contractID: "@mozilla.org/xmlextras/xmlhttprequest;1",
+ classID: Components.ID("{c9b37f43-4278-4304-a5e0-600991ab08cb}"),
+ createInstance: function(aOuter, aIID) {
+ if (aOuter == null)
+ return this.QueryInterface(aIID);
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ },
+ QueryInterface: function(aIID) {
+ if (aIID.equals(Ci.nsIClassInfo) ||
+ aIID.equals(Ci.nsISupports))
+ return this;
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+ get wrappedJSObject() { return this; }
+};
+
+/**
+ * Helper used to overrideXHR requests (no matter to what URL) with the
+ * specified status and response.
+ * @param status The status you want to get back when an XHR request is made
+ * @param response The response you want to get back when an XHR request is made
+ */
+function overrideXHR(status, response, options) {
+ overrideXHR.myxhr = new xhr(status, response, options);
+ ProductAddonCheckerScope.CreateXHR = function() {
+ return overrideXHR.myxhr;
+ };
+ return overrideXHR.myxhr;
+}
+
+/**
+ * Creates a new zip file containing a file with the specified data
+ * @param zipName The name of the zip file
+ * @param data The data to go inside the zip for the filename entry1.info
+ */
+function createNewZipFile(zipName, data) {
+ // Create a zip file which will be used for extracting
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"].
+ createInstance(Ci.nsIStringInputStream);
+ stream.setData(data, data.length);
+ let zipWriter = Cc["@mozilla.org/zipwriter;1"].
+ createInstance(Components.interfaces.nsIZipWriter);
+ let zipFile = FileUtils.getFile("TmpD", [zipName]);
+ if (zipFile.exists()) {
+ zipFile.remove(false);
+ }
+ // From prio.h
+ const PR_RDWR = 0x04;
+ const PR_CREATE_FILE = 0x08;
+ const PR_TRUNCATE = 0x20;
+ zipWriter.open(zipFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
+ zipWriter.addEntryStream("entry1.info", Date.now(),
+ Ci.nsIZipWriter.COMPRESSION_BEST, stream, false);
+ zipWriter.close();
+ stream.close();
+ do_print("zip file created on disk at: " + zipFile.path);
+ return zipFile;
+}
diff --git a/toolkit/modules/tests/xpcshell/test_Http.js b/toolkit/modules/tests/xpcshell/test_Http.js
new file mode 100644
index 000000000..3dfd769b7
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_Http.js
@@ -0,0 +1,257 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/Http.jsm");
+Components.utils.import("resource://testing-common/httpd.js");
+
+const BinaryInputStream = Components.Constructor("@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream", "setInputStream");
+
+var server;
+
+const kDefaultServerPort = 9000;
+const kSuccessPath = "/success";
+const kBaseUrl = "http://localhost:" + kDefaultServerPort;
+const kSuccessUrl = kBaseUrl + kSuccessPath;
+
+const kPostPath = "/post";
+const kPostUrl = kBaseUrl + kPostPath;
+const kPostDataSent = [["foo", "bar"], ["complex", "!*()@"]];
+const kPostDataReceived = "foo=bar&complex=%21%2A%28%29%40";
+const kPostMimeTypeReceived = "application/x-www-form-urlencoded; charset=utf-8";
+
+const kJsonPostPath = "/json_post";
+const kJsonPostUrl = kBaseUrl + kJsonPostPath;
+const kJsonPostData = JSON.stringify(kPostDataSent);
+const kJsonPostMimeType = "application/json";
+
+const kPutPath = "/put";
+const kPutUrl = kBaseUrl + kPutPath;
+const kPutDataSent = [["P", "NP"]];
+const kPutDataReceived = "P=NP";
+
+const kGetPath = "/get";
+const kGetUrl = kBaseUrl + kGetPath;
+
+function successResult(aRequest, aResponse) {
+ aResponse.setStatusLine(null, 200, "OK");
+ aResponse.setHeader("Content-Type", "application/json");
+ aResponse.write("Success!");
+}
+
+function getDataChecker(aExpectedMethod, aExpectedData, aExpectedMimeType = null) {
+ return function(aRequest, aResponse) {
+ let body = new BinaryInputStream(aRequest.bodyInputStream);
+ let bytes = [];
+ let avail;
+ while ((avail = body.available()) > 0)
+ Array.prototype.push.apply(bytes, body.readByteArray(avail));
+
+ do_check_eq(aRequest.method, aExpectedMethod);
+
+ // Checking if the Content-Type is as expected.
+ if (aExpectedMimeType) {
+ let contentType = aRequest.getHeader("Content-Type");
+ do_check_eq(contentType, aExpectedMimeType);
+ }
+
+ var data = String.fromCharCode.apply(null, bytes);
+
+ do_check_eq(data, aExpectedData);
+
+ aResponse.setStatusLine(null, 200, "OK");
+ aResponse.setHeader("Content-Type", "application/json");
+ aResponse.write("Success!");
+ }
+}
+
+add_test(function test_successCallback() {
+ do_test_pending();
+ let options = {
+ onLoad: function(aResponse) {
+ do_check_eq(aResponse, "Success!");
+ do_test_finished();
+ run_next_test();
+ },
+ onError: function(e) {
+ do_check_true(false);
+ do_test_finished();
+ run_next_test();
+ }
+ }
+ httpRequest(kSuccessUrl, options);
+});
+
+add_test(function test_errorCallback() {
+ do_test_pending();
+ let options = {
+ onSuccess: function(aResponse) {
+ do_check_true(false);
+ do_test_finished();
+ run_next_test();
+ },
+ onError: function(e, aResponse) {
+ do_check_eq(e, "404 - Not Found");
+ do_test_finished();
+ run_next_test();
+ }
+ }
+ httpRequest(kBaseUrl + "/failure", options);
+});
+
+add_test(function test_PostData() {
+ do_test_pending();
+ let options = {
+ onLoad: function(aResponse) {
+ do_check_eq(aResponse, "Success!");
+ do_test_finished();
+ run_next_test();
+ },
+ onError: function(e) {
+ do_check_true(false);
+ do_test_finished();
+ run_next_test();
+ },
+ postData: kPostDataSent
+ }
+ httpRequest(kPostUrl, options);
+});
+
+add_test(function test_PutData() {
+ do_test_pending();
+ let options = {
+ method: "PUT",
+ onLoad: function(aResponse) {
+ do_check_eq(aResponse, "Success!");
+ do_test_finished();
+ run_next_test();
+ },
+ onError: function(e) {
+ do_check_true(false);
+ do_test_finished();
+ run_next_test();
+ },
+ postData: kPutDataSent
+ }
+ httpRequest(kPutUrl, options);
+});
+
+add_test(function test_GetData() {
+ do_test_pending();
+ let options = {
+ onLoad: function(aResponse) {
+ do_check_eq(aResponse, "Success!");
+ do_test_finished();
+ run_next_test();
+ },
+ onError: function(e) {
+ do_check_true(false);
+ do_test_finished();
+ run_next_test();
+ },
+ postData: null
+ }
+ httpRequest(kGetUrl, options);
+});
+
+add_test(function test_OptionalParameters() {
+ let options = {
+ onLoad: null,
+ onError: null,
+ logger: null
+ };
+ // Just make sure that nothing throws when doing this (i.e. httpRequest
+ // doesn't try to access null options).
+ httpRequest(kGetUrl, options);
+ run_next_test();
+});
+
+/**
+ * Makes sure that httpRequest API allows setting a custom Content-Type header
+ * for POST requests when data is a string.
+ */
+add_test(function test_CustomContentTypeOnPost() {
+ do_test_pending();
+
+ // Preparing the request parameters.
+ let options = {
+ onLoad: function(aResponse) {
+ do_check_eq(aResponse, "Success!");
+ do_test_finished();
+ run_next_test();
+ },
+ onError: function(e) {
+ do_check_true(false);
+ do_test_finished();
+ run_next_test();
+ },
+ postData: kJsonPostData,
+ // Setting a custom Content-Type header.
+ headers: [['Content-Type', "application/json"]]
+ }
+
+ // Firing the request.
+ httpRequest(kJsonPostUrl, options);
+});
+
+/**
+ * Ensures that the httpRequest API provides a way to override the response
+ * MIME type.
+ */
+add_test(function test_OverrideMimeType() {
+ do_test_pending();
+
+ // Preparing the request parameters.
+ const kMimeType = 'text/xml; charset=UTF-8';
+ let options = {
+ onLoad: function(aResponse, xhr) {
+ do_check_eq(aResponse, "Success!");
+
+ // Set the expected MIME-type.
+ let reportedMimeType = xhr.getResponseHeader("Content-Type");
+ do_check_neq(reportedMimeType, kMimeType);
+
+ // responseXML should not be not null if overriding mime type succeeded.
+ do_check_true(xhr.responseXML != null);
+
+ do_test_finished();
+ run_next_test();
+ },
+ onError: function(e) {
+ do_check_true(false);
+ do_test_finished();
+ run_next_test();
+ }
+ };
+
+ // Firing the request.
+ let xhr = httpRequest(kGetUrl, options);
+
+ // Override the response MIME type.
+ xhr.overrideMimeType(kMimeType);
+});
+
+function run_test() {
+ // Set up a mock HTTP server to serve a success page.
+ server = new HttpServer();
+ server.registerPathHandler(kSuccessPath, successResult);
+ server.registerPathHandler(kPostPath,
+ getDataChecker("POST", kPostDataReceived,
+ kPostMimeTypeReceived));
+ server.registerPathHandler(kPutPath,
+ getDataChecker("PUT", kPutDataReceived));
+ server.registerPathHandler(kGetPath, getDataChecker("GET", ""));
+ server.registerPathHandler(kJsonPostPath,
+ getDataChecker("POST", kJsonPostData,
+ kJsonPostMimeType));
+
+ server.start(kDefaultServerPort);
+
+ run_next_test();
+
+ // Teardown.
+ do_register_cleanup(function() {
+ server.stop(function() { });
+ });
+}
+
diff --git a/toolkit/modules/tests/xpcshell/test_Integration.js b/toolkit/modules/tests/xpcshell/test_Integration.js
new file mode 100644
index 000000000..808e2d34f
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_Integration.js
@@ -0,0 +1,238 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the Integration.jsm module.
+ */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Integration.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+
+const TestIntegration = {
+ value: "value",
+
+ get valueFromThis() {
+ return this.value;
+ },
+
+ get property() {
+ return this._property;
+ },
+
+ set property(value) {
+ this._property = value;
+ },
+
+ method(argument) {
+ this.methodArgument = argument;
+ return "method" + argument;
+ },
+
+ asyncMethod: Task.async(function* (argument) {
+ this.asyncMethodArgument = argument;
+ return "asyncMethod" + argument;
+ }),
+};
+
+let overrideFn = base => ({
+ value: "overridden-value",
+
+ get property() {
+ return "overridden-" + base.__lookupGetter__("property").call(this);
+ },
+
+ set property(value) {
+ base.__lookupSetter__("property").call(this, "overridden-" + value);
+ },
+
+ method() {
+ return "overridden-" + base.method.apply(this, arguments);
+ },
+
+ asyncMethod: Task.async(function* () {
+ return "overridden-" + (yield base.asyncMethod.apply(this, arguments));
+ }),
+});
+
+let superOverrideFn = base => ({
+ __proto__: base,
+
+ value: "overridden-value",
+
+ get property() {
+ return "overridden-" + super.property;
+ },
+
+ set property(value) {
+ super.property = "overridden-" + value;
+ },
+
+ method() {
+ return "overridden-" + super.method(...arguments);
+ },
+
+ asyncMethod: Task.async(function* () {
+ // We cannot use the "super" keyword in methods defined using "Task.async".
+ return "overridden-" + (yield base.asyncMethod.apply(this, arguments));
+ }),
+});
+
+/**
+ * Fails the test if the results of method invocations on the combined object
+ * don't match the expected results based on how many overrides are registered.
+ *
+ * @param combined
+ * The combined object based on the TestIntegration root.
+ * @param overridesCount
+ * Zero if the root object is not overridden, or a higher value to test
+ * the presence of one or more integration overrides.
+ */
+function* assertCombinedResults(combined, overridesCount) {
+ let expectedValue = overridesCount > 0 ? "overridden-value" : "value";
+ let prefix = "overridden-".repeat(overridesCount);
+
+ Assert.equal(combined.value, expectedValue);
+ Assert.equal(combined.valueFromThis, expectedValue);
+
+ combined.property = "property";
+ Assert.equal(combined.property, prefix.repeat(2) + "property");
+
+ combined.methodArgument = "";
+ Assert.equal(combined.method("-argument"), prefix + "method-argument");
+ Assert.equal(combined.methodArgument, "-argument");
+
+ combined.asyncMethodArgument = "";
+ Assert.equal(yield combined.asyncMethod("-argument"),
+ prefix + "asyncMethod-argument");
+ Assert.equal(combined.asyncMethodArgument, "-argument");
+}
+
+/**
+ * Fails the test if the results of method invocations on the combined object
+ * for the "testModule" integration point don't match the expected results based
+ * on how many overrides are registered.
+ *
+ * @param overridesCount
+ * Zero if the root object is not overridden, or a higher value to test
+ * the presence of one or more integration overrides.
+ */
+function* assertCurrentCombinedResults(overridesCount) {
+ let combined = Integration.testModule.getCombined(TestIntegration);
+ yield assertCombinedResults(combined, overridesCount);
+}
+
+/**
+ * Checks the initial state with no integration override functions registered.
+ */
+add_task(function* test_base() {
+ yield assertCurrentCombinedResults(0);
+});
+
+/**
+ * Registers and unregisters an integration override function.
+ */
+add_task(function* test_override() {
+ Integration.testModule.register(overrideFn);
+ yield assertCurrentCombinedResults(1);
+
+ // Registering the same function more than once has no effect.
+ Integration.testModule.register(overrideFn);
+ yield assertCurrentCombinedResults(1);
+
+ Integration.testModule.unregister(overrideFn);
+ yield assertCurrentCombinedResults(0);
+});
+
+/**
+ * Registers and unregisters more than one integration override function, of
+ * which one uses the prototype and the "super" keyword to access the base.
+ */
+add_task(function* test_override_super_multiple() {
+ Integration.testModule.register(overrideFn);
+ Integration.testModule.register(superOverrideFn);
+ yield assertCurrentCombinedResults(2);
+
+ Integration.testModule.unregister(overrideFn);
+ yield assertCurrentCombinedResults(1);
+
+ Integration.testModule.unregister(superOverrideFn);
+ yield assertCurrentCombinedResults(0);
+});
+
+/**
+ * Registers an integration override function that throws an exception, and
+ * ensures that this does not block other functions from being registered.
+ */
+add_task(function* test_override_error() {
+ let errorOverrideFn = base => { throw "Expected error." };
+
+ Integration.testModule.register(errorOverrideFn);
+ Integration.testModule.register(overrideFn);
+ yield assertCurrentCombinedResults(1);
+
+ Integration.testModule.unregister(errorOverrideFn);
+ Integration.testModule.unregister(overrideFn);
+ yield assertCurrentCombinedResults(0);
+});
+
+/**
+ * Checks that state saved using the "this" reference is preserved as a shallow
+ * copy when registering new integration override functions.
+ */
+add_task(function* test_state_preserved() {
+ let valueObject = { toString: () => "toString" };
+
+ let combined = Integration.testModule.getCombined(TestIntegration);
+ combined.property = valueObject;
+ Assert.ok(combined.property === valueObject);
+
+ Integration.testModule.register(overrideFn);
+ combined = Integration.testModule.getCombined(TestIntegration);
+ Assert.equal(combined.property, "overridden-toString");
+
+ Integration.testModule.unregister(overrideFn);
+ combined = Integration.testModule.getCombined(TestIntegration);
+ Assert.ok(combined.property === valueObject);
+});
+
+/**
+ * Checks that the combined integration objects cannot be used with XPCOM.
+ *
+ * This is limited by the fact that interfaces with the "[function]" annotation,
+ * for example nsIObserver, do not call the QueryInterface implementation.
+ */
+add_task(function* test_xpcom_throws() {
+ let combined = Integration.testModule.getCombined(TestIntegration);
+
+ // This calls QueryInterface because it looks for nsISupportsWeakReference.
+ Assert.throws(() => Services.obs.addObserver(combined, "test-topic", true),
+ "NS_NOINTERFACE");
+});
+
+/**
+ * Checks that getters defined by defineModuleGetter are able to retrieve the
+ * latest version of the combined integration object.
+ */
+add_task(function* test_defineModuleGetter() {
+ let objectForGetters = {};
+
+ // Test with and without the optional "symbol" parameter.
+ Integration.testModule.defineModuleGetter(objectForGetters,
+ "TestIntegration", "resource://testing-common/TestIntegration.jsm");
+ Integration.testModule.defineModuleGetter(objectForGetters,
+ "integration", "resource://testing-common/TestIntegration.jsm",
+ "TestIntegration");
+
+ Integration.testModule.register(overrideFn);
+ yield assertCombinedResults(objectForGetters.integration, 1);
+ yield assertCombinedResults(objectForGetters.TestIntegration, 1);
+
+ Integration.testModule.unregister(overrideFn);
+ yield assertCombinedResults(objectForGetters.integration, 0);
+ yield assertCombinedResults(objectForGetters.TestIntegration, 0);
+});
diff --git a/toolkit/modules/tests/xpcshell/test_JSONFile.js b/toolkit/modules/tests/xpcshell/test_JSONFile.js
new file mode 100644
index 000000000..77e8c55b9
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_JSONFile.js
@@ -0,0 +1,242 @@
+/**
+ * Tests the JSONFile object.
+ */
+
+"use strict";
+
+// Globals
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
+ "resource://gre/modules/DownloadPaths.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "JSONFile",
+ "resource://gre/modules/JSONFile.jsm");
+
+let gFileCounter = Math.floor(Math.random() * 1000000);
+
+/**
+ * Returns a reference to a temporary file, that is guaranteed not to exist, and
+ * to have never been created before.
+ *
+ * @param aLeafName
+ * Suggested leaf name for the file to be created.
+ *
+ * @return nsIFile pointing to a non-existent file in a temporary directory.
+ *
+ * @note It is not enough to delete the file if it exists, or to delete the file
+ * after calling nsIFile.createUnique, because on Windows the delete
+ * operation in the file system may still be pending, preventing a new
+ * file with the same name to be created.
+ */
+function getTempFile(aLeafName)
+{
+ // Prepend a serial number to the extension in the suggested leaf name.
+ let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
+ let leafName = base + "-" + gFileCounter + ext;
+ gFileCounter++;
+
+ // Get a file reference under the temporary directory for this test file.
+ let file = FileUtils.getFile("TmpD", [leafName]);
+ do_check_false(file.exists());
+
+ do_register_cleanup(function () {
+ if (file.exists()) {
+ file.remove(false);
+ }
+ });
+
+ return file;
+}
+
+const TEST_STORE_FILE_NAME = "test-store.json";
+
+const TEST_DATA = {
+ number: 123,
+ string: "test",
+ object: {
+ prop1: 1,
+ prop2: 2,
+ },
+};
+
+// Tests
+
+add_task(function* test_save_reload()
+{
+ let storeForSave = new JSONFile({
+ path: getTempFile(TEST_STORE_FILE_NAME).path,
+ });
+
+ yield storeForSave.load();
+
+ do_check_true(storeForSave.dataReady);
+ do_check_matches(storeForSave.data, {});
+
+ Object.assign(storeForSave.data, TEST_DATA);
+
+ yield new Promise((resolve) => {
+ let save = storeForSave._save.bind(storeForSave);
+ storeForSave._save = () => {
+ save();
+ resolve();
+ };
+ storeForSave.saveSoon();
+ });
+
+ let storeForLoad = new JSONFile({
+ path: storeForSave.path,
+ });
+
+ yield storeForLoad.load();
+
+ Assert.deepEqual(storeForLoad.data, TEST_DATA);
+});
+
+add_task(function* test_load_sync()
+{
+ let storeForSave = new JSONFile({
+ path: getTempFile(TEST_STORE_FILE_NAME).path
+ });
+ yield storeForSave.load();
+ Object.assign(storeForSave.data, TEST_DATA);
+ yield storeForSave._save();
+
+ let storeForLoad = new JSONFile({
+ path: storeForSave.path,
+ });
+ storeForLoad.ensureDataReady();
+
+ Assert.deepEqual(storeForLoad.data, TEST_DATA);
+});
+
+add_task(function* test_load_with_dataPostProcessor()
+{
+ let storeForSave = new JSONFile({
+ path: getTempFile(TEST_STORE_FILE_NAME).path
+ });
+ yield storeForSave.load();
+ Object.assign(storeForSave.data, TEST_DATA);
+ yield storeForSave._save();
+
+ let random = Math.random();
+ let storeForLoad = new JSONFile({
+ path: storeForSave.path,
+ dataPostProcessor: (data) => {
+ Assert.deepEqual(data, TEST_DATA);
+
+ data.test = random;
+ return data;
+ },
+ });
+
+ yield storeForLoad.load();
+
+ do_check_eq(storeForLoad.data.test, random);
+});
+
+add_task(function* test_load_with_dataPostProcessor_fails()
+{
+ let store = new JSONFile({
+ path: getTempFile(TEST_STORE_FILE_NAME).path,
+ dataPostProcessor: () => {
+ throw new Error("dataPostProcessor fails.");
+ },
+ });
+
+ yield Assert.rejects(store.load(), /dataPostProcessor fails\./);
+
+ do_check_false(store.dataReady);
+});
+
+add_task(function* test_load_sync_with_dataPostProcessor_fails()
+{
+ let store = new JSONFile({
+ path: getTempFile(TEST_STORE_FILE_NAME).path,
+ dataPostProcessor: () => {
+ throw new Error("dataPostProcessor fails.");
+ },
+ });
+
+ Assert.throws(() => store.ensureDataReady(), /dataPostProcessor fails\./);
+
+ do_check_false(store.dataReady);
+});
+
+/**
+ * Loads data from a string in a predefined format. The purpose of this test is
+ * to verify that the JSON format used in previous versions can be loaded.
+ */
+add_task(function* test_load_string_predefined()
+{
+ let store = new JSONFile({
+ path: getTempFile(TEST_STORE_FILE_NAME).path,
+ });
+
+ let string =
+ "{\"number\":123,\"string\":\"test\",\"object\":{\"prop1\":1,\"prop2\":2}}";
+
+ yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string),
+ { tmpPath: store.path + ".tmp" });
+
+ yield store.load();
+
+ Assert.deepEqual(store.data, TEST_DATA);
+});
+
+/**
+ * Loads data from a malformed JSON string.
+ */
+add_task(function* test_load_string_malformed()
+{
+ let store = new JSONFile({
+ path: getTempFile(TEST_STORE_FILE_NAME).path,
+ });
+
+ let string = "{\"number\":123,\"string\":\"test\",\"object\":{\"prop1\":1,";
+
+ yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string),
+ { tmpPath: store.path + ".tmp" });
+
+ yield store.load();
+
+ // A backup file should have been created.
+ do_check_true(yield OS.File.exists(store.path + ".corrupt"));
+ yield OS.File.remove(store.path + ".corrupt");
+
+ // The store should be ready to accept new data.
+ do_check_true(store.dataReady);
+ do_check_matches(store.data, {});
+});
+
+/**
+ * Loads data from a malformed JSON string, using the synchronous initialization
+ * path.
+ */
+add_task(function* test_load_string_malformed_sync()
+{
+ let store = new JSONFile({
+ path: getTempFile(TEST_STORE_FILE_NAME).path,
+ });
+
+ let string = "{\"number\":123,\"string\":\"test\",\"object\":{\"prop1\":1,";
+
+ yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string),
+ { tmpPath: store.path + ".tmp" });
+
+ store.ensureDataReady();
+
+ // A backup file should have been created.
+ do_check_true(yield OS.File.exists(store.path + ".corrupt"));
+ yield OS.File.remove(store.path + ".corrupt");
+
+ // The store should be ready to accept new data.
+ do_check_true(store.dataReady);
+ do_check_matches(store.data, {});
+});
diff --git a/toolkit/modules/tests/xpcshell/test_Log.js b/toolkit/modules/tests/xpcshell/test_Log.js
new file mode 100644
index 000000000..429bbcc50
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_Log.js
@@ -0,0 +1,592 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-disable block-spacing */
+
+var {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+
+Cu.import("resource://gre/modules/Promise.jsm")
+Cu.import("resource://gre/modules/Log.jsm");
+
+var testFormatter = {
+ format: function format(message) {
+ return message.loggerName + "\t" +
+ message.levelDesc + "\t" +
+ message.message;
+ }
+};
+
+function MockAppender(formatter) {
+ Log.Appender.call(this, formatter);
+ this.messages = [];
+}
+MockAppender.prototype = {
+ __proto__: Log.Appender.prototype,
+
+ doAppend: function DApp_doAppend(message) {
+ this.messages.push(message);
+ }
+};
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test_Logger() {
+ let log = Log.repository.getLogger("test.logger");
+ let appender = new MockAppender(new Log.BasicFormatter());
+
+ log.level = Log.Level.Debug;
+ appender.level = Log.Level.Info;
+ log.addAppender(appender);
+ log.info("info test");
+ log.debug("this should be logged but not appended.");
+
+ do_check_eq(appender.messages.length, 1);
+
+ let msgRe = /\d+\ttest.logger\t\INFO\tinfo test/;
+ do_check_true(msgRe.test(appender.messages[0]));
+});
+
+add_task(function test_Logger_parent() {
+ // Check whether parenting is correct
+ let grandparentLog = Log.repository.getLogger("grandparent");
+ let childLog = Log.repository.getLogger("grandparent.parent.child");
+ do_check_eq(childLog.parent.name, "grandparent");
+
+ let parentLog = Log.repository.getLogger("grandparent.parent");
+ do_check_eq(childLog.parent.name, "grandparent.parent");
+
+ // Check that appends are exactly in scope
+ let gpAppender = new MockAppender(new Log.BasicFormatter());
+ gpAppender.level = Log.Level.Info;
+ grandparentLog.addAppender(gpAppender);
+ childLog.info("child info test");
+ Log.repository.rootLogger.info("this shouldn't show up in gpAppender");
+
+ do_check_eq(gpAppender.messages.length, 1);
+ do_check_true(gpAppender.messages[0].indexOf("child info test") > 0);
+});
+
+add_test(function test_LoggerWithMessagePrefix() {
+ let log = Log.repository.getLogger("test.logger.prefix");
+ let appender = new MockAppender(new Log.MessageOnlyFormatter());
+ log.addAppender(appender);
+
+ let prefixed = Log.repository.getLoggerWithMessagePrefix(
+ "test.logger.prefix", "prefix: ");
+
+ log.warn("no prefix");
+ prefixed.warn("with prefix");
+
+ Assert.equal(appender.messages.length, 2, "2 messages were logged.");
+ Assert.deepEqual(appender.messages, [
+ "no prefix",
+ "prefix: with prefix",
+ ], "Prefix logger works.");
+
+ run_next_test();
+});
+
+/*
+ * A utility method for checking object equivalence.
+ * Fields with a reqular expression value in expected will be tested
+ * against the corresponding value in actual. Otherwise objects
+ * are expected to have the same keys and equal values.
+ */
+function checkObjects(expected, actual) {
+ do_check_true(expected instanceof Object);
+ do_check_true(actual instanceof Object);
+ for (let key in expected) {
+ do_check_neq(actual[key], undefined);
+ if (expected[key] instanceof RegExp) {
+ do_check_true(expected[key].test(actual[key].toString()));
+ } else if (expected[key] instanceof Object) {
+ checkObjects(expected[key], actual[key]);
+ } else {
+ do_check_eq(expected[key], actual[key]);
+ }
+ }
+
+ for (let key in actual) {
+ do_check_neq(expected[key], undefined);
+ }
+}
+
+add_task(function test_StructuredLogCommands() {
+ let appender = new MockAppender(new Log.StructuredFormatter());
+ let logger = Log.repository.getLogger("test.StructuredOutput");
+ logger.addAppender(appender);
+ logger.level = Log.Level.Info;
+
+ logger.logStructured("test_message", {_message: "message string one"});
+ logger.logStructured("test_message", {_message: "message string two",
+ _level: "ERROR",
+ source_file: "test_Log.js"});
+ logger.logStructured("test_message");
+ logger.logStructured("test_message", {source_file: "test_Log.js",
+ message_position: 4});
+
+ let messageOne = {"_time": /\d+/,
+ "_namespace": "test.StructuredOutput",
+ "_level": "INFO",
+ "_message": "message string one",
+ "action": "test_message"};
+
+ let messageTwo = {"_time": /\d+/,
+ "_namespace": "test.StructuredOutput",
+ "_level": "ERROR",
+ "_message": "message string two",
+ "action": "test_message",
+ "source_file": "test_Log.js"};
+
+ let messageThree = {"_time": /\d+/,
+ "_namespace": "test.StructuredOutput",
+ "_level": "INFO",
+ "action": "test_message"};
+
+ let messageFour = {"_time": /\d+/,
+ "_namespace": "test.StructuredOutput",
+ "_level": "INFO",
+ "action": "test_message",
+ "source_file": "test_Log.js",
+ "message_position": 4};
+
+ checkObjects(messageOne, JSON.parse(appender.messages[0]));
+ checkObjects(messageTwo, JSON.parse(appender.messages[1]));
+ checkObjects(messageThree, JSON.parse(appender.messages[2]));
+ checkObjects(messageFour, JSON.parse(appender.messages[3]));
+
+ let errored = false;
+ try {
+ logger.logStructured("", {_message: "invalid message"});
+ } catch (e) {
+ errored = true;
+ do_check_eq(e, "An action is required when logging a structured message.");
+ } finally {
+ do_check_true(errored);
+ }
+
+ errored = false;
+ try {
+ logger.logStructured("message_action", "invalid params");
+ } catch (e) {
+ errored = true;
+ do_check_eq(e, "The params argument is required to be an object.");
+ } finally {
+ do_check_true(errored);
+ }
+
+ // Logging with unstructured interface should produce the same messages
+ // as the structured interface for these cases.
+ appender = new MockAppender(new Log.StructuredFormatter());
+ logger = Log.repository.getLogger("test.StructuredOutput1");
+ messageOne._namespace = "test.StructuredOutput1";
+ messageTwo._namespace = "test.StructuredOutput1";
+ logger.addAppender(appender);
+ logger.level = Log.Level.All;
+ logger.info("message string one", {action: "test_message"});
+ logger.error("message string two", {action: "test_message",
+ source_file: "test_Log.js"});
+
+ checkObjects(messageOne, JSON.parse(appender.messages[0]));
+ checkObjects(messageTwo, JSON.parse(appender.messages[1]));
+});
+
+add_task(function test_StorageStreamAppender() {
+ let appender = new Log.StorageStreamAppender(testFormatter);
+ do_check_eq(appender.getInputStream(), null);
+
+ // Log to the storage stream and verify the log was written and can be
+ // read back.
+ let logger = Log.repository.getLogger("test.StorageStreamAppender");
+ logger.addAppender(appender);
+ logger.info("OHAI");
+ let inputStream = appender.getInputStream();
+ let data = NetUtil.readInputStreamToString(inputStream,
+ inputStream.available());
+ do_check_eq(data, "test.StorageStreamAppender\tINFO\tOHAI\n");
+
+ // We can read it again even.
+ let sndInputStream = appender.getInputStream();
+ let sameData = NetUtil.readInputStreamToString(sndInputStream,
+ sndInputStream.available());
+ do_check_eq(data, sameData);
+
+ // Reset the appender and log some more.
+ appender.reset();
+ do_check_eq(appender.getInputStream(), null);
+ logger.debug("wut?!?");
+ inputStream = appender.getInputStream();
+ data = NetUtil.readInputStreamToString(inputStream,
+ inputStream.available());
+ do_check_eq(data, "test.StorageStreamAppender\tDEBUG\twut?!?\n");
+});
+
+function fileContents(path) {
+ let decoder = new TextDecoder();
+ return OS.File.read(path).then(array => {
+ return decoder.decode(array);
+ });
+}
+
+add_task(function* test_FileAppender() {
+ // This directory does not exist yet
+ let dir = OS.Path.join(do_get_profile().path, "test_Log");
+ do_check_false(yield OS.File.exists(dir));
+ let path = OS.Path.join(dir, "test_FileAppender");
+ let appender = new Log.FileAppender(path, testFormatter);
+ let logger = Log.repository.getLogger("test.FileAppender");
+ logger.addAppender(appender);
+
+ // Logging to a file that can't be created won't do harm.
+ do_check_false(yield OS.File.exists(path));
+ logger.info("OHAI!");
+
+ yield OS.File.makeDir(dir);
+ logger.info("OHAI");
+ yield appender._lastWritePromise;
+
+ do_check_eq((yield fileContents(path)),
+ "test.FileAppender\tINFO\tOHAI\n");
+
+ logger.info("OHAI");
+ yield appender._lastWritePromise;
+
+ do_check_eq((yield fileContents(path)),
+ "test.FileAppender\tINFO\tOHAI\n" +
+ "test.FileAppender\tINFO\tOHAI\n");
+
+ // Reset the appender and log some more.
+ yield appender.reset();
+ do_check_false(yield OS.File.exists(path));
+
+ logger.debug("O RLY?!?");
+ yield appender._lastWritePromise;
+ do_check_eq((yield fileContents(path)),
+ "test.FileAppender\tDEBUG\tO RLY?!?\n");
+
+ yield appender.reset();
+ logger.debug("1");
+ logger.info("2");
+ logger.info("3");
+ logger.info("4");
+ logger.info("5");
+ // Waiting on only the last promise should account for all of these.
+ yield appender._lastWritePromise;
+
+ // Messages ought to be logged in order.
+ do_check_eq((yield fileContents(path)),
+ "test.FileAppender\tDEBUG\t1\n" +
+ "test.FileAppender\tINFO\t2\n" +
+ "test.FileAppender\tINFO\t3\n" +
+ "test.FileAppender\tINFO\t4\n" +
+ "test.FileAppender\tINFO\t5\n");
+});
+
+add_task(function* test_BoundedFileAppender() {
+ let dir = OS.Path.join(do_get_profile().path, "test_Log");
+
+ if (!(yield OS.File.exists(dir))) {
+ yield OS.File.makeDir(dir);
+ }
+
+ let path = OS.Path.join(dir, "test_BoundedFileAppender");
+ // This appender will hold about two lines at a time.
+ let appender = new Log.BoundedFileAppender(path, testFormatter, 40);
+ let logger = Log.repository.getLogger("test.BoundedFileAppender");
+ logger.addAppender(appender);
+
+ logger.info("ONE");
+ logger.info("TWO");
+ yield appender._lastWritePromise;
+
+ do_check_eq((yield fileContents(path)),
+ "test.BoundedFileAppender\tINFO\tONE\n" +
+ "test.BoundedFileAppender\tINFO\tTWO\n");
+
+ logger.info("THREE");
+ logger.info("FOUR");
+
+ do_check_neq(appender._removeFilePromise, undefined);
+ yield appender._removeFilePromise;
+ yield appender._lastWritePromise;
+
+ do_check_eq((yield fileContents(path)),
+ "test.BoundedFileAppender\tINFO\tTHREE\n" +
+ "test.BoundedFileAppender\tINFO\tFOUR\n");
+
+ yield appender.reset();
+ logger.info("ONE");
+ logger.info("TWO");
+ logger.info("THREE");
+ logger.info("FOUR");
+
+ do_check_neq(appender._removeFilePromise, undefined);
+ yield appender._removeFilePromise;
+ yield appender._lastWritePromise;
+
+ do_check_eq((yield fileContents(path)),
+ "test.BoundedFileAppender\tINFO\tTHREE\n" +
+ "test.BoundedFileAppender\tINFO\tFOUR\n");
+
+});
+
+/*
+ * Test parameter formatting.
+ */
+add_task(function* log_message_with_params() {
+ let formatter = new Log.BasicFormatter();
+
+ function formatMessage(text, params) {
+ let full = formatter.format(new Log.LogMessage("test.logger", Log.Level.Warn, text, params));
+ return full.split("\t")[3];
+ }
+
+ // Strings are substituted directly.
+ do_check_eq(formatMessage("String is ${foo}", {foo: "bar"}),
+ "String is bar");
+
+ // Numbers are substituted.
+ do_check_eq(formatMessage("Number is ${number}", {number: 47}),
+ "Number is 47")
+
+ // The entire params object is JSON-formatted and substituted.
+ do_check_eq(formatMessage("Object is ${}", {foo: "bar"}),
+ 'Object is {"foo":"bar"}');
+
+ // An object nested inside params is JSON-formatted and substituted.
+ do_check_eq(formatMessage("Sub object is ${sub}", {sub: {foo: "bar"}}),
+ 'Sub object is {"foo":"bar"}');
+
+ // The substitution field is missing from params. Leave the placeholder behind
+ // to make the mistake obvious.
+ do_check_eq(formatMessage("Missing object is ${missing}", {}),
+ 'Missing object is ${missing}');
+
+ // Make sure we don't treat the parameter name 'false' as a falsey value.
+ do_check_eq(formatMessage("False is ${false}", {false: true}),
+ 'False is true');
+
+ // If an object has a .toJSON method, the formatter uses it.
+ let ob = function() {};
+ ob.toJSON = function() {return {sneaky: "value"}};
+ do_check_eq(formatMessage("JSON is ${sub}", {sub: ob}),
+ 'JSON is {"sneaky":"value"}');
+
+ // Fall back to .toSource() if JSON.stringify() fails on an object.
+ ob = function() {};
+ ob.toJSON = function() {throw "oh noes JSON"};
+ do_check_eq(formatMessage("Fail is ${sub}", {sub: ob}),
+ 'Fail is (function () {})');
+
+ // Fall back to .toString if both .toJSON and .toSource fail.
+ ob.toSource = function() {throw "oh noes SOURCE"};
+ do_check_eq(formatMessage("Fail is ${sub}", {sub: ob}),
+ 'Fail is function () {}');
+
+ // Fall back to '[object]' if .toJSON, .toSource and .toString fail.
+ ob.toString = function() {throw "oh noes STRING"};
+ do_check_eq(formatMessage("Fail is ${sub}", {sub: ob}),
+ 'Fail is [object]');
+
+ // If params are passed but there are no substitution in the text
+ // we JSON format and append the entire parameters object.
+ do_check_eq(formatMessage("Text with no subs", {a: "b", c: "d"}),
+ 'Text with no subs: {"a":"b","c":"d"}');
+
+ // If we substitute one parameter but not the other,
+ // we ignore any params that aren't substituted.
+ do_check_eq(formatMessage("Text with partial sub ${a}", {a: "b", c: "d"}),
+ 'Text with partial sub b');
+
+ // We don't format internal fields stored in params.
+ do_check_eq(formatMessage("Params with _ ${}", {a: "b", _c: "d", _level:20, _message:"froo",
+ _time:123456, _namespace:"here.there"}),
+ 'Params with _ {"a":"b","_c":"d"}');
+
+ // Don't print an empty params holder if all params are internal.
+ do_check_eq(formatMessage("All params internal", {_level:20, _message:"froo",
+ _time:123456, _namespace:"here.there"}),
+ 'All params internal');
+
+ // Format params with null and undefined values.
+ do_check_eq(formatMessage("Null ${n} undefined ${u}", {n: null, u: undefined}),
+ 'Null null undefined undefined');
+
+ // Format params with number, bool, and Object/String type.
+ do_check_eq(formatMessage("number ${n} boolean ${b} boxed Boolean ${bx} String ${s}",
+ {n: 45, b: false, bx: new Boolean(true), s: new String("whatevs")}),
+ 'number 45 boolean false boxed Boolean true String whatevs');
+
+ /*
+ * Check that errors get special formatting if they're formatted directly as
+ * a named param or they're the only param, but not if they're a field in a
+ * larger structure.
+ */
+ let err = Components.Exception("test exception", Components.results.NS_ERROR_FAILURE);
+ let str = formatMessage("Exception is ${}", err);
+ do_check_true(str.includes('Exception is [Exception... "test exception"'));
+ do_check_true(str.includes("(NS_ERROR_FAILURE)"));
+ str = formatMessage("Exception is", err);
+ do_check_true(str.includes('Exception is: [Exception... "test exception"'));
+ str = formatMessage("Exception is ${error}", {error: err});
+ do_check_true(str.includes('Exception is [Exception... "test exception"'));
+ str = formatMessage("Exception is", {_error: err});
+ do_print(str);
+ // Exceptions buried inside objects are formatted badly.
+ do_check_true(str.includes('Exception is: {"_error":{}'));
+ // If the message text is null, the message contains only the formatted params object.
+ str = formatMessage(null, err);
+ do_check_true(str.startsWith('[Exception... "test exception"'));
+ // If the text is null and 'params' is a String object, the message is exactly that string.
+ str = formatMessage(null, new String("String in place of params"));
+ do_check_eq(str, "String in place of params");
+
+ // We use object.valueOf() internally; make sure a broken valueOf() method
+ // doesn't cause the logger to fail.
+ let vOf = {a: 1, valueOf: function() {throw "oh noes valueOf"}};
+ do_check_eq(formatMessage("Broken valueOf ${}", vOf),
+ 'Broken valueOf ({a:1, valueOf:(function () {throw "oh noes valueOf"})})');
+
+ // Test edge cases of bad data to formatter:
+ // If 'params' is not an object, format it as a basic type.
+ do_check_eq(formatMessage("non-object no subst", 1),
+ 'non-object no subst: 1');
+ do_check_eq(formatMessage("non-object all subst ${}", 2),
+ 'non-object all subst 2');
+ do_check_eq(formatMessage("false no subst", false),
+ 'false no subst: false');
+ do_check_eq(formatMessage("null no subst", null),
+ 'null no subst: null');
+ // If 'params' is undefined and there are no substitutions expected,
+ // the message should still be output.
+ do_check_eq(formatMessage("undefined no subst", undefined),
+ 'undefined no subst');
+ // If 'params' is not an object, no named substitutions can succeed;
+ // therefore we leave the placeholder and append the formatted params.
+ do_check_eq(formatMessage("non-object named subst ${junk} space", 3),
+ 'non-object named subst ${junk} space: 3');
+ // If there are no params, we leave behind the placeholders in the text.
+ do_check_eq(formatMessage("no params ${missing}", undefined),
+ 'no params ${missing}');
+ // If params doesn't contain any of the tags requested in the text,
+ // we leave them all behind and append the formatted params.
+ do_check_eq(formatMessage("object missing tag ${missing} space", {mising: "not here"}),
+ 'object missing tag ${missing} space: {"mising":"not here"}');
+ // If we are given null text and no params, the resulting formatted message is empty.
+ do_check_eq(formatMessage(null), '');
+});
+
+/*
+ * If we call a log function with a non-string object in place of the text
+ * argument, and no parameters, treat that the same as logging empty text
+ * with the object argument as parameters. This makes the log useful when the
+ * caller does "catch(err) {logger.error(err)}"
+ */
+add_task(function* test_log_err_only() {
+ let log = Log.repository.getLogger("error.only");
+ let mockFormatter = { format: msg => msg };
+ let appender = new MockAppender(mockFormatter);
+ log.addAppender(appender);
+
+ /*
+ * Check that log.error(err) is treated the same as
+ * log.error(null, err) by the logMessage constructor; the formatMessage()
+ * tests above ensure that the combination of null text and an error object
+ * is formatted correctly.
+ */
+ try {
+ eval("javascript syntax error");
+ }
+ catch (e) {
+ log.error(e);
+ msg = appender.messages.pop();
+ do_check_eq(msg.message, null);
+ do_check_eq(msg.params, e);
+ }
+});
+
+/*
+ * Test logStructured() messages through basic formatter.
+ */
+add_task(function* test_structured_basic() {
+ let log = Log.repository.getLogger("test.logger");
+ let appender = new MockAppender(new Log.BasicFormatter());
+
+ log.level = Log.Level.Info;
+ appender.level = Log.Level.Info;
+ log.addAppender(appender);
+
+ // A structured entry with no _message is treated the same as log./level/(null, params)
+ // except the 'action' field is added to the object.
+ log.logStructured("action", {data: "structure"});
+ do_check_eq(appender.messages.length, 1);
+ do_check_true(appender.messages[0].includes('{"data":"structure","action":"action"}'));
+
+ // A structured entry with _message and substitution is treated the same as
+ // log./level/(null, params).
+ log.logStructured("action", {_message: "Structured sub ${data}", data: "structure"});
+ do_check_eq(appender.messages.length, 2);
+ do_print(appender.messages[1]);
+ do_check_true(appender.messages[1].includes('Structured sub structure'));
+});
+
+/*
+ * Test that all the basic logger methods pass the message and params through to the appender.
+ */
+add_task(function* log_message_with_params() {
+ let log = Log.repository.getLogger("error.logger");
+ let mockFormatter = { format: msg => msg };
+ let appender = new MockAppender(mockFormatter);
+ log.addAppender(appender);
+
+ let testParams = {a:1, b:2};
+ log.fatal("Test fatal", testParams);
+ log.error("Test error", testParams);
+ log.warn("Test warn", testParams);
+ log.info("Test info", testParams);
+ log.config("Test config", testParams);
+ log.debug("Test debug", testParams);
+ log.trace("Test trace", testParams);
+ do_check_eq(appender.messages.length, 7);
+ for (let msg of appender.messages) {
+ do_check_true(msg.params === testParams);
+ do_check_true(msg.message.startsWith("Test "));
+ }
+});
+
+/*
+ * Check that we format JS Errors reasonably.
+ */
+add_task(function* format_errors() {
+ let pFormat = new Log.ParameterFormatter();
+
+ // Test that subclasses of Error are recognized as errors.
+ err = new ReferenceError("Ref Error", "ERROR_FILE", 28);
+ str = pFormat.format(err);
+ do_check_true(str.includes("ReferenceError"));
+ do_check_true(str.includes("ERROR_FILE:28"));
+ do_check_true(str.includes("Ref Error"));
+
+ // Test that JS-generated Errors are recognized and formatted.
+ try {
+ yield Promise.resolve(); // Scrambles the stack
+ eval("javascript syntax error");
+ }
+ catch (e) {
+ str = pFormat.format(e);
+ do_check_true(str.includes("SyntaxError: missing ;"));
+ // Make sure we identified it as an Error and formatted the error location as
+ // lineNumber:columnNumber.
+ do_check_true(str.includes(":1:11)"));
+ // Make sure that we use human-readable stack traces
+ // Check that the error doesn't contain any reference to "Promise.jsm" or "Task.jsm"
+ do_check_false(str.includes("Promise.jsm"));
+ do_check_false(str.includes("Task.jsm"));
+ do_check_true(str.includes("format_errors"));
+ }
+});
diff --git a/toolkit/modules/tests/xpcshell/test_Log_stackTrace.js b/toolkit/modules/tests/xpcshell/test_Log_stackTrace.js
new file mode 100644
index 000000000..6e53db058
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_Log_stackTrace.js
@@ -0,0 +1,30 @@
+print("Define some functions in well defined line positions for the test");
+function foo(v) { return bar(v + 1); } // line 2
+function bar(v) { return baz(v + 1); } // line 3
+function baz(v) { throw new Error(v + 1); } // line 4
+
+print("Make sure lazy constructor calling/assignment works");
+Components.utils.import("resource://gre/modules/Log.jsm");
+
+function run_test() {
+ print("Make sure functions, arguments, files are pretty printed in the trace");
+ let trace = "";
+ try {
+ foo(0);
+ }
+ catch (ex) {
+ trace = Log.stackTrace(ex);
+ }
+ print(`Got trace: ${trace}`);
+ do_check_neq(trace, "");
+
+ let bazPos = trace.indexOf("baz@test_Log_stackTrace.js:4");
+ let barPos = trace.indexOf("bar@test_Log_stackTrace.js:3");
+ let fooPos = trace.indexOf("foo@test_Log_stackTrace.js:2");
+ print(`String positions: ${bazPos} ${barPos} ${fooPos}`);
+
+ print("Make sure the desired messages show up");
+ do_check_true(bazPos >= 0);
+ do_check_true(barPos > bazPos);
+ do_check_true(fooPos > barPos);
+}
diff --git a/toolkit/modules/tests/xpcshell/test_MatchGlobs.js b/toolkit/modules/tests/xpcshell/test_MatchGlobs.js
new file mode 100644
index 000000000..5dcfd19cb
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_MatchGlobs.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/MatchPattern.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function test(url, pattern) {
+ let uri = Services.io.newURI(url, null, null);
+ let m = new MatchGlobs(pattern);
+ return m.matches(uri.spec);
+}
+
+function pass({url, pattern}) {
+ ok(test(url, pattern), `Expected match: ${JSON.stringify(pattern)}, ${url}`);
+}
+
+function fail({url, pattern}) {
+ ok(!test(url, pattern), `Expected no match: ${JSON.stringify(pattern)}, ${url}`);
+}
+
+function run_test() {
+ let moz = "http://mozilla.org";
+
+ pass({url: moz, pattern: ["*"]});
+ pass({url: moz, pattern: ["http://*"]}),
+ pass({url: moz, pattern: ["*mozilla*"]});
+ pass({url: moz, pattern: ["*example*", "*mozilla*"]});
+
+ pass({url: moz, pattern: ["*://*"]});
+ pass({url: "https://mozilla.org", pattern: ["*://*"]});
+
+ // Documentation example
+ pass({url: "http://www.example.com/foo/bar", pattern: ["http://???.example.com/foo/*"]});
+ pass({url: "http://the.example.com/foo/", pattern: ["http://???.example.com/foo/*"]});
+ fail({url: "http://my.example.com/foo/bar", pattern: ["http://???.example.com/foo/*"]});
+ fail({url: "http://example.com/foo/", pattern: ["http://???.example.com/foo/*"]});
+ fail({url: "http://www.example.com/foo", pattern: ["http://???.example.com/foo/*"]});
+
+ // Matches path
+ let path = moz + "/abc/def";
+ pass({url: path, pattern: ["*def"]});
+ pass({url: path, pattern: ["*c/d*"]});
+ pass({url: path, pattern: ["*org/abc*"]});
+ fail({url: path + "/", pattern: ["*def"]});
+
+ // Trailing slash
+ pass({url: moz, pattern: ["*.org/"]});
+ fail({url: moz, pattern: ["*.org"]});
+
+ // Wrong TLD
+ fail({url: moz, pattern: ["www*.m*.com/"]});
+ // Case sensitive
+ fail({url: moz, pattern: ["*.ORG/"]});
+
+ fail({url: moz, pattern: []});
+}
diff --git a/toolkit/modules/tests/xpcshell/test_MatchPattern.js b/toolkit/modules/tests/xpcshell/test_MatchPattern.js
new file mode 100644
index 000000000..583038361
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_MatchPattern.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/MatchPattern.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function test(url, pattern)
+{
+ let uri = Services.io.newURI(url, null, null);
+ let m = new MatchPattern(pattern);
+ return m.matches(uri);
+}
+
+function pass({url, pattern})
+{
+ do_check_true(test(url, pattern), `Expected match: ${JSON.stringify(pattern)}, ${url}`);
+}
+
+function fail({url, pattern})
+{
+ do_check_false(test(url, pattern), `Expected no match: ${JSON.stringify(pattern)}, ${url}`);
+}
+
+function run_test()
+{
+ // Invalid pattern.
+ fail({url: "http://mozilla.org", pattern: ""});
+
+ // Pattern must include trailing slash.
+ fail({url: "http://mozilla.org", pattern: "http://mozilla.org"});
+
+ // Protocol not allowed.
+ fail({url: "http://mozilla.org", pattern: "gopher://wuarchive.wustl.edu/"});
+
+ pass({url: "http://mozilla.org", pattern: "http://mozilla.org/"});
+ pass({url: "http://mozilla.org/", pattern: "http://mozilla.org/"});
+
+ pass({url: "http://mozilla.org/", pattern: "*://mozilla.org/"});
+ pass({url: "https://mozilla.org/", pattern: "*://mozilla.org/"});
+ fail({url: "file://mozilla.org/", pattern: "*://mozilla.org/"});
+ fail({url: "ftp://mozilla.org/", pattern: "*://mozilla.org/"});
+
+ fail({url: "http://mozilla.com", pattern: "http://*mozilla.com*/"});
+ fail({url: "http://mozilla.com", pattern: "http://mozilla.*/"});
+ fail({url: "http://mozilla.com", pattern: "http:/mozilla.com/"});
+
+ pass({url: "http://google.com", pattern: "http://*.google.com/"});
+ pass({url: "http://docs.google.com", pattern: "http://*.google.com/"});
+
+ pass({url: "http://mozilla.org:8080", pattern: "http://mozilla.org/"});
+ pass({url: "http://mozilla.org:8080", pattern: "*://mozilla.org/"});
+ fail({url: "http://mozilla.org:8080", pattern: "http://mozilla.org:8080/"});
+
+ // Now try with * in the path.
+ pass({url: "http://mozilla.org", pattern: "http://mozilla.org/*"});
+ pass({url: "http://mozilla.org/", pattern: "http://mozilla.org/*"});
+
+ pass({url: "http://mozilla.org/", pattern: "*://mozilla.org/*"});
+ pass({url: "https://mozilla.org/", pattern: "*://mozilla.org/*"});
+ fail({url: "file://mozilla.org/", pattern: "*://mozilla.org/*"});
+ fail({url: "http://mozilla.com", pattern: "http://mozilla.*/*"});
+
+ pass({url: "http://google.com", pattern: "http://*.google.com/*"});
+ pass({url: "http://docs.google.com", pattern: "http://*.google.com/*"});
+
+ // Check path stuff.
+ fail({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/"});
+ pass({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/*"});
+ pass({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/a*f"});
+ pass({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/a*"});
+ pass({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/*f"});
+ fail({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/*e"});
+ fail({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/*c"});
+
+ fail({url: "http:///a.html", pattern: "http:///a.html"});
+ pass({url: "file:///foo", pattern: "file:///foo*"});
+ pass({url: "file:///foo/bar.html", pattern: "file:///foo*"});
+
+ pass({url: "http://mozilla.org/a", pattern: "<all_urls>"});
+ pass({url: "https://mozilla.org/a", pattern: "<all_urls>"});
+ pass({url: "ftp://mozilla.org/a", pattern: "<all_urls>"});
+ pass({url: "file:///a", pattern: "<all_urls>"});
+ fail({url: "gopher://wuarchive.wustl.edu/a", pattern: "<all_urls>"});
+
+ // Multiple patterns.
+ pass({url: "http://mozilla.org", pattern: ["http://mozilla.org/"]});
+ pass({url: "http://mozilla.org", pattern: ["http://mozilla.org/", "http://mozilla.com/"]});
+ pass({url: "http://mozilla.com", pattern: ["http://mozilla.org/", "http://mozilla.com/"]});
+ fail({url: "http://mozilla.biz", pattern: ["http://mozilla.org/", "http://mozilla.com/"]});
+
+ // Match url with fragments.
+ pass({url: "http://mozilla.org/base#some-fragment", pattern: "http://mozilla.org/base"});
+}
diff --git a/toolkit/modules/tests/xpcshell/test_MatchURLFilters.js b/toolkit/modules/tests/xpcshell/test_MatchURLFilters.js
new file mode 100644
index 000000000..52e03a6cc
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_MatchURLFilters.js
@@ -0,0 +1,396 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/MatchPattern.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function createTestFilter({url, filters}) {
+ let m = new MatchURLFilters(filters);
+ return m.matches(url);
+}
+
+function expectPass({url, filters}) {
+ ok(createTestFilter({url, filters}),
+ `Expected match: ${JSON.stringify(filters)}, ${url}`);
+}
+
+function expectFail({url, filters}) {
+ ok(!createTestFilter({url, filters}),
+ `Expected no match: ${JSON.stringify(filters)}, ${url}`);
+}
+
+function expectThrow({url, filters, exceptionMessageContains}) {
+ let logData = {filters, url};
+
+ Assert.throws(
+ () => {
+ createTestFilter({url, filters});
+ },
+ exceptionMessageContains,
+ `Check received exception for expected message: ${JSON.stringify(logData)}`
+ );
+}
+
+add_task(function* test_match_url_filters() {
+ const shouldPass = true;
+ const shouldFail = true;
+ const shouldThrow = true;
+
+ var testCases = [
+ // Empty, undefined and null filters.
+ {shouldThrow, exceptionMessageContains: "filters array should not be empty",
+ filters: [], url: "http://mozilla.org", },
+ {shouldThrow, exceptionMessageContains: "filters should be an array",
+ filters: undefined, url: "http://mozilla.org"},
+ {shouldThrow, exceptionMessageContains: "filters should be an array",
+ filters: null, url: "http://mozilla.org"},
+
+ // Wrong formats (in a real webextension this will be blocked by the schema validation).
+ {shouldThrow, exceptionMessageContains: "filters should be an array", filters: {},
+ url: "http://mozilla.org"},
+ {shouldThrow, exceptionMessageContains: "filters should be an array",
+ filters: {nonExistentCriteria: true}, url: "http://mozilla.org", },
+ {shouldPass, filters: [{nonExistentCriteria: true}], url: "http://mozilla.org"},
+
+ // Schemes filter over various url schemes.
+ {shouldPass, filters: [{schemes: ["http"]}], url: "http://mozilla.org"},
+ {shouldPass, filters: [{schemes: ["https"]}], url: "https://mozilla.org"},
+ {shouldPass, filters: [{schemes: ["ftp"]}], url: "ftp://fake/ftp/url"},
+ {shouldPass, filters: [{schemes: ["about"]}], url: "about:blank"},
+ {shouldPass, filters: [{schemes: ["data"]}], url: "data:,testDataURL"},
+ {shouldFail, filters: [{schemes: ["http"]}], url: "ftp://fake/ftp/url"},
+
+ // Multiple schemes: pass when at least one scheme matches.
+ {shouldPass, filters: [{schemes: ["https", "about"]}], url: "https://mozilla.org"},
+ {shouldPass, filters: [{schemes: ["about", "https"]}], url: "https://mozilla.org"},
+ {shouldFail, filters: [{schemes: ["about", "http"]}], url: "https://mozilla.org"},
+
+ // Port filter: standard (implicit) ports.
+ {shouldPass, filters: [{ports: [443]}], url: "https://mozilla.org"},
+ {shouldPass, filters: [{ports: [80]}], url: "http://mozilla.org"},
+ {shouldPass, filters: [{ports: [21]}], url: "ftp://ftp.mozilla.org"},
+
+ // Port filter: schemes without a default port.
+ {shouldFail, filters: [{ports: [-1]}], url: "about:blank"},
+ {shouldFail, filters: [{ports: [-1]}], url: "data:,testDataURL"},
+
+ {shouldFail, filters: [{ports: [[1, 65535]]}], url: "about:blank"},
+ {shouldFail, filters: [{ports: [[1, 65535]]}], url: "data:,testDataURL"},
+
+ // Host filters (hostEquals, hostContains, hostPrefix, hostSuffix): schemes with an host.
+ {shouldFail, filters: [{hostEquals: ""}], url: "https://mozilla.org"},
+ {shouldPass, filters: [{hostEquals: null}], url: "https://mozilla.org"},
+ {shouldPass, filters: [{hostEquals: "mozilla.org"}], url: "https://mozilla.org"},
+ {shouldFail, filters: [{hostEquals: "mozilla.com"}], url: "https://mozilla.org"},
+ // NOTE: trying at least once another valid protocol.
+ {shouldPass, filters: [{hostEquals: "mozilla.org"}], url: "ftp://mozilla.org"},
+ {shouldFail, filters: [{hostEquals: "mozilla.com"}], url: "ftp://mozilla.org"},
+ {shouldPass, filters: [{hostEquals: "mozilla.org"}], url: "https://mozilla.org:8888"},
+
+ {shouldPass, filters: [{hostContains: "moz"}], url: "https://mozilla.org"},
+ // NOTE: an implicit '.' char is inserted into the host.
+ {shouldPass, filters: [{hostContains: ".moz"}], url: "https://mozilla.org"},
+ {shouldFail, filters: [{hostContains: "com"}], url: "https://mozilla.org"},
+ {shouldPass, filters: [{hostContains: ""}], url: "https://mozilla.org"},
+ {shouldPass, filters: [{hostContains: null}], url: "https://mozilla.org"},
+
+ {shouldPass, filters: [{hostPrefix: "moz"}], url: "https://mozilla.org"},
+ {shouldFail, filters: [{hostPrefix: "org"}], url: "https://mozilla.org"},
+ {shouldPass, filters: [{hostPrefix: ""}], url: "https://mozilla.org"},
+ {shouldPass, filters: [{hostPrefix: null}], url: "https://mozilla.org"},
+
+ {shouldPass, filters: [{hostSuffix: ".org"}], url: "https://mozilla.org"},
+ {shouldFail, filters: [{hostSuffix: "moz"}], url: "https://mozilla.org"},
+ {shouldPass, filters: [{hostSuffix: ""}], url: "https://mozilla.org"},
+ {shouldPass, filters: [{hostSuffix: null}], url: "https://mozilla.org"},
+ {shouldPass, filters: [{hostSuffix: "lla.org"}], url: "https://mozilla.org:8888"},
+
+ // hostEquals: urls without an host.
+ // TODO: should we explicitly cover hostContains, hostPrefix, hostSuffix for
+ // these sub-cases?
+ {shouldFail, filters: [{hostEquals: "blank"}], url: "about:blank"},
+ {shouldFail, filters: [{hostEquals: "blank"}], url: "about://blank"},
+ {shouldFail, filters: [{hostEquals: "testDataURL"}], url: "data:,testDataURL"},
+ {shouldPass, filters: [{hostEquals: ""}], url: "about:blank"},
+ {shouldPass, filters: [{hostEquals: ""}], url: "about://blank"},
+ {shouldPass, filters: [{hostEquals: ""}], url: "data:,testDataURL"},
+
+ // Path filters (pathEquals, pathContains, pathPrefix, pathSuffix).
+ {shouldFail, filters: [{pathEquals: ""}], url: "https://mozilla.org/test/path"},
+ {shouldPass, filters: [{pathEquals: null}], url: "https://mozilla.org/test/path"},
+ {shouldPass, filters: [{pathEquals: "/test/path"}], url: "https://mozilla.org/test/path"},
+ {shouldFail, filters: [{pathEquals: "/wrong/path"}], url: "https://mozilla.org/test/path"},
+ {shouldPass, filters: [{pathEquals: "/test/path"}], url: "https://mozilla.org:8888/test/path"},
+ // NOTE: trying at least once another valid protocol
+ {shouldPass, filters: [{pathEquals: "/test/path"}], url: "ftp://mozilla.org/test/path"},
+ {shouldFail, filters: [{pathEquals: "/wrong/path"}], url: "ftp://mozilla.org/test/path"},
+
+ {shouldPass, filters: [{pathContains: "st/"}], url: "https://mozilla.org/test/path"},
+ {shouldPass, filters: [{pathContains: "/test"}], url: "https://mozilla.org/test/path"},
+ {shouldFail, filters: [{pathContains: "org"}], url: "https://mozilla.org/test/path"},
+ {shouldPass, filters: [{pathContains: ""}], url: "https://mozilla.org/test/path"},
+ {shouldPass, filters: [{pathContains: null}], url: "https://mozilla.org/test/path"},
+ {shouldFail, filters: [{pathContains: "param"}], url: "https://mozilla.org:8888/test/path?param=1"},
+ {shouldFail, filters: [{pathContains: "ref"}], url: "https://mozilla.org:8888/test/path#ref"},
+ {shouldPass, filters: [{pathContains: "st/pa"}], url: "https://mozilla.org:8888/test/path"},
+
+ {shouldPass, filters: [{pathPrefix: "/te"}], url: "https://mozilla.org/test/path"},
+ {shouldFail, filters: [{pathPrefix: "org/"}], url: "https://mozilla.org/test/path"},
+ {shouldPass, filters: [{pathPrefix: ""}], url: "https://mozilla.org/test/path"},
+ {shouldPass, filters: [{pathPrefix: null}], url: "https://mozilla.org/test/path"},
+
+ {shouldPass, filters: [{pathSuffix: "/path"}], url: "https://mozilla.org/test/path"},
+ {shouldFail, filters: [{pathSuffix: "th/"}], url: "https://mozilla.org/test/path"},
+ {shouldPass, filters: [{pathSuffix: ""}], url: "https://mozilla.org/test/path"},
+ {shouldPass, filters: [{pathSuffix: null}], url: "https://mozilla.org/test/path"},
+ {shouldFail, filters: [{pathSuffix: "p=1"}], url: "https://mozilla.org:8888/test/path?p=1"},
+ {shouldFail, filters: [{pathSuffix: "ref"}], url: "https://mozilla.org:8888/test/path#ref"},
+
+ // Query filters (queryEquals, queryContains, queryPrefix, querySuffix).
+ {shouldFail, filters: [{queryEquals: ""}], url: "https://mozilla.org/?param=val"},
+ {shouldPass, filters: [{queryEquals: null}], url: "https://mozilla.org/?param=val"},
+ {shouldPass, filters: [{queryEquals: "param=val"}], url: "https://mozilla.org/?param=val"},
+ {shouldFail, filters: [{queryEquals: "?param=val"}], url: "https://mozilla.org/?param=val"},
+ {shouldFail, filters: [{queryEquals: "/path?param=val"}], url: "https://mozilla.org/path?param=val"},
+
+ // NOTE: about scheme urls cannot be matched by query.
+ {shouldFail, filters: [{queryEquals: "param=val"}], url: "about:blank?param=val"},
+ {shouldFail, filters: [{queryEquals: "param"}], url: "ftp://mozilla.org?param=val"},
+
+ {shouldPass, filters: [{queryContains: "ram"}], url: "https://mozilla.org/?param=val"},
+ {shouldPass, filters: [{queryContains: "=val"}], url: "https://mozilla.org/?param=val"},
+ {shouldFail, filters: [{queryContains: "?param"}], url: "https://mozilla.org/?param=val"},
+ {shouldFail, filters: [{queryContains: "path"}], url: "https://mozilla.org/path/?p=v#ref"},
+ {shouldPass, filters: [{queryContains: ""}], url: "https://mozilla.org/?param=val"},
+ {shouldPass, filters: [{queryContains: null}], url: "https://mozilla.org/?param=val"},
+
+ {shouldPass, filters: [{queryPrefix: "param"}], url: "https://mozilla.org/?param=val"},
+ {shouldFail, filters: [{queryPrefix: "p="}], url: "https://mozilla.org/?param=val"},
+ {shouldFail, filters: [{queryPrefix: "path"}], url: "https://mozilla.org/path?param=val"},
+ {shouldPass, filters: [{queryPrefix: ""}], url: "https://mozilla.org/?param=val"},
+ {shouldPass, filters: [{queryPrefix: null}], url: "https://mozilla.org/?param=val"},
+
+ {shouldPass, filters: [{querySuffix: "=val"}], url: "https://mozilla.org/?param=val"},
+ {shouldFail, filters: [{querySuffix: "=wrong"}], url: "https://mozilla.org/?param=val"},
+ {shouldPass, filters: [{querySuffix: ""}], url: "https://mozilla.org/?param=val"},
+ {shouldPass, filters: [{querySuffix: null}], url: "https://mozilla.org/?param=val"},
+
+ // URL filters (urlEquals, urlContains, urlPrefix, urlSuffix).
+ {shouldFail, filters: [{urlEquals: ""}], url: "https://mozilla.org/?p=v#ref"},
+ {shouldPass, filters: [{urlEquals: null}], url: "https://mozilla.org/?p=v#ref"},
+ {shouldPass, filters: [{urlEquals: "https://mozilla.org/?p=v#ref"}],
+ url: "https://mozilla.org/?p=v#ref"},
+ {shouldFail, filters: [{urlEquals: "https://mozilla.org/?p=v#ref2"}],
+ url: "https://mozilla.org/?p=v#ref"},
+ {shouldPass, filters: [{urlEquals: "about:blank?p=v#ref"}], url: "about:blank?p=v#ref"},
+ {shouldPass, filters: [{urlEquals: "ftp://mozilla.org?p=v#ref"}],
+ url: "ftp://mozilla.org?p=v#ref"},
+
+ {shouldPass, filters: [{urlContains: "org/?p"}], url: "https://mozilla.org/?p=v#ref"},
+ {shouldPass, filters: [{urlContains: "=v#ref"}], url: "https://mozilla.org/?p=v#ref"},
+ {shouldFail, filters: [{urlContains: "ftp"}], url: "https://mozilla.org/?p=v#ref"},
+ {shouldPass, filters: [{urlContains: ""}], url: "https://mozilla.org/?p=v#ref"},
+ {shouldPass, filters: [{urlContains: null}], url: "https://mozilla.org/?p=v#ref"},
+
+ {shouldPass, filters: [{urlPrefix: "http"}], url: "https://mozilla.org/?p=v#ref"},
+ {shouldFail, filters: [{urlPrefix: "moz"}], url: "https://mozilla.org/?p=v#ref"},
+ {shouldPass, filters: [{urlPrefix: ""}], url: "https://mozilla.org/?p=v#ref"},
+ {shouldPass, filters: [{urlPrefix: null}], url: "https://mozilla.org/?p=v#ref"},
+
+ {shouldPass, filters: [{urlSuffix: "#ref"}], url: "https://mozilla.org/?p=v#ref"},
+ {shouldFail, filters: [{urlSuffix: "=wrong"}], url: "https://mozilla.org/?p=v#ref"},
+ {shouldPass, filters: [{urlSuffix: ""}], url: "https://mozilla.org/?p=v#ref"},
+ {shouldPass, filters: [{urlSuffix: null}], url: "https://mozilla.org/?p=v#ref"},
+
+ // More url filters: urlMatches.
+ {shouldPass, filters: [{urlMatches: ".*://mozilla"}], url: "https://mozilla.org/?p=v#ref"},
+ {shouldPass, filters: [{urlMatches: ".*://mozilla"}], url: "ftp://mozilla.org/?p=v#ref"},
+ {shouldPass, filters: [{urlMatches: ".*://.*/\?p"}], url: "ftp://mozilla.org/?p=v#ref"},
+ // NOTE: urlMatches should not match the url without the ref.
+ {shouldFail, filters: [{urlMatches: "v#ref$"}], url: "https://mozilla.org/?p=v#ref"},
+ {shouldPass, filters: [{urlMatches: "^ftp"}], url: "ftp://mozilla.org/?p=v#ref"},
+
+ // More url filters: originAndPathMatches.
+ {shouldPass, filters: [{originAndPathMatches: ".*://mozilla"}],
+ url: "https://mozilla.org/?p=v#ref"},
+ {shouldPass, filters: [{originAndPathMatches: ".*://mozilla"}],
+ url: "ftp://mozilla.org/?p=v#ref"},
+ // NOTE: urlMatches should not match the url without the query and the ref.
+ {shouldFail, filters: [{originAndPathMatches: ".*://.*/\?p"}],
+ url: "ftp://mozilla.org/?p=v#ref"},
+ {shouldFail, filters: [{originAndPathMatches: "v#ref$"}],
+ url: "https://mozilla.org/?p=v#ref"},
+ {shouldPass, filters: [{originAndPathMatches: "^ftp"}],
+ url: "ftp://mozilla.org/?p=v#ref"},
+
+ // Filter with all criteria: all matches, none matches, some matches.
+
+ // All matches.
+ {shouldPass, filters: [
+ {
+ schemes: ["https", "http"],
+ ports: [443, 80],
+ hostEquals: "www.mozilla.org",
+ hostContains: ".moz",
+ hostPrefix: "www",
+ hostSuffix: "org",
+ pathEquals: "/sub/path",
+ pathContains: "b/p",
+ pathPrefix: "/sub",
+ pathSuffix: "/path",
+ queryEquals: "p=v",
+ queryContains: "1=",
+ queryPrefix: "p1",
+ querySuffix: "=v",
+ urlEquals: "https://www.mozilla.org/sub/path?p1=v#ref",
+ urlContains: "org/sub",
+ urlPrefix: "https://moz",
+ urlSuffix: "#ref",
+ urlMatches: "v#ref$",
+ originAndPathMatches: ".*://moz.*/"
+ },
+ ], url: "https://www.mozilla.org/sub/path?p1=v#ref"},
+ // None matches.
+ {shouldFail, filters: [
+ {
+ schemes: ["http"],
+ ports: [80],
+ hostEquals: "mozilla.com",
+ hostContains: "www.moz",
+ hostPrefix: "www",
+ hostSuffix: "com",
+ pathEquals: "/wrong/path",
+ pathContains: "g/p",
+ pathPrefix: "/wrong",
+ pathSuffix: "/wrong",
+ queryEquals: "p2=v",
+ queryContains: "2=",
+ queryPrefix: "p2",
+ querySuffix: "=value",
+ urlEquals: "http://mozilla.com/sub/path?p1=v#ref",
+ urlContains: "com/sub",
+ urlPrefix: "http://moz",
+ urlSuffix: "#ref2",
+ urlMatches: "value#ref2$",
+ originAndPathMatches: ".*://moz.*com/"
+ },
+ ], url: "https://mozilla.org/sub/path?p1=v#ref"},
+ // Some matches
+ {shouldFail, filters: [
+ {
+ schemes: ["https"],
+ ports: [80],
+ hostEquals: "mozilla.com",
+ hostContains: "www.moz",
+ hostPrefix: "www",
+ hostSuffix: "com",
+ pathEquals: "/wrong/path",
+ pathContains: "g/p",
+ pathPrefix: "/wrong",
+ pathSuffix: "/wrong",
+ queryEquals: "p2=v",
+ queryContains: "2=",
+ queryPrefix: "p2",
+ querySuffix: "=value",
+ urlEquals: "http://mozilla.com/sub/path?p1=v#ref",
+ urlContains: "com/sub",
+ urlPrefix: "http://moz",
+ urlSuffix: "#ref2",
+ urlMatches: "value#ref2$",
+ originAndPathMatches: ".*://moz.*com/"
+ },
+ ], url: "https://mozilla.org/sub/path?p1=v#ref"},
+
+ // Filter with multiple filters: all matches, some matches, none matches.
+
+ // All matches.
+ {shouldPass, filters: [
+ {schemes: ["https", "http"]},
+ {ports: [443, 80]},
+ {hostEquals: "www.mozilla.org"},
+ {hostContains: ".moz"},
+ {hostPrefix: "www"},
+ {hostSuffix: "org"},
+ {pathEquals: "/sub/path"},
+ {pathContains: "b/p"},
+ {pathPrefix: "/sub"},
+ {pathSuffix: "/path"},
+ {queryEquals: "p=v"},
+ {queryContains: "1="},
+ {queryPrefix: "p1"},
+ {querySuffix: "=v"},
+ {urlEquals: "https://www.mozilla.org/sub/path?p1=v#ref"},
+ {urlContains: "org/sub"},
+ {urlPrefix: "https://moz"},
+ {urlSuffix: "#ref"},
+ {urlMatches: "v#ref$"},
+ {originAndPathMatches: ".*://moz.*/"},
+ ], url: "https://www.mozilla.org/sub/path?p1=v#ref"},
+
+ // None matches.
+ {shouldFail, filters: [
+ {schemes: ["http"]},
+ {ports: [80]},
+ {hostEquals: "mozilla.com"},
+ {hostContains: "www.moz"},
+ {hostPrefix: "www"},
+ {hostSuffix: "com"},
+ {pathEquals: "/wrong/path"},
+ {pathContains: "g/p"},
+ {pathPrefix: "/wrong"},
+ {pathSuffix: "/wrong"},
+ {queryEquals: "p2=v"},
+ {queryContains: "2="},
+ {queryPrefix: "p2"},
+ {querySuffix: "=value"},
+ {urlEquals: "http://mozilla.com/sub/path?p1=v#ref"},
+ {urlContains: "com/sub"},
+ {urlPrefix: "http://moz"},
+ {urlSuffix: "#ref2"},
+ {urlMatches: "value#ref2$"},
+ {originAndPathMatches: ".*://moz.*com/"},
+ ], url: "https://mozilla.org/sub/path?p1=v#ref"},
+
+ // Some matches.
+ {shouldPass, filters: [
+ {schemes: ["https"]},
+ {ports: [80]},
+ {hostEquals: "mozilla.com"},
+ {hostContains: "www.moz"},
+ {hostPrefix: "www"},
+ {hostSuffix: "com"},
+ {pathEquals: "/wrong/path"},
+ {pathContains: "g/p"},
+ {pathPrefix: "/wrong"},
+ {pathSuffix: "/wrong"},
+ {queryEquals: "p2=v"},
+ {queryContains: "2="},
+ {queryPrefix: "p2"},
+ {querySuffix: "=value"},
+ {urlEquals: "http://mozilla.com/sub/path?p1=v#ref"},
+ {urlContains: "com/sub"},
+ {urlPrefix: "http://moz"},
+ {urlSuffix: "#ref2"},
+ {urlMatches: "value#ref2$"},
+ {originAndPathMatches: ".*://moz.*com/"},
+ ], url: "https://mozilla.org/sub/path?p1=v#ref"},
+ ];
+
+ // Run all the the testCases defined above.
+ for (let currentTest of testCases) {
+ let {
+ exceptionMessageContains,
+ url, filters,
+ } = currentTest;
+
+ if (currentTest.shouldThrow) {
+ expectThrow({url, filters, exceptionMessageContains})
+ } else if (currentTest.shouldFail) {
+ expectFail({url, filters});
+ } else {
+ expectPass({url, filters});
+ }
+ }
+});
diff --git a/toolkit/modules/tests/xpcshell/test_NewTabUtils.js b/toolkit/modules/tests/xpcshell/test_NewTabUtils.js
new file mode 100644
index 000000000..8cdb63550
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_NewTabUtils.js
@@ -0,0 +1,378 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// See also browser/base/content/test/newtab/.
+
+var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+Cu.import("resource://gre/modules/NewTabUtils.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const PREF_NEWTAB_ENHANCED = "browser.newtabpage.enhanced";
+
+function run_test() {
+ Services.prefs.setBoolPref(PREF_NEWTAB_ENHANCED, true);
+ run_next_test();
+}
+
+add_task(function* validCacheMidPopulation() {
+ let expectedLinks = makeLinks(0, 3, 1);
+
+ let provider = new TestProvider(done => done(expectedLinks));
+ provider.maxNumLinks = expectedLinks.length;
+
+ NewTabUtils.initWithoutProviders();
+ NewTabUtils.links.addProvider(provider);
+ let promise = new Promise(resolve => NewTabUtils.links.populateCache(resolve));
+
+ // isTopSiteGivenProvider() and getProviderLinks() should still return results
+ // even when cache is empty or being populated.
+ do_check_false(NewTabUtils.isTopSiteGivenProvider("example1.com", provider));
+ do_check_links(NewTabUtils.getProviderLinks(provider), []);
+
+ yield promise;
+
+ // Once the cache is populated, we get the expected results
+ do_check_true(NewTabUtils.isTopSiteGivenProvider("example1.com", provider));
+ do_check_links(NewTabUtils.getProviderLinks(provider), expectedLinks);
+ NewTabUtils.links.removeProvider(provider);
+});
+
+add_task(function* notifyLinkDelete() {
+ let expectedLinks = makeLinks(0, 3, 1);
+
+ let provider = new TestProvider(done => done(expectedLinks));
+ provider.maxNumLinks = expectedLinks.length;
+
+ NewTabUtils.initWithoutProviders();
+ NewTabUtils.links.addProvider(provider);
+ yield new Promise(resolve => NewTabUtils.links.populateCache(resolve));
+
+ do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
+
+ // Remove a link.
+ let removedLink = expectedLinks[2];
+ provider.notifyLinkChanged(removedLink, 2, true);
+ let links = NewTabUtils.links._providers.get(provider);
+
+ // Check that sortedLinks is correctly updated.
+ do_check_links(NewTabUtils.links.getLinks(), expectedLinks.slice(0, 2));
+
+ // Check that linkMap is accurately updated.
+ do_check_eq(links.linkMap.size, 2);
+ do_check_true(links.linkMap.get(expectedLinks[0].url));
+ do_check_true(links.linkMap.get(expectedLinks[1].url));
+ do_check_false(links.linkMap.get(removedLink.url));
+
+ // Check that siteMap is correctly updated.
+ do_check_eq(links.siteMap.size, 2);
+ do_check_true(links.siteMap.has(NewTabUtils.extractSite(expectedLinks[0].url)));
+ do_check_true(links.siteMap.has(NewTabUtils.extractSite(expectedLinks[1].url)));
+ do_check_false(links.siteMap.has(NewTabUtils.extractSite(removedLink.url)));
+
+ NewTabUtils.links.removeProvider(provider);
+});
+
+add_task(function* populatePromise() {
+ let count = 0;
+ let expectedLinks = makeLinks(0, 10, 2);
+
+ let getLinksFcn = Task.async(function* (callback) {
+ // Should not be calling getLinksFcn twice
+ count++;
+ do_check_eq(count, 1);
+ yield Promise.resolve();
+ callback(expectedLinks);
+ });
+
+ let provider = new TestProvider(getLinksFcn);
+
+ NewTabUtils.initWithoutProviders();
+ NewTabUtils.links.addProvider(provider);
+
+ NewTabUtils.links.populateProviderCache(provider, () => {});
+ NewTabUtils.links.populateProviderCache(provider, () => {
+ do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
+ NewTabUtils.links.removeProvider(provider);
+ });
+});
+
+add_task(function* isTopSiteGivenProvider() {
+ let expectedLinks = makeLinks(0, 10, 2);
+
+ // The lowest 2 frecencies have the same base domain.
+ expectedLinks[expectedLinks.length - 2].url = expectedLinks[expectedLinks.length - 1].url + "Test";
+
+ let provider = new TestProvider(done => done(expectedLinks));
+ provider.maxNumLinks = expectedLinks.length;
+
+ NewTabUtils.initWithoutProviders();
+ NewTabUtils.links.addProvider(provider);
+ yield new Promise(resolve => NewTabUtils.links.populateCache(resolve));
+
+ do_check_eq(NewTabUtils.isTopSiteGivenProvider("example2.com", provider), true);
+ do_check_eq(NewTabUtils.isTopSiteGivenProvider("example1.com", provider), false);
+
+ // Push out frecency 2 because the maxNumLinks is reached when adding frecency 3
+ let newLink = makeLink(3);
+ provider.notifyLinkChanged(newLink);
+
+ // There is still a frecent url with example2 domain, so it's still frecent.
+ do_check_eq(NewTabUtils.isTopSiteGivenProvider("example3.com", provider), true);
+ do_check_eq(NewTabUtils.isTopSiteGivenProvider("example2.com", provider), true);
+
+ // Push out frecency 3
+ newLink = makeLink(5);
+ provider.notifyLinkChanged(newLink);
+
+ // Push out frecency 4
+ newLink = makeLink(9);
+ provider.notifyLinkChanged(newLink);
+
+ // Our count reached 0 for the example2.com domain so it's no longer a frecent site.
+ do_check_eq(NewTabUtils.isTopSiteGivenProvider("example5.com", provider), true);
+ do_check_eq(NewTabUtils.isTopSiteGivenProvider("example2.com", provider), false);
+
+ NewTabUtils.links.removeProvider(provider);
+});
+
+add_task(function* multipleProviders() {
+ // Make each provider generate NewTabUtils.links.maxNumLinks links to check
+ // that no more than maxNumLinks are actually returned in the merged list.
+ let evenLinks = makeLinks(0, 2 * NewTabUtils.links.maxNumLinks, 2);
+ let evenProvider = new TestProvider(done => done(evenLinks));
+ let oddLinks = makeLinks(0, 2 * NewTabUtils.links.maxNumLinks - 1, 2);
+ let oddProvider = new TestProvider(done => done(oddLinks));
+
+ NewTabUtils.initWithoutProviders();
+ NewTabUtils.links.addProvider(evenProvider);
+ NewTabUtils.links.addProvider(oddProvider);
+
+ yield new Promise(resolve => NewTabUtils.links.populateCache(resolve));
+
+ let links = NewTabUtils.links.getLinks();
+ let expectedLinks = makeLinks(NewTabUtils.links.maxNumLinks,
+ 2 * NewTabUtils.links.maxNumLinks,
+ 1);
+ do_check_eq(links.length, NewTabUtils.links.maxNumLinks);
+ do_check_links(links, expectedLinks);
+
+ NewTabUtils.links.removeProvider(evenProvider);
+ NewTabUtils.links.removeProvider(oddProvider);
+});
+
+add_task(function* changeLinks() {
+ let expectedLinks = makeLinks(0, 20, 2);
+ let provider = new TestProvider(done => done(expectedLinks));
+
+ NewTabUtils.initWithoutProviders();
+ NewTabUtils.links.addProvider(provider);
+
+ yield new Promise(resolve => NewTabUtils.links.populateCache(resolve));
+
+ do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
+
+ // Notify of a new link.
+ let newLink = makeLink(19);
+ expectedLinks.splice(1, 0, newLink);
+ provider.notifyLinkChanged(newLink);
+ do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
+
+ // Notify of a link that's changed sort criteria.
+ newLink.frecency = 17;
+ expectedLinks.splice(1, 1);
+ expectedLinks.splice(2, 0, newLink);
+ provider.notifyLinkChanged({
+ url: newLink.url,
+ frecency: 17,
+ });
+ do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
+
+ // Notify of a link that's changed title.
+ newLink.title = "My frecency is now 17";
+ provider.notifyLinkChanged({
+ url: newLink.url,
+ title: newLink.title,
+ });
+ do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
+
+ // Notify of a new link again, but this time make it overflow maxNumLinks.
+ provider.maxNumLinks = expectedLinks.length;
+ newLink = makeLink(21);
+ expectedLinks.unshift(newLink);
+ expectedLinks.pop();
+ do_check_eq(expectedLinks.length, provider.maxNumLinks); // Sanity check.
+ provider.notifyLinkChanged(newLink);
+ do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
+
+ // Notify of many links changed.
+ expectedLinks = makeLinks(0, 3, 1);
+ provider.notifyManyLinksChanged();
+
+ // Since _populateProviderCache() is async, we must wait until the provider's
+ // populate promise has been resolved.
+ yield NewTabUtils.links._providers.get(provider).populatePromise;
+
+ // NewTabUtils.links will now repopulate its cache
+ do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
+
+ NewTabUtils.links.removeProvider(provider);
+});
+
+add_task(function* oneProviderAlreadyCached() {
+ let links1 = makeLinks(0, 10, 1);
+ let provider1 = new TestProvider(done => done(links1));
+
+ NewTabUtils.initWithoutProviders();
+ NewTabUtils.links.addProvider(provider1);
+
+ yield new Promise(resolve => NewTabUtils.links.populateCache(resolve));
+ do_check_links(NewTabUtils.links.getLinks(), links1);
+
+ let links2 = makeLinks(10, 20, 1);
+ let provider2 = new TestProvider(done => done(links2));
+ NewTabUtils.links.addProvider(provider2);
+
+ yield new Promise(resolve => NewTabUtils.links.populateCache(resolve));
+ do_check_links(NewTabUtils.links.getLinks(), links2.concat(links1));
+
+ NewTabUtils.links.removeProvider(provider1);
+ NewTabUtils.links.removeProvider(provider2);
+});
+
+add_task(function* newLowRankedLink() {
+ // Init a provider with 10 links and make its maximum number also 10.
+ let links = makeLinks(0, 10, 1);
+ let provider = new TestProvider(done => done(links));
+ provider.maxNumLinks = links.length;
+
+ NewTabUtils.initWithoutProviders();
+ NewTabUtils.links.addProvider(provider);
+
+ yield new Promise(resolve => NewTabUtils.links.populateCache(resolve));
+ do_check_links(NewTabUtils.links.getLinks(), links);
+
+ // Notify of a new link that's low-ranked enough not to make the list.
+ let newLink = makeLink(0);
+ provider.notifyLinkChanged(newLink);
+ do_check_links(NewTabUtils.links.getLinks(), links);
+
+ // Notify about the new link's title change.
+ provider.notifyLinkChanged({
+ url: newLink.url,
+ title: "a new title",
+ });
+ do_check_links(NewTabUtils.links.getLinks(), links);
+
+ NewTabUtils.links.removeProvider(provider);
+});
+
+add_task(function* extractSite() {
+ // All these should extract to the same site
+ [ "mozilla.org",
+ "m.mozilla.org",
+ "mobile.mozilla.org",
+ "www.mozilla.org",
+ "www3.mozilla.org",
+ ].forEach(host => {
+ let url = "http://" + host;
+ do_check_eq(NewTabUtils.extractSite(url), "mozilla.org", "extracted same " + host);
+ });
+
+ // All these should extract to the same subdomain
+ [ "bugzilla.mozilla.org",
+ "www.bugzilla.mozilla.org",
+ ].forEach(host => {
+ let url = "http://" + host;
+ do_check_eq(NewTabUtils.extractSite(url), "bugzilla.mozilla.org", "extracted eTLD+2 " + host);
+ });
+
+ // All these should not extract to the same site
+ [ "bugzilla.mozilla.org",
+ "bug123.bugzilla.mozilla.org",
+ "too.many.levels.bugzilla.mozilla.org",
+ "m2.mozilla.org",
+ "mobile30.mozilla.org",
+ "ww.mozilla.org",
+ "ww2.mozilla.org",
+ "wwwww.mozilla.org",
+ "wwwww50.mozilla.org",
+ "wwws.mozilla.org",
+ "secure.mozilla.org",
+ "secure10.mozilla.org",
+ "many.levels.deep.mozilla.org",
+ "just.check.in",
+ "192.168.0.1",
+ "localhost",
+ ].forEach(host => {
+ let url = "http://" + host;
+ do_check_neq(NewTabUtils.extractSite(url), "mozilla.org", "extracted diff " + host);
+ });
+
+ // All these should not extract to the same site
+ [ "about:blank",
+ "file:///Users/user/file",
+ "chrome://browser/something",
+ "ftp://ftp.mozilla.org/",
+ ].forEach(url => {
+ do_check_neq(NewTabUtils.extractSite(url), "mozilla.org", "extracted diff url " + url);
+ });
+});
+
+function TestProvider(getLinksFn) {
+ this.getLinks = getLinksFn;
+ this._observers = new Set();
+}
+
+TestProvider.prototype = {
+ addObserver: function (observer) {
+ this._observers.add(observer);
+ },
+ notifyLinkChanged: function (link, index=-1, deleted=false) {
+ this._notifyObservers("onLinkChanged", link, index, deleted);
+ },
+ notifyManyLinksChanged: function () {
+ this._notifyObservers("onManyLinksChanged");
+ },
+ _notifyObservers: function () {
+ let observerMethodName = arguments[0];
+ let args = Array.prototype.slice.call(arguments, 1);
+ args.unshift(this);
+ for (let obs of this._observers) {
+ if (obs[observerMethodName])
+ obs[observerMethodName].apply(NewTabUtils.links, args);
+ }
+ },
+};
+
+function do_check_links(actualLinks, expectedLinks) {
+ do_check_true(Array.isArray(actualLinks));
+ do_check_eq(actualLinks.length, expectedLinks.length);
+ for (let i = 0; i < expectedLinks.length; i++) {
+ let expected = expectedLinks[i];
+ let actual = actualLinks[i];
+ do_check_eq(actual.url, expected.url);
+ do_check_eq(actual.title, expected.title);
+ do_check_eq(actual.frecency, expected.frecency);
+ do_check_eq(actual.lastVisitDate, expected.lastVisitDate);
+ }
+}
+
+function makeLinks(frecRangeStart, frecRangeEnd, step) {
+ let links = [];
+ // Remember, links are ordered by frecency descending.
+ for (let i = frecRangeEnd; i > frecRangeStart; i -= step) {
+ links.push(makeLink(i));
+ }
+ return links;
+}
+
+function makeLink(frecency) {
+ return {
+ url: "http://example" + frecency + ".com/",
+ title: "My frecency is " + frecency,
+ frecency: frecency,
+ lastVisitDate: 0,
+ };
+}
diff --git a/toolkit/modules/tests/xpcshell/test_ObjectUtils.js b/toolkit/modules/tests/xpcshell/test_ObjectUtils.js
new file mode 100644
index 000000000..9aef3e907
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_ObjectUtils.js
@@ -0,0 +1,96 @@
+Components.utils.import("resource://gre/modules/ObjectUtils.jsm");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_deepEqual() {
+ let deepEqual = ObjectUtils.deepEqual.bind(ObjectUtils);
+ // CommonJS 7.2
+ Assert.ok(deepEqual(new Date(2000, 3, 14), new Date(2000, 3, 14)), "deepEqual date");
+ Assert.ok(deepEqual(new Date(NaN), new Date(NaN)), "deepEqual invalid dates");
+
+ Assert.ok(!deepEqual(new Date(), new Date(2000, 3, 14)), "deepEqual date");
+
+ // 7.3
+ Assert.ok(deepEqual(/a/, /a/));
+ Assert.ok(deepEqual(/a/g, /a/g));
+ Assert.ok(deepEqual(/a/i, /a/i));
+ Assert.ok(deepEqual(/a/m, /a/m));
+ Assert.ok(deepEqual(/a/igm, /a/igm));
+ Assert.ok(!deepEqual(/ab/, /a/));
+ Assert.ok(!deepEqual(/a/g, /a/));
+ Assert.ok(!deepEqual(/a/i, /a/));
+ Assert.ok(!deepEqual(/a/m, /a/));
+ Assert.ok(!deepEqual(/a/igm, /a/im));
+
+ let re1 = /a/;
+ re1.lastIndex = 3;
+ Assert.ok(!deepEqual(re1, /a/));
+
+ // 7.4
+ Assert.ok(deepEqual(4, "4"), "deepEqual == check");
+ Assert.ok(deepEqual(true, 1), "deepEqual == check");
+ Assert.ok(!deepEqual(4, "5"), "deepEqual == check");
+
+ // 7.5
+ // having the same number of owned properties && the same set of keys
+ Assert.ok(deepEqual({a: 4}, {a: 4}));
+ Assert.ok(deepEqual({a: 4, b: "2"}, {a: 4, b: "2"}));
+ Assert.ok(deepEqual([4], ["4"]));
+ Assert.ok(!deepEqual({a: 4}, {a: 4, b: true}));
+ Assert.ok(deepEqual(["a"], {0: "a"}));
+
+ let a1 = [1, 2, 3];
+ let a2 = [1, 2, 3];
+ a1.a = "test";
+ a1.b = true;
+ a2.b = true;
+ a2.a = "test";
+ Assert.ok(!deepEqual(Object.keys(a1), Object.keys(a2)));
+ Assert.ok(deepEqual(a1, a2));
+
+ let nbRoot = {
+ toString: function() { return this.first + " " + this.last; }
+ };
+
+ function nameBuilder(first, last) {
+ this.first = first;
+ this.last = last;
+ return this;
+ }
+ nameBuilder.prototype = nbRoot;
+
+ function nameBuilder2(first, last) {
+ this.first = first;
+ this.last = last;
+ return this;
+ }
+ nameBuilder2.prototype = nbRoot;
+
+ let nb1 = new nameBuilder("Ryan", "Dahl");
+ let nb2 = new nameBuilder2("Ryan", "Dahl");
+
+ Assert.ok(deepEqual(nb1, nb2));
+
+ nameBuilder2.prototype = Object;
+ nb2 = new nameBuilder2("Ryan", "Dahl");
+ Assert.ok(!deepEqual(nb1, nb2));
+
+ // String literal + object
+ Assert.ok(!deepEqual("a", {}));
+
+ // Make sure deepEqual doesn't loop forever on circular refs
+
+ let b = {};
+ b.b = b;
+
+ let c = {};
+ c.b = c;
+
+ try {
+ Assert.ok(!deepEqual(b, c));
+ } catch (e) {
+ Assert.ok(true, "Didn't recurse infinitely.");
+ }
+});
diff --git a/toolkit/modules/tests/xpcshell/test_ObjectUtils_strict.js b/toolkit/modules/tests/xpcshell/test_ObjectUtils_strict.js
new file mode 100644
index 000000000..44572e600
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_ObjectUtils_strict.js
@@ -0,0 +1,29 @@
+"use strict";
+
+var {ObjectUtils} = Components.utils.import("resource://gre/modules/ObjectUtils.jsm", {});
+var {PromiseTestUtils} = Components.utils.import("resource://testing-common/PromiseTestUtils.jsm", {});
+
+add_task(function* test_strict() {
+ let loose = { a: 1 };
+ let strict = ObjectUtils.strict(loose);
+
+ loose.a; // Should not throw.
+ loose.b || undefined; // Should not throw.
+
+ strict.a; // Should not throw.
+ PromiseTestUtils.expectUncaughtRejection(/No such property: "b"/);
+ Assert.throws(() => strict.b, /No such property: "b"/);
+ "b" in strict; // Should not throw.
+ strict.b = 2;
+ strict.b; // Should not throw.
+
+ PromiseTestUtils.expectUncaughtRejection(/No such property: "c"/);
+ Assert.throws(() => strict.c, /No such property: "c"/);
+ "c" in strict; // Should not throw.
+ loose.c = 3;
+ strict.c; // Should not throw.
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/modules/tests/xpcshell/test_PermissionsUtils.js b/toolkit/modules/tests/xpcshell/test_PermissionsUtils.js
new file mode 100644
index 000000000..3982ce015
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_PermissionsUtils.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that PerrmissionsUtils.jsm works as expected, including:
+// * PermissionsUtils.importfromPrefs()
+// <ROOT>.[whitelist|blacklist].add preferences are emptied when
+// converted into permissions on startup.
+
+
+const PREF_ROOT = "testpermissions.";
+const TEST_PERM = "test-permission";
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/PermissionsUtils.jsm");
+
+function run_test() {
+ test_importfromPrefs();
+}
+
+
+function test_importfromPrefs() {
+ // Create own preferences to test
+ Services.prefs.setCharPref(PREF_ROOT + "whitelist.add.EMPTY", "");
+ Services.prefs.setCharPref(PREF_ROOT + "whitelist.add.EMPTY2", ",");
+ Services.prefs.setCharPref(PREF_ROOT + "whitelist.add.TEST", "http://whitelist.example.com");
+ Services.prefs.setCharPref(PREF_ROOT + "whitelist.add.TEST2", "https://whitelist2-1.example.com,http://whitelist2-2.example.com:8080,about:home");
+ Services.prefs.setCharPref(PREF_ROOT + "whitelist.add.TEST3", "whitelist3-1.example.com,about:config"); // legacy style - host only
+ Services.prefs.setCharPref(PREF_ROOT + "blacklist.add.EMPTY", "");
+ Services.prefs.setCharPref(PREF_ROOT + "blacklist.add.TEST", "http://blacklist.example.com,");
+ Services.prefs.setCharPref(PREF_ROOT + "blacklist.add.TEST2", ",https://blacklist2-1.example.com,http://blacklist2-2.example.com:8080,about:mozilla");
+ Services.prefs.setCharPref(PREF_ROOT + "blacklist.add.TEST3", "blacklist3-1.example.com,about:preferences"); // legacy style - host only
+
+ // Check they are unknown in the permission manager prior to importing.
+ let whitelisted = ["http://whitelist.example.com",
+ "https://whitelist2-1.example.com",
+ "http://whitelist2-2.example.com:8080",
+ "http://whitelist3-1.example.com",
+ "https://whitelist3-1.example.com",
+ "about:config",
+ "about:home"];
+ let blacklisted = ["http://blacklist.example.com",
+ "https://blacklist2-1.example.com",
+ "http://blacklist2-2.example.com:8080",
+ "http://blacklist3-1.example.com",
+ "https://blacklist3-1.example.com",
+ "about:preferences",
+ "about:mozilla"];
+ let untouched = ["https://whitelist.example.com",
+ "https://blacklist.example.com",
+ "http://whitelist2-1.example.com",
+ "http://blacklist2-1.example.com",
+ "https://whitelist2-2.example.com:8080",
+ "https://blacklist2-2.example.com:8080"];
+ let unknown = whitelisted.concat(blacklisted).concat(untouched);
+ for (let url of unknown) {
+ let uri = Services.io.newURI(url, null, null);
+ do_check_eq(Services.perms.testPermission(uri, TEST_PERM), Services.perms.UNKNOWN_ACTION);
+ }
+
+ // Import them
+ PermissionsUtils.importFromPrefs(PREF_ROOT, TEST_PERM);
+
+ // Get list of preferences to check
+ let preferences = Services.prefs.getChildList(PREF_ROOT, {});
+
+ // Check preferences were emptied
+ for (let pref of preferences) {
+ do_check_eq(Services.prefs.getCharPref(pref), "");
+ }
+
+ // Check they were imported into the permissions manager
+ for (let url of whitelisted) {
+ let uri = Services.io.newURI(url, null, null);
+ do_check_eq(Services.perms.testPermission(uri, TEST_PERM), Services.perms.ALLOW_ACTION);
+ }
+ for (let url of blacklisted) {
+ let uri = Services.io.newURI(url, null, null);
+ do_check_eq(Services.perms.testPermission(uri, TEST_PERM), Services.perms.DENY_ACTION);
+ }
+ for (let url of untouched) {
+ let uri = Services.io.newURI(url, null, null);
+ do_check_eq(Services.perms.testPermission(uri, TEST_PERM), Services.perms.UNKNOWN_ACTION);
+ }
+}
diff --git a/toolkit/modules/tests/xpcshell/test_Preferences.js b/toolkit/modules/tests/xpcshell/test_Preferences.js
new file mode 100644
index 000000000..ef430909f
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_Preferences.js
@@ -0,0 +1,378 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} = Components;
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+function run_test() {
+ run_next_test();
+}
+
+add_test(function test_set_get_pref() {
+ Preferences.set("test_set_get_pref.integer", 1);
+ do_check_eq(Preferences.get("test_set_get_pref.integer"), 1);
+
+ Preferences.set("test_set_get_pref.string", "foo");
+ do_check_eq(Preferences.get("test_set_get_pref.string"), "foo");
+
+ Preferences.set("test_set_get_pref.boolean", true);
+ do_check_eq(Preferences.get("test_set_get_pref.boolean"), true);
+
+ // Clean up.
+ Preferences.resetBranch("test_set_get_pref.");
+
+ run_next_test();
+});
+
+add_test(function test_set_get_branch_pref() {
+ let prefs = new Preferences("test_set_get_branch_pref.");
+
+ prefs.set("something", 1);
+ do_check_eq(prefs.get("something"), 1);
+ do_check_false(Preferences.has("something"));
+
+ // Clean up.
+ prefs.reset("something");
+
+ run_next_test();
+});
+
+add_test(function test_set_get_multiple_prefs() {
+ Preferences.set({ "test_set_get_multiple_prefs.integer": 1,
+ "test_set_get_multiple_prefs.string": "foo",
+ "test_set_get_multiple_prefs.boolean": true });
+
+ let [i, s, b] = Preferences.get(["test_set_get_multiple_prefs.integer",
+ "test_set_get_multiple_prefs.string",
+ "test_set_get_multiple_prefs.boolean"]);
+
+ do_check_eq(i, 1);
+ do_check_eq(s, "foo");
+ do_check_eq(b, true);
+
+ // Clean up.
+ Preferences.resetBranch("test_set_get_multiple_prefs.");
+
+ run_next_test();
+});
+
+add_test(function test_get_multiple_prefs_with_default_value() {
+ Preferences.set({ "test_get_multiple_prefs_with_default_value.a": 1,
+ "test_get_multiple_prefs_with_default_value.b": 2 });
+
+ let [a, b, c] = Preferences.get(["test_get_multiple_prefs_with_default_value.a",
+ "test_get_multiple_prefs_with_default_value.b",
+ "test_get_multiple_prefs_with_default_value.c"],
+ 0);
+
+ do_check_eq(a, 1);
+ do_check_eq(b, 2);
+ do_check_eq(c, 0);
+
+ // Clean up.
+ Preferences.resetBranch("test_get_multiple_prefs_with_default_value.");
+
+ run_next_test();
+});
+
+add_test(function test_set_get_unicode_pref() {
+ Preferences.set("test_set_get_unicode_pref", String.fromCharCode(960));
+ do_check_eq(Preferences.get("test_set_get_unicode_pref"), String.fromCharCode(960));
+
+ // Clean up.
+ Preferences.reset("test_set_get_unicode_pref");
+
+ run_next_test();
+});
+
+add_test(function test_set_null_pref() {
+ try {
+ Preferences.set("test_set_null_pref", null);
+ // We expect this to throw, so the test is designed to fail if it doesn't.
+ do_check_true(false);
+ }
+ catch (ex) {}
+
+ run_next_test();
+});
+
+add_test(function test_set_undefined_pref() {
+ try {
+ Preferences.set("test_set_undefined_pref");
+ // We expect this to throw, so the test is designed to fail if it doesn't.
+ do_check_true(false);
+ }
+ catch (ex) {}
+
+ run_next_test();
+});
+
+add_test(function test_set_unsupported_pref() {
+ try {
+ Preferences.set("test_set_unsupported_pref", new Array());
+ // We expect this to throw, so the test is designed to fail if it doesn't.
+ do_check_true(false);
+ }
+ catch (ex) {}
+
+ run_next_test();
+});
+
+// Make sure that we can get a string pref that we didn't set ourselves
+// (i.e. that the way we get a string pref using getComplexValue doesn't
+// hork us getting a string pref that wasn't set using setComplexValue).
+add_test(function test_get_string_pref() {
+ let svc = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefService).
+ getBranch("");
+ svc.setCharPref("test_get_string_pref", "a normal string");
+ do_check_eq(Preferences.get("test_get_string_pref"), "a normal string");
+
+ // Clean up.
+ Preferences.reset("test_get_string_pref");
+
+ run_next_test();
+});
+
+add_test(function test_get_localized_string_pref() {
+ let svc = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefService).
+ getBranch("");
+ let prefName = "test_get_localized_string_pref";
+ let localizedString = Cc["@mozilla.org/pref-localizedstring;1"]
+ .createInstance(Ci.nsIPrefLocalizedString);
+ localizedString.data = "a localized string";
+ svc.setComplexValue(prefName, Ci.nsIPrefLocalizedString, localizedString);
+ do_check_eq(Preferences.get(prefName, null, Ci.nsIPrefLocalizedString),
+ "a localized string");
+
+ // Clean up.
+ Preferences.reset(prefName);
+
+ run_next_test();
+});
+
+add_test(function test_set_get_number_pref() {
+ Preferences.set("test_set_get_number_pref", 5);
+ do_check_eq(Preferences.get("test_set_get_number_pref"), 5);
+
+ // Non-integer values get converted to integers.
+ Preferences.set("test_set_get_number_pref", 3.14159);
+ do_check_eq(Preferences.get("test_set_get_number_pref"), 3);
+
+ // Values outside the range -(2^31-1) to 2^31-1 overflow.
+ try {
+ Preferences.set("test_set_get_number_pref", Math.pow(2, 31));
+ // We expect this to throw, so the test is designed to fail if it doesn't.
+ do_check_true(false);
+ }
+ catch (ex) {}
+
+ // Clean up.
+ Preferences.reset("test_set_get_number_pref");
+
+ run_next_test();
+});
+
+add_test(function test_reset_pref() {
+ Preferences.set("test_reset_pref", 1);
+ Preferences.reset("test_reset_pref");
+ do_check_eq(Preferences.get("test_reset_pref"), undefined);
+
+ run_next_test();
+});
+
+add_test(function test_reset_pref_branch() {
+ Preferences.set("test_reset_pref_branch.foo", 1);
+ Preferences.set("test_reset_pref_branch.bar", 2);
+ Preferences.resetBranch("test_reset_pref_branch.");
+ do_check_eq(Preferences.get("test_reset_pref_branch.foo"), undefined);
+ do_check_eq(Preferences.get("test_reset_pref_branch.bar"), undefined);
+
+ run_next_test();
+});
+
+// Make sure the module doesn't throw an exception when asked to reset
+// a nonexistent pref.
+add_test(function test_reset_nonexistent_pref() {
+ Preferences.reset("test_reset_nonexistent_pref");
+
+ run_next_test();
+});
+
+// Make sure the module doesn't throw an exception when asked to reset
+// a nonexistent pref branch.
+add_test(function test_reset_nonexistent_pref_branch() {
+ Preferences.resetBranch("test_reset_nonexistent_pref_branch.");
+
+ run_next_test();
+});
+
+add_test(function test_observe_prefs_function() {
+ let observed = false;
+ let observer = function() { observed = !observed };
+
+ Preferences.observe("test_observe_prefs_function", observer);
+ Preferences.set("test_observe_prefs_function", "something");
+ do_check_true(observed);
+
+ Preferences.ignore("test_observe_prefs_function", observer);
+ Preferences.set("test_observe_prefs_function", "something else");
+ do_check_true(observed);
+
+ // Clean up.
+ Preferences.reset("test_observe_prefs_function");
+
+ run_next_test();
+});
+
+add_test(function test_observe_prefs_object() {
+ let observer = {
+ observed: false,
+ observe: function() {
+ this.observed = !this.observed;
+ }
+ };
+
+ Preferences.observe("test_observe_prefs_object", observer.observe, observer);
+ Preferences.set("test_observe_prefs_object", "something");
+ do_check_true(observer.observed);
+
+ Preferences.ignore("test_observe_prefs_object", observer.observe, observer);
+ Preferences.set("test_observe_prefs_object", "something else");
+ do_check_true(observer.observed);
+
+ // Clean up.
+ Preferences.reset("test_observe_prefs_object");
+
+ run_next_test();
+});
+
+add_test(function test_observe_prefs_nsIObserver() {
+ let observer = {
+ observed: false,
+ observe: function(subject, topic, data) {
+ this.observed = !this.observed;
+ do_check_true(subject instanceof Ci.nsIPrefBranch);
+ do_check_eq(topic, "nsPref:changed");
+ do_check_eq(data, "test_observe_prefs_nsIObserver");
+ }
+ };
+
+ Preferences.observe("test_observe_prefs_nsIObserver", observer);
+ Preferences.set("test_observe_prefs_nsIObserver", "something");
+ do_check_true(observer.observed);
+
+ Preferences.ignore("test_observe_prefs_nsIObserver", observer);
+ Preferences.set("test_observe_prefs_nsIObserver", "something else");
+ do_check_true(observer.observed);
+
+ // Clean up.
+ Preferences.reset("test_observe_prefs_nsIObserver");
+
+ run_next_test();
+});
+
+/*
+add_test(function test_observe_exact_pref() {
+ let observed = false;
+ let observer = function() { observed = !observed };
+
+ Preferences.observe("test_observe_exact_pref", observer);
+ Preferences.set("test_observe_exact_pref.sub-pref", "something");
+ do_check_false(observed);
+
+ // Clean up.
+ Preferences.ignore("test_observe_exact_pref", observer);
+ Preferences.reset("test_observe_exact_pref.sub-pref");
+
+ run_next_test();
+});
+*/
+
+add_test(function test_observe_value_of_set_pref() {
+ let observer = function(newVal) { do_check_eq(newVal, "something") };
+
+ Preferences.observe("test_observe_value_of_set_pref", observer);
+ Preferences.set("test_observe_value_of_set_pref", "something");
+
+ // Clean up.
+ Preferences.ignore("test_observe_value_of_set_pref", observer);
+ Preferences.reset("test_observe_value_of_set_pref");
+
+ run_next_test();
+});
+
+add_test(function test_observe_value_of_reset_pref() {
+ let observer = function(newVal) { do_check_true(typeof newVal == "undefined") };
+
+ Preferences.set("test_observe_value_of_reset_pref", "something");
+ Preferences.observe("test_observe_value_of_reset_pref", observer);
+ Preferences.reset("test_observe_value_of_reset_pref");
+
+ // Clean up.
+ Preferences.ignore("test_observe_value_of_reset_pref", observer);
+
+ run_next_test();
+});
+
+add_test(function test_has_pref() {
+ do_check_false(Preferences.has("test_has_pref"));
+ Preferences.set("test_has_pref", "foo");
+ do_check_true(Preferences.has("test_has_pref"));
+
+ Preferences.set("test_has_pref.foo", "foo");
+ Preferences.set("test_has_pref.bar", "bar");
+ let [hasFoo, hasBar, hasBaz] = Preferences.has(["test_has_pref.foo",
+ "test_has_pref.bar",
+ "test_has_pref.baz"]);
+ do_check_true(hasFoo);
+ do_check_true(hasBar);
+ do_check_false(hasBaz);
+
+ // Clean up.
+ Preferences.resetBranch("test_has_pref");
+
+ run_next_test();
+});
+
+add_test(function test_isSet_pref() {
+ // Use a pref that we know has a default value but no user-set value.
+ // This feels dangerous; perhaps we should create some other default prefs
+ // that we can use for testing.
+ do_check_false(Preferences.isSet("toolkit.defaultChromeURI"));
+ Preferences.set("toolkit.defaultChromeURI", "foo");
+ do_check_true(Preferences.isSet("toolkit.defaultChromeURI"));
+
+ // Clean up.
+ Preferences.reset("toolkit.defaultChromeURI");
+
+ run_next_test();
+});
+
+/*
+add_test(function test_lock_prefs() {
+ // Use a pref that we know has a default value.
+ // This feels dangerous; perhaps we should create some other default prefs
+ // that we can use for testing.
+ do_check_false(Preferences.locked("toolkit.defaultChromeURI"));
+ Preferences.lock("toolkit.defaultChromeURI");
+ do_check_true(Preferences.locked("toolkit.defaultChromeURI"));
+ Preferences.unlock("toolkit.defaultChromeURI");
+ do_check_false(Preferences.locked("toolkit.defaultChromeURI"));
+
+ let val = Preferences.get("toolkit.defaultChromeURI");
+ Preferences.set("toolkit.defaultChromeURI", "test_lock_prefs");
+ do_check_eq(Preferences.get("toolkit.defaultChromeURI"), "test_lock_prefs");
+ Preferences.lock("toolkit.defaultChromeURI");
+ do_check_eq(Preferences.get("toolkit.defaultChromeURI"), val);
+ Preferences.unlock("toolkit.defaultChromeURI");
+ do_check_eq(Preferences.get("toolkit.defaultChromeURI"), "test_lock_prefs");
+
+ // Clean up.
+ Preferences.reset("toolkit.defaultChromeURI");
+
+ run_next_test();
+});
+*/
diff --git a/toolkit/modules/tests/xpcshell/test_Promise.js b/toolkit/modules/tests/xpcshell/test_Promise.js
new file mode 100644
index 000000000..6c7220692
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_Promise.js
@@ -0,0 +1,1105 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+Components.utils.import("resource://gre/modules/Promise.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+Components.utils.import("resource://testing-common/PromiseTestUtils.jsm");
+
+// Prevent test failures due to the unhandled rejections in this test file.
+PromiseTestUtils.disableUncaughtRejectionObserverForSelfTest();
+
+// Test runner
+
+var run_promise_tests = function run_promise_tests(tests, cb) {
+ let loop = function loop(index) {
+ if (index >= tests.length) {
+ if (cb) {
+ cb.call();
+ }
+ return;
+ }
+ do_print("Launching test " + (index + 1) + "/" + tests.length);
+ let test = tests[index];
+ // Execute from an empty stack
+ let next = function next() {
+ do_print("Test " + (index + 1) + "/" + tests.length + " complete");
+ do_execute_soon(function() {
+ loop(index + 1);
+ });
+ };
+ let result = test();
+ result.then(next, next);
+ };
+ return loop(0);
+};
+
+var make_promise_test = function(test) {
+ return function runtest() {
+ do_print("Test starting: " + test.name);
+ try {
+ let result = test();
+ if (result && "promise" in result) {
+ result = result.promise;
+ }
+ if (!result || !("then" in result)) {
+ let exn;
+ try {
+ do_throw("Test " + test.name + " did not return a promise: " + result);
+ } catch (x) {
+ exn = x;
+ }
+ return Promise.reject(exn);
+ }
+ // The test returns a promise
+ result = result.then(
+ // Test complete
+ function onResolve() {
+ do_print("Test complete: " + test.name);
+ },
+ // The test failed with an unexpected error
+ function onReject(err) {
+ let detail;
+ if (err && typeof err == "object" && "stack" in err) {
+ detail = err.stack;
+ } else {
+ detail = "(no stack)";
+ }
+ do_throw("Test " + test.name + " rejected with the following reason: "
+ + err + detail);
+ });
+ return result;
+ } catch (x) {
+ // The test failed because of an error outside of a promise
+ do_throw("Error in body of test " + test.name + ": " + x + " at " + x.stack);
+ return Promise.reject();
+ }
+ };
+};
+
+// Tests
+
+var tests = [];
+
+// Utility function to observe an failures in a promise
+// This function is useful if the promise itself is
+// not returned.
+var observe_failures = function observe_failures(promise) {
+ promise.catch(function onReject(reason) {
+ test.do_throw("Observed failure in test " + test + ": " + reason);
+ });
+};
+
+// Test that all observers are notified
+tests.push(make_promise_test(
+ function notification(test) {
+ // The size of the test
+ const SIZE = 10;
+ const RESULT = "this is an arbitrary value";
+
+ // Number of observers that yet need to be notified
+ let expected = SIZE;
+
+ // |true| once an observer has been notified
+ let notified = [];
+
+ // The promise observed
+ let source = Promise.defer();
+ let result = Promise.defer();
+
+ let install_observer = function install_observer(i) {
+ observe_failures(source.promise.then(
+ function onSuccess(value) {
+ do_check_true(!notified[i], "Ensuring that observer is notified at most once");
+ notified[i] = true;
+
+ do_check_eq(value, RESULT, "Ensuring that the observed value is correct");
+ if (--expected == 0) {
+ result.resolve();
+ }
+ }));
+ };
+
+ // Install a number of observers before resolving
+ let i;
+ for (i = 0; i < SIZE/2; ++i) {
+ install_observer(i);
+ }
+
+ source.resolve(RESULT);
+
+ // Install remaining observers
+ for (;i < SIZE; ++i) {
+ install_observer(i);
+ }
+
+ return result;
+ }));
+
+// Test that observers get the correct "this" value in strict mode.
+tests.push(
+ make_promise_test(function handlers_this_value(test) {
+ return Promise.resolve().then(
+ function onResolve() {
+ // Since this file is in strict mode, the correct value is "undefined".
+ do_check_eq(this, undefined);
+ throw "reject";
+ }
+ ).then(
+ null,
+ function onReject() {
+ // Since this file is in strict mode, the correct value is "undefined".
+ do_check_eq(this, undefined);
+ }
+ );
+ }));
+
+// Test that observers registered on a pending promise are notified in order.
+tests.push(
+ make_promise_test(function then_returns_before_callbacks(test) {
+ let deferred = Promise.defer();
+ let promise = deferred.promise;
+
+ let order = 0;
+
+ promise.then(
+ function onResolve() {
+ do_check_eq(order, 0);
+ order++;
+ }
+ );
+
+ promise.then(
+ function onResolve() {
+ do_check_eq(order, 1);
+ order++;
+ }
+ );
+
+ let newPromise = promise.then(
+ function onResolve() {
+ do_check_eq(order, 2);
+ }
+ );
+
+ deferred.resolve();
+
+ // This test finishes after the last handler succeeds.
+ return newPromise;
+ }));
+
+// Test that observers registered on a resolved promise are notified in order.
+tests.push(
+ make_promise_test(function then_returns_before_callbacks(test) {
+ let promise = Promise.resolve();
+
+ let order = 0;
+
+ promise.then(
+ function onResolve() {
+ do_check_eq(order, 0);
+ order++;
+ }
+ );
+
+ promise.then(
+ function onResolve() {
+ do_check_eq(order, 1);
+ order++;
+ }
+ );
+
+ // This test finishes after the last handler succeeds.
+ return promise.then(
+ function onResolve() {
+ do_check_eq(order, 2);
+ }
+ );
+ }));
+
+// Test that all observers are notified at most once, even if source
+// is resolved/rejected several times
+tests.push(make_promise_test(
+ function notification_once(test) {
+ // The size of the test
+ const SIZE = 10;
+ const RESULT = "this is an arbitrary value";
+
+ // Number of observers that yet need to be notified
+ let expected = SIZE;
+
+ // |true| once an observer has been notified
+ let notified = [];
+
+ // The promise observed
+ let observed = Promise.defer();
+ let result = Promise.defer();
+
+ let install_observer = function install_observer(i) {
+ observe_failures(observed.promise.then(
+ function onSuccess(value) {
+ do_check_true(!notified[i], "Ensuring that observer is notified at most once");
+ notified[i] = true;
+
+ do_check_eq(value, RESULT, "Ensuring that the observed value is correct");
+ if (--expected == 0) {
+ result.resolve();
+ }
+ }));
+ };
+
+ // Install a number of observers before resolving
+ let i;
+ for (i = 0; i < SIZE/2; ++i) {
+ install_observer(i);
+ }
+
+ observed.resolve(RESULT);
+
+ // Install remaining observers
+ for (;i < SIZE; ++i) {
+ install_observer(i);
+ }
+
+ // Resolve some more
+ for (i = 0; i < 10; ++i) {
+ observed.resolve(RESULT);
+ observed.reject();
+ }
+
+ return result;
+ }));
+
+// Test that throwing an exception from a onResolve listener
+// does not prevent other observers from receiving the notification
+// of success.
+tests.push(
+ make_promise_test(function exceptions_do_not_stop_notifications(test) {
+ let source = Promise.defer();
+
+ let exception_thrown = false;
+ let exception_content = new Error("Boom!");
+
+ let observer_1 = source.promise.then(
+ function onResolve() {
+ exception_thrown = true;
+ throw exception_content;
+ });
+
+ let observer_2 = source.promise.then(
+ function onResolve() {
+ do_check_true(exception_thrown, "Second observer called after first observer has thrown");
+ }
+ );
+
+ let result = observer_1.then(
+ function onResolve() {
+ do_throw("observer_1 should not have resolved");
+ },
+ function onReject(reason) {
+ do_check_true(reason == exception_content, "Obtained correct rejection");
+ }
+ );
+
+ source.resolve();
+ return result;
+ }
+));
+
+// Test that, once a promise is resolved, further resolve/reject
+// are ignored.
+tests.push(
+ make_promise_test(function subsequent_resolves_are_ignored(test) {
+ let deferred = Promise.defer();
+ deferred.resolve(1);
+ deferred.resolve(2);
+ deferred.reject(3);
+
+ let result = deferred.promise.then(
+ function onResolve(value) {
+ do_check_eq(value, 1, "Resolution chose the first value");
+ },
+ function onReject(reason) {
+ do_throw("Obtained a rejection while the promise was already resolved");
+ }
+ );
+
+ return result;
+ }));
+
+// Test that, once a promise is rejected, further resolve/reject
+// are ignored.
+tests.push(
+ make_promise_test(function subsequent_rejects_are_ignored(test) {
+ let deferred = Promise.defer();
+ deferred.reject(1);
+ deferred.reject(2);
+ deferred.resolve(3);
+
+ let result = deferred.promise.then(
+ function onResolve() {
+ do_throw("Obtained a resolution while the promise was already rejected");
+ },
+ function onReject(reason) {
+ do_check_eq(reason, 1, "Rejection chose the first value");
+ }
+ );
+
+ return result;
+ }));
+
+// Test that returning normally from a rejection recovers from the error
+// and that listeners are informed of a success.
+tests.push(
+ make_promise_test(function recovery(test) {
+ let boom = new Error("Boom!");
+ let deferred = Promise.defer();
+ const RESULT = "An arbitrary value";
+
+ let promise = deferred.promise.then(
+ function onResolve() {
+ do_throw("A rejected promise should not resolve");
+ },
+ function onReject(reason) {
+ do_check_true(reason == boom, "Promise was rejected with the correct error");
+ return RESULT;
+ }
+ );
+
+ promise = promise.then(
+ function onResolve(value) {
+ do_check_eq(value, RESULT, "Promise was recovered with the correct value");
+ }
+ );
+
+ deferred.reject(boom);
+ return promise;
+ }));
+
+// Test that returning a resolved promise from a onReject causes a resolution
+// (recovering from the error) and that returning a rejected promise
+// from a onResolve listener causes a rejection (raising an error).
+tests.push(
+ make_promise_test(function recovery_with_promise(test) {
+ let boom = new Error("Arbitrary error");
+ let deferred = Promise.defer();
+ const RESULT = "An arbitrary value";
+ const boom2 = new Error("Another arbitrary error");
+
+ // return a resolved promise from a onReject listener
+ let promise = deferred.promise.then(
+ function onResolve() {
+ do_throw("A rejected promise should not resolve");
+ },
+ function onReject(reason) {
+ do_check_true(reason == boom, "Promise was rejected with the correct error");
+ return Promise.resolve(RESULT);
+ }
+ );
+
+ // return a rejected promise from a onResolve listener
+ promise = promise.then(
+ function onResolve(value) {
+ do_check_eq(value, RESULT, "Promise was recovered with the correct value");
+ return Promise.reject(boom2);
+ }
+ );
+
+ promise = promise.catch(
+ function onReject(reason) {
+ do_check_eq(reason, boom2, "Rejection was propagated with the correct " +
+ "reason, through a promise");
+ }
+ );
+
+ deferred.reject(boom);
+ return promise;
+ }));
+
+// Test that we can resolve with promises of promises
+tests.push(
+ make_promise_test(function test_propagation(test) {
+ const RESULT = "Yet another arbitrary value";
+ let d1 = Promise.defer();
+ let d2 = Promise.defer();
+ let d3 = Promise.defer();
+
+ d3.resolve(d2.promise);
+ d2.resolve(d1.promise);
+ d1.resolve(RESULT);
+
+ return d3.promise.then(
+ function onSuccess(value) {
+ do_check_eq(value, RESULT, "Resolution with a promise eventually yielded "
+ + " the correct result");
+ }
+ );
+ }));
+
+// Test sequences of |then| and |catch|
+tests.push(
+ make_promise_test(function test_chaining(test) {
+ let error_1 = new Error("Error 1");
+ let error_2 = new Error("Error 2");
+ let result_1 = "First result";
+ let result_2 = "Second result";
+ let result_3 = "Third result";
+
+ let source = Promise.defer();
+
+ let promise = source.promise.then().then();
+
+ source.resolve(result_1);
+
+ // Check that result_1 is correctly propagated
+ promise = promise.then(
+ function onSuccess(result) {
+ do_check_eq(result, result_1, "Result was propagated correctly through " +
+ " several applications of |then|");
+ return result_2;
+ }
+ );
+
+ // Check that returning from the promise produces a resolution
+ promise = promise.catch(
+ function onReject() {
+ do_throw("Incorrect rejection");
+ }
+ );
+
+ // ... and that the check did not alter the value
+ promise = promise.then(
+ function onResolve(value) {
+ do_check_eq(value, result_2, "Result was propagated correctly once again");
+ }
+ );
+
+ // Now the same kind of tests for rejections
+ promise = promise.then(
+ function onResolve() {
+ throw error_1;
+ }
+ );
+
+ promise = promise.then(
+ function onResolve() {
+ do_throw("Incorrect resolution: the exception should have caused a rejection");
+ }
+ );
+
+ promise = promise.catch(
+ function onReject(reason) {
+ do_check_true(reason == error_1, "Reason was propagated correctly");
+ throw error_2;
+ }
+ );
+
+ promise = promise.catch(
+ function onReject(reason) {
+ do_check_true(reason == error_2, "Throwing an error altered the reason " +
+ "as expected");
+ return result_3;
+ }
+ );
+
+ promise = promise.then(
+ function onResolve(result) {
+ do_check_eq(result, result_3, "Error was correctly recovered");
+ }
+ );
+
+ return promise;
+ }));
+
+// Test that resolving with a rejected promise actually rejects
+tests.push(
+ make_promise_test(function resolve_to_rejected(test) {
+ let source = Promise.defer();
+ let error = new Error("Boom");
+
+ let promise = source.promise.then(
+ function onResolve() {
+ do_throw("Incorrect call to onResolve listener");
+ },
+ function onReject(reason) {
+ do_check_eq(reason, error, "Rejection lead to the expected reason");
+ }
+ );
+
+ source.resolve(Promise.reject(error));
+
+ return promise;
+ }));
+
+// Test that Promise.resolve resolves as expected
+tests.push(
+ make_promise_test(function test_resolve(test) {
+ const RESULT = "arbitrary value";
+ let p1 = Promise.resolve(RESULT);
+ let p2 = Promise.resolve(p1);
+ do_check_eq(p1, p2, "Promise.resolve used on a promise just returns the promise");
+
+ return p1.then(
+ function onResolve(result) {
+ do_check_eq(result, RESULT, "Promise.resolve propagated the correct result");
+ }
+ );
+ }));
+
+// Test that Promise.resolve throws when its argument is an async function.
+tests.push(
+ make_promise_test(function test_promise_resolve_throws_with_async_function(test) {
+ Assert.throws(() => Promise.resolve(Task.async(function* () {})),
+ /Cannot resolve a promise with an async function/);
+ return Promise.resolve();
+ }));
+
+// Test that the code after "then" is always executed before the callbacks
+tests.push(
+ make_promise_test(function then_returns_before_callbacks(test) {
+ let promise = Promise.resolve();
+
+ let thenExecuted = false;
+
+ promise = promise.then(
+ function onResolve() {
+ thenExecuted = true;
+ }
+ );
+
+ do_check_false(thenExecuted);
+
+ return promise;
+ }));
+
+// Test that chaining promises does not generate long stack traces
+tests.push(
+ make_promise_test(function chaining_short_stack(test) {
+ let source = Promise.defer();
+ let promise = source.promise;
+
+ const NUM_ITERATIONS = 100;
+
+ for (let i = 0; i < NUM_ITERATIONS; i++) {
+ promise = promise.then(
+ function onResolve(result) {
+ return result + ".";
+ }
+ );
+ }
+
+ promise = promise.then(
+ function onResolve(result) {
+ // Check that the execution went as expected.
+ let expectedString = new Array(1 + NUM_ITERATIONS).join(".");
+ do_check_true(result == expectedString);
+
+ // Check that we didn't generate one or more stack frames per iteration.
+ let stackFrameCount = 0;
+ let stackFrame = Components.stack;
+ while (stackFrame) {
+ stackFrameCount++;
+ stackFrame = stackFrame.caller;
+ }
+
+ do_check_true(stackFrameCount < NUM_ITERATIONS);
+ }
+ );
+
+ source.resolve("");
+
+ return promise;
+ }));
+
+// Test that the values of the promise return by Promise.all() are kept in the
+// given order even if the given promises are resolved in arbitrary order
+tests.push(
+ make_promise_test(function all_resolve(test) {
+ let d1 = Promise.defer();
+ let d2 = Promise.defer();
+ let d3 = Promise.defer();
+
+ d3.resolve(4);
+ d2.resolve(2);
+ do_execute_soon(() => d1.resolve(1));
+
+ let promises = [d1.promise, d2.promise, 3, d3.promise];
+
+ return Promise.all(promises).then(
+ function onResolve([val1, val2, val3, val4]) {
+ do_check_eq(val1, 1);
+ do_check_eq(val2, 2);
+ do_check_eq(val3, 3);
+ do_check_eq(val4, 4);
+ }
+ );
+ }));
+
+// Test that rejecting one of the promises passed to Promise.all()
+// rejects the promise return by Promise.all()
+tests.push(
+ make_promise_test(function all_reject(test) {
+ let error = new Error("Boom");
+
+ let d1 = Promise.defer();
+ let d2 = Promise.defer();
+ let d3 = Promise.defer();
+
+ d3.resolve(3);
+ d2.resolve(2);
+ do_execute_soon(() => d1.reject(error));
+
+ let promises = [d1.promise, d2.promise, d3.promise];
+
+ return Promise.all(promises).then(
+ function onResolve() {
+ do_throw("Incorrect call to onResolve listener");
+ },
+ function onReject(reason) {
+ do_check_eq(reason, error, "Rejection lead to the expected reason");
+ }
+ );
+ }));
+
+// Test that passing only values (not promises) to Promise.all()
+// forwards them all as resolution values.
+tests.push(
+ make_promise_test(function all_resolve_no_promises(test) {
+ try {
+ Promise.all(null);
+ do_check_true(false, "all() should only accept iterables");
+ } catch (e) {
+ do_check_true(true, "all() fails when first the arg is not an iterable");
+ }
+
+ let p1 = Promise.all([]).then(
+ function onResolve(val) {
+ do_check_true(Array.isArray(val) && val.length == 0);
+ }
+ );
+
+ let p2 = Promise.all([1, 2, 3]).then(
+ function onResolve([val1, val2, val3]) {
+ do_check_eq(val1, 1);
+ do_check_eq(val2, 2);
+ do_check_eq(val3, 3);
+ }
+ );
+
+ return Promise.all([p1, p2]);
+ }));
+
+// Test that Promise.all() handles non-array iterables
+tests.push(
+ make_promise_test(function all_iterable(test) {
+ function* iterable() {
+ yield 1;
+ yield 2;
+ yield 3;
+ }
+
+ return Promise.all(iterable()).then(
+ function onResolve([val1, val2, val3]) {
+ do_check_eq(val1, 1);
+ do_check_eq(val2, 2);
+ do_check_eq(val3, 3);
+ },
+ function onReject() {
+ do_throw("all() unexpectedly rejected");
+ }
+ );
+ }));
+
+// Test that throwing from the iterable passed to Promise.all() rejects the
+// promise returned by Promise.all()
+tests.push(
+ make_promise_test(function all_iterable_throws(test) {
+ function* iterable() {
+ throw 1;
+ }
+
+ return Promise.all(iterable()).then(
+ function onResolve() {
+ do_throw("all() unexpectedly resolved");
+ },
+ function onReject(reason) {
+ do_check_eq(reason, 1, "all() rejects when the iterator throws");
+ }
+ );
+ }));
+
+// Test that Promise.race() resolves with the first available resolution value
+tests.push(
+ make_promise_test(function race_resolve(test) {
+ let p1 = Promise.resolve(1);
+ let p2 = Promise.resolve().then(() => 2);
+
+ return Promise.race([p1, p2]).then(
+ function onResolve(value) {
+ do_check_eq(value, 1);
+ }
+ );
+ }));
+
+// Test that passing only values (not promises) to Promise.race() works
+tests.push(
+ make_promise_test(function race_resolve_no_promises(test) {
+ try {
+ Promise.race(null);
+ do_check_true(false, "race() should only accept iterables");
+ } catch (e) {
+ do_check_true(true, "race() fails when first the arg is not an iterable");
+ }
+
+ return Promise.race([1, 2, 3]).then(
+ function onResolve(value) {
+ do_check_eq(value, 1);
+ }
+ );
+ }));
+
+// Test that Promise.race() never resolves when passed an empty iterable
+tests.push(
+ make_promise_test(function race_resolve_never(test) {
+ return new Promise(resolve => {
+ Promise.race([]).then(
+ function onResolve() {
+ do_throw("race() unexpectedly resolved");
+ },
+ function onReject() {
+ do_throw("race() unexpectedly rejected");
+ }
+ );
+
+ // Approximate "never" so we don't have to solve the halting problem.
+ do_timeout(200, resolve);
+ });
+ }));
+
+// Test that Promise.race() handles non-array iterables.
+tests.push(
+ make_promise_test(function race_iterable(test) {
+ function* iterable() {
+ yield 1;
+ yield 2;
+ yield 3;
+ }
+
+ return Promise.race(iterable()).then(
+ function onResolve(value) {
+ do_check_eq(value, 1);
+ },
+ function onReject() {
+ do_throw("race() unexpectedly rejected");
+ }
+ );
+ }));
+
+// Test that throwing from the iterable passed to Promise.race() rejects the
+// promise returned by Promise.race()
+tests.push(
+ make_promise_test(function race_iterable_throws(test) {
+ function* iterable() {
+ throw 1;
+ }
+
+ return Promise.race(iterable()).then(
+ function onResolve() {
+ do_throw("race() unexpectedly resolved");
+ },
+ function onReject(reason) {
+ do_check_eq(reason, 1, "race() rejects when the iterator throws");
+ }
+ );
+ }));
+
+// Test that rejecting one of the promises passed to Promise.race() rejects the
+// promise returned by Promise.race()
+tests.push(
+ make_promise_test(function race_reject(test) {
+ let p1 = Promise.reject(1);
+ let p2 = Promise.resolve(2);
+ let p3 = Promise.resolve(3);
+
+ return Promise.race([p1, p2, p3]).then(
+ function onResolve() {
+ do_throw("race() unexpectedly resolved");
+ },
+ function onReject(reason) {
+ do_check_eq(reason, 1, "race() rejects when given a rejected promise");
+ }
+ );
+ }));
+
+// Test behavior of the Promise constructor.
+tests.push(
+ make_promise_test(function test_constructor(test) {
+ try {
+ new Promise(null);
+ do_check_true(false, "Constructor should fail when not passed a function");
+ } catch (e) {
+ do_check_true(true, "Constructor fails when not passed a function");
+ }
+
+ let executorRan = false;
+ let promise = new Promise(
+ function executor(resolve, reject) {
+ executorRan = true;
+ do_check_eq(this, undefined);
+ do_check_eq(typeof resolve, "function",
+ "resolve function should be passed to the executor");
+ do_check_eq(typeof reject, "function",
+ "reject function should be passed to the executor");
+ }
+ );
+ do_check_instanceof(promise, Promise);
+ do_check_true(executorRan, "Executor should execute synchronously");
+
+ // resolve a promise from the executor
+ let resolvePromise = new Promise(
+ function executor(resolve) {
+ resolve(1);
+ }
+ ).then(
+ function onResolve(value) {
+ do_check_eq(value, 1, "Executor resolved with correct value");
+ },
+ function onReject() {
+ do_throw("Executor unexpectedly rejected");
+ }
+ );
+
+ // reject a promise from the executor
+ let rejectPromise = new Promise(
+ function executor(_, reject) {
+ reject(1);
+ }
+ ).then(
+ function onResolve() {
+ do_throw("Executor unexpectedly resolved");
+ },
+ function onReject(reason) {
+ do_check_eq(reason, 1, "Executor rejected with correct value");
+ }
+ );
+
+ // throw from the executor, causing a rejection
+ let throwPromise = new Promise(
+ function executor() {
+ throw 1;
+ }
+ ).then(
+ function onResolve() {
+ do_throw("Throwing inside an executor should not resolve the promise");
+ },
+ function onReject(reason) {
+ do_check_eq(reason, 1, "Executor rejected with correct value");
+ }
+ );
+
+ return Promise.all([resolvePromise, rejectPromise, throwPromise]);
+ }));
+
+// Test deadlock in Promise.jsm with nested event loops
+// The scenario being tested is:
+// promise_1.then({
+// do some work that will asynchronously signal done
+// start an event loop waiting for the done signal
+// }
+// where the async work uses resolution of a second promise to
+// trigger the "done" signal. While this would likely work in a
+// naive implementation, our constant-stack implementation needs
+// a special case to avoid deadlock. Note that this test is
+// sensitive to the implementation-dependent order in which then()
+// clauses for two different promises are executed, so it is
+// possible for other implementations to pass this test and still
+// have similar deadlocks.
+tests.push(
+ make_promise_test(function promise_nested_eventloop_deadlock(test) {
+ // Set up a (long enough to be noticeable) timeout to
+ // exit the nested event loop and throw if the test run is hung
+ let shouldExitNestedEventLoop = false;
+
+ function event_loop() {
+ let thr = Services.tm.mainThread;
+ while (!shouldExitNestedEventLoop) {
+ thr.processNextEvent(true);
+ }
+ }
+
+ // I wish there was a way to cancel xpcshell do_timeout()s
+ do_timeout(2000, () => {
+ if (!shouldExitNestedEventLoop) {
+ shouldExitNestedEventLoop = true;
+ do_throw("Test timed out");
+ }
+ });
+
+ let promise1 = Promise.resolve(1);
+ let promise2 = Promise.resolve(2);
+
+ do_print("Setting wait for first promise");
+ promise1.then(value => {
+ do_print("Starting event loop");
+ event_loop();
+ }, null);
+
+ do_print("Setting wait for second promise");
+ return promise2.catch(error => { return 3; })
+ .then(
+ count => {
+ shouldExitNestedEventLoop = true;
+ });
+ }));
+
+function wait_for_uncaught(aMustAppear, aTimeout = undefined) {
+ let remaining = new Set();
+ for (let k of aMustAppear) {
+ remaining.add(k);
+ }
+ let deferred = Promise.defer();
+ let print = do_print;
+ let execute_soon = do_execute_soon;
+ let observer = function({message, stack}) {
+ let data = message + stack;
+ print("Observing " + message + ", looking for " + aMustAppear.join(", "));
+ for (let expected of remaining) {
+ if (data.indexOf(expected) != -1) {
+ print("I found " + expected);
+ remaining.delete(expected);
+ }
+ if (remaining.size == 0 && observer) {
+ Promise.Debugging.removeUncaughtErrorObserver(observer);
+ observer = null;
+ deferred.resolve();
+ }
+ }
+ };
+ Promise.Debugging.addUncaughtErrorObserver(observer);
+ if (aTimeout) {
+ do_timeout(aTimeout, function timeout() {
+ if (observer) {
+ Promise.Debugging.removeUncaughtErrorObserver(observer);
+ observer = null;
+ }
+ deferred.reject(new Error("Timeout"));
+ });
+ }
+ return deferred.promise;
+}
+
+// Test that uncaught errors are reported as uncaught
+(function() {
+ let make_string_rejection = function make_string_rejection() {
+ let salt = (Math.random() * ( Math.pow(2, 24) - 1 ));
+ let string = "This is an uncaught rejection " + salt;
+ // Our error is not Error-like nor an nsIException, so the stack will
+ // include the closure doing the actual rejection.
+ return {mustFind: ["test_rejection_closure", string], error: string};
+ };
+ let make_num_rejection = function make_num_rejection() {
+ let salt = (Math.random() * ( Math.pow(2, 24) - 1 ));
+ // Our error is not Error-like nor an nsIException, so the stack will
+ // include the closure doing the actual rejection.
+ return {mustFind: ["test_rejection_closure", salt], error: salt};
+ };
+ let make_undefined_rejection = function make_undefined_rejection() {
+ // Our error is not Error-like nor an nsIException, so the stack will
+ // include the closure doing the actual rejection.
+ return {mustFind: ["test_rejection_closure"], error: undefined};
+ };
+ let make_error_rejection = function make_error_rejection() {
+ let salt = (Math.random() * ( Math.pow(2, 24) - 1 ));
+ let error = new Error("This is an uncaught error " + salt);
+ return {
+ mustFind: [error.message, error.fileName, error.lineNumber, error.stack],
+ error: error
+ };
+ };
+ let make_exception_rejection = function make_exception_rejection() {
+ let salt = (Math.random() * ( Math.pow(2, 24) - 1 ));
+ let exn = new Components.Exception("This is an uncaught exception " + salt,
+ Components.results.NS_ERROR_NOT_AVAILABLE);
+ return {
+ mustFind: [exn.message, exn.filename, exn.lineNumber, exn.location.toString()],
+ error: exn
+ };
+ };
+ for (let make_rejection of [make_string_rejection,
+ make_num_rejection,
+ make_undefined_rejection,
+ make_error_rejection,
+ make_exception_rejection]) {
+ let {mustFind, error} = make_rejection();
+ let name = make_rejection.name;
+ tests.push(make_promise_test(function test_uncaught_is_reported() {
+ do_print("Testing with rejection " + name);
+ let promise = wait_for_uncaught(mustFind);
+ (function test_rejection_closure() {
+ // For the moment, we cannot be absolutely certain that a value is
+ // garbage-collected, even if it is not referenced anymore, due to
+ // the conservative stack-scanning algorithm.
+ //
+ // To be _almost_ certain that a value will be garbage-collected, we
+ // 1. isolate that value in an anonymous closure;
+ // 2. allocate 100 values instead of 1 (gc-ing a single value from
+ // these is sufficient for the test);
+ // 3. place everything in a loop, as the JIT typically reuses memory;
+ // 4. call all the GC methods we can.
+ //
+ // Unfortunately, we might still have intermittent failures,
+ // materialized as timeouts.
+ //
+ for (let i = 0; i < 100; ++i) {
+ Promise.reject(error);
+ }
+ })();
+ do_print("Posted all rejections");
+ Components.utils.forceGC();
+ Components.utils.forceCC();
+ Components.utils.forceShrinkingGC();
+ return promise;
+ }));
+ }
+})();
+
+
+// Test that caught errors are not reported as uncaught
+tests.push(
+make_promise_test(function test_caught_is_not_reported() {
+ let salt = (Math.random() * ( Math.pow(2, 24) - 1 ));
+ let promise = wait_for_uncaught([salt], 500);
+ (function() {
+ let uncaught = Promise.reject("This error, on the other hand, is caught " + salt);
+ uncaught.catch(function() { /* ignore rejection */ });
+ uncaught = null;
+ })();
+ // Isolate this in a function to increase likelihood that the gc will
+ // realise that |uncaught| has remained uncaught.
+ Components.utils.forceGC();
+
+ return promise.then(function onSuccess() {
+ throw new Error("This error was caught and should not have been reported");
+ }, function onError() {
+ do_print("The caught error was not reported, all is fine");
+ }
+ );
+}));
+
+// Bug 1033406 - Make sure Promise works even after freezing.
+tests.push(
+ make_promise_test(function test_freezing_promise(test) {
+ var p = new Promise(function executor(resolve) {
+ do_execute_soon(resolve);
+ });
+ Object.freeze(p);
+ return p;
+ })
+);
+
+function run_test()
+{
+ do_test_pending();
+ run_promise_tests(tests, do_test_finished);
+}
diff --git a/toolkit/modules/tests/xpcshell/test_PromiseUtils.js b/toolkit/modules/tests/xpcshell/test_PromiseUtils.js
new file mode 100644
index 000000000..c3ab839e4
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_PromiseUtils.js
@@ -0,0 +1,105 @@
+ /* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/PromiseUtils.jsm");
+Components.utils.import("resource://gre/modules/Timer.jsm");
+Components.utils.import("resource://testing-common/PromiseTestUtils.jsm");
+
+// Tests for PromiseUtils.jsm
+function run_test() {
+ run_next_test();
+}
+
+// Tests for PromiseUtils.defer()
+
+/* Tests for checking the resolve method of the Deferred object
+ * returned by PromiseUtils.defer() */
+add_task(function* test_resolve_string() {
+ let def = PromiseUtils.defer();
+ let expected = "The promise is resolved " + Math.random();
+ def.resolve(expected);
+ let result = yield def.promise;
+ Assert.equal(result, expected, "def.resolve() resolves the promise");
+});
+
+/* Test for the case when undefined is passed to the resolve method
+ * of the Deferred object */
+add_task(function* test_resolve_undefined() {
+ let def = PromiseUtils.defer();
+ def.resolve();
+ let result = yield def.promise;
+ Assert.equal(result, undefined, "resolve works with undefined as well");
+});
+
+/* Test when a pending promise is passed to the resolve method
+ * of the Deferred object */
+add_task(function* test_resolve_pending_promise() {
+ let def = PromiseUtils.defer();
+ let expected = 100 + Math.random();
+ let p = new Promise((resolve, reject) => {
+ setTimeout(() => resolve(expected), 100);
+ });
+ def.resolve(p);
+ let result = yield def.promise;
+ Assert.equal(result, expected, "def.promise assumed the state of the passed promise");
+});
+
+/* Test when a resovled promise is passed
+ * to the resolve method of the Deferred object */
+add_task(function* test_resolve_resolved_promise() {
+ let def = PromiseUtils.defer();
+ let expected = "Yeah resolved" + Math.random();
+ let p = new Promise((resolve, reject) => resolve(expected));
+ def.resolve(p);
+ let result = yield def.promise;
+ Assert.equal(result, expected, "Resolved promise is passed to the resolve method");
+});
+
+/* Test for the case when a rejected promise is
+ * passed to the resolve method */
+add_task(function* test_resolve_rejected_promise() {
+ let def = PromiseUtils.defer();
+ let p = new Promise((resolve, reject) => reject(new Error("There its an rejection")));
+ def.resolve(p);
+ yield Assert.rejects(def.promise, /There its an rejection/, "Settled rejection promise passed to the resolve method");
+});
+
+/* Test for the checking the reject method of
+ * the Deferred object returned by PromiseUtils.defer() */
+add_task(function* test_reject_Error() {
+ let def = PromiseUtils.defer();
+ def.reject(new Error("This one rejects"));
+ yield Assert.rejects(def.promise, /This one rejects/, "reject method with Error for rejection");
+});
+
+/* Test for the case when a pending Promise is passed to
+ * the reject method of Deferred object */
+add_task(function* test_reject_pending_promise() {
+ let def = PromiseUtils.defer();
+ let p = new Promise((resolve, reject) => {
+ setTimeout(() => resolve(100), 100);
+ });
+ def.reject(p);
+ yield Assert.rejects(def.promise, Promise, "Rejection with a pending promise uses the passed promise itself as the reason of rejection");
+});
+
+/* Test for the case when a resolved Promise
+ * is passed to the reject method */
+add_task(function* test_reject_resolved_promise() {
+ let def = PromiseUtils.defer();
+ let p = new Promise((resolve, reject) => resolve("This resolved"));
+ def.reject(p);
+ yield Assert.rejects(def.promise, Promise, "Rejection with a resolved promise uses the passed promise itself as the reason of rejection");
+});
+
+/* Test for the case when a rejected Promise is
+ * passed to the reject method */
+add_task(function* test_reject_resolved_promise() {
+ PromiseTestUtils.expectUncaughtRejection(/This one rejects/);
+ let def = PromiseUtils.defer();
+ let p = new Promise((resolve, reject) => reject(new Error("This one rejects")));
+ def.reject(p);
+ yield Assert.rejects(def.promise, Promise, "Rejection with a rejected promise uses the passed promise itself as the reason of rejection");
+});
diff --git a/toolkit/modules/tests/xpcshell/test_Services.js b/toolkit/modules/tests/xpcshell/test_Services.js
new file mode 100644
index 000000000..a50ecca3d
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_Services.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the Services.jsm module.
+ */
+
+// Globals
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function checkService(service, interface) {
+ do_print("Checking that Services." + service + " is an " + interface);
+ do_check_true(service in Services);
+ do_check_true(Services[service] instanceof interface);
+}
+
+// Tests
+
+function run_test()
+{
+ do_get_profile();
+
+ checkService("appShell", Ci.nsIAppShellService);
+ checkService("appinfo", Ci.nsIXULRuntime);
+ checkService("blocklist", Ci.nsIBlocklistService);
+ checkService("cache", Ci.nsICacheService);
+ checkService("cache2", Ci.nsICacheStorageService);
+ checkService("clipboard", Ci.nsIClipboard);
+ checkService("console", Ci.nsIConsoleService);
+ checkService("contentPrefs", Ci.nsIContentPrefService);
+ checkService("cookies", Ci.nsICookieManager2);
+ checkService("dirsvc", Ci.nsIDirectoryService);
+ checkService("dirsvc", Ci.nsIProperties);
+ checkService("DOMRequest", Ci.nsIDOMRequestService);
+ checkService("domStorageManager", Ci.nsIDOMStorageManager);
+ checkService("downloads", Ci.nsIDownloadManager);
+ checkService("droppedLinkHandler", Ci.nsIDroppedLinkHandler);
+ checkService("eTLD", Ci.nsIEffectiveTLDService);
+ checkService("focus", Ci.nsIFocusManager);
+ checkService("io", Ci.nsIIOService);
+ checkService("io", Ci.nsIIOService2);
+ checkService("locale", Ci.nsILocaleService);
+ checkService("logins", Ci.nsILoginManager);
+ checkService("obs", Ci.nsIObserverService);
+ checkService("perms", Ci.nsIPermissionManager);
+ checkService("prefs", Ci.nsIPrefBranch);
+ checkService("prefs", Ci.nsIPrefService);
+ checkService("prompt", Ci.nsIPromptService);
+ checkService("scriptSecurityManager", Ci.nsIScriptSecurityManager);
+ checkService("scriptloader", Ci.mozIJSSubScriptLoader);
+ checkService("startup", Ci.nsIAppStartup);
+ checkService("storage", Ci.mozIStorageService);
+ checkService("strings", Ci.nsIStringBundleService);
+ checkService("sysinfo", Ci.nsIPropertyBag2);
+ checkService("telemetry", Ci.nsITelemetry);
+ checkService("tm", Ci.nsIThreadManager);
+ checkService("uriFixup", Ci.nsIURIFixup);
+ checkService("urlFormatter", Ci.nsIURLFormatter);
+ checkService("vc", Ci.nsIVersionComparator);
+ checkService("wm", Ci.nsIWindowMediator);
+ checkService("ww", Ci.nsIWindowWatcher);
+ if ("nsIBrowserSearchService" in Ci) {
+ checkService("search", Ci.nsIBrowserSearchService);
+ }
+ if ("nsIAndroidBridge" in Ci) {
+ checkService("androidBridge", Ci.nsIAndroidBridge);
+ }
+
+ // In xpcshell tests, the "@mozilla.org/xre/app-info;1" component implements
+ // only the nsIXULRuntime interface, but not nsIXULAppInfo. To test the
+ // service getter for the latter interface, load mock app-info.
+ let tmp = {};
+ Cu.import("resource://testing-common/AppInfo.jsm", tmp);
+ tmp.updateAppInfo();
+
+ // We need to reload the module to update the lazy getter.
+ Cu.unload("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ checkService("appinfo", Ci.nsIXULAppInfo);
+
+ Cu.unload("resource://gre/modules/Services.jsm");
+}
diff --git a/toolkit/modules/tests/xpcshell/test_UpdateUtils_updatechannel.js b/toolkit/modules/tests/xpcshell/test_UpdateUtils_updatechannel.js
new file mode 100644
index 000000000..75d7a1992
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_UpdateUtils_updatechannel.js
@@ -0,0 +1,38 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/UpdateUtils.jsm");
+
+const PREF_APP_UPDATE_CHANNEL = "app.update.channel";
+const TEST_CHANNEL = "TestChannel";
+const PREF_PARTNER_A = "app.partner.test_partner_a";
+const TEST_PARTNER_A = "TestPartnerA";
+const PREF_PARTNER_B = "app.partner.test_partner_b";
+const TEST_PARTNER_B = "TestPartnerB";
+
+add_task(function* test_updatechannel() {
+ let defaultPrefs = new Preferences({ defaultBranch: true });
+ let currentChannel = defaultPrefs.get(PREF_APP_UPDATE_CHANNEL);
+
+ do_check_eq(UpdateUtils.UpdateChannel, currentChannel);
+ do_check_eq(UpdateUtils.getUpdateChannel(true), currentChannel);
+ do_check_eq(UpdateUtils.getUpdateChannel(false), currentChannel);
+
+ defaultPrefs.set(PREF_APP_UPDATE_CHANNEL, TEST_CHANNEL);
+ do_check_eq(UpdateUtils.UpdateChannel, TEST_CHANNEL);
+ do_check_eq(UpdateUtils.getUpdateChannel(true), TEST_CHANNEL);
+ do_check_eq(UpdateUtils.getUpdateChannel(false), TEST_CHANNEL);
+
+ defaultPrefs.set(PREF_PARTNER_A, TEST_PARTNER_A);
+ defaultPrefs.set(PREF_PARTNER_B, TEST_PARTNER_B);
+ do_check_eq(UpdateUtils.UpdateChannel,
+ TEST_CHANNEL + "-cck-" + TEST_PARTNER_A + "-" + TEST_PARTNER_B);
+ do_check_eq(UpdateUtils.getUpdateChannel(true),
+ TEST_CHANNEL + "-cck-" + TEST_PARTNER_A + "-" + TEST_PARTNER_B);
+ do_check_eq(UpdateUtils.getUpdateChannel(false), TEST_CHANNEL);
+});
diff --git a/toolkit/modules/tests/xpcshell/test_UpdateUtils_url.js b/toolkit/modules/tests/xpcshell/test_UpdateUtils_url.js
new file mode 100644
index 000000000..da5d868e3
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_UpdateUtils_url.js
@@ -0,0 +1,292 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/UpdateUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://testing-common/AppInfo.jsm");
+Cu.import("resource://gre/modules/ctypes.jsm");
+
+const PREF_APP_UPDATE_CHANNEL = "app.update.channel";
+const PREF_APP_PARTNER_BRANCH = "app.partner.";
+const PREF_DISTRIBUTION_ID = "distribution.id";
+const PREF_DISTRIBUTION_VERSION = "distribution.version";
+
+const URL_PREFIX = "http://localhost/";
+
+const MSG_SHOULD_EQUAL = " should equal the expected value";
+
+updateAppInfo();
+const gAppInfo = getAppInfo();
+const gDefaultPrefBranch = Services.prefs.getDefaultBranch(null);
+
+function setUpdateChannel(aChannel) {
+ gDefaultPrefBranch.setCharPref(PREF_APP_UPDATE_CHANNEL, aChannel);
+}
+
+function getServicePack() {
+ // NOTE: This function is a helper function and not a test. Thus,
+ // it uses throw() instead of do_throw(). Any tests that use this function
+ // should catch exceptions thrown in this function and deal with them
+ // appropriately (usually by calling do_throw).
+ const BYTE = ctypes.uint8_t;
+ const WORD = ctypes.uint16_t;
+ const DWORD = ctypes.uint32_t;
+ const WCHAR = ctypes.char16_t;
+ const BOOL = ctypes.int;
+
+ // This structure is described at:
+ // http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx
+ const SZCSDVERSIONLENGTH = 128;
+ const OSVERSIONINFOEXW = new ctypes.StructType('OSVERSIONINFOEXW',
+ [
+ {dwOSVersionInfoSize: DWORD},
+ {dwMajorVersion: DWORD},
+ {dwMinorVersion: DWORD},
+ {dwBuildNumber: DWORD},
+ {dwPlatformId: DWORD},
+ {szCSDVersion: ctypes.ArrayType(WCHAR, SZCSDVERSIONLENGTH)},
+ {wServicePackMajor: WORD},
+ {wServicePackMinor: WORD},
+ {wSuiteMask: WORD},
+ {wProductType: BYTE},
+ {wReserved: BYTE}
+ ]);
+
+ let kernel32 = ctypes.open("kernel32");
+ try {
+ let GetVersionEx = kernel32.declare("GetVersionExW",
+ ctypes.default_abi,
+ BOOL,
+ OSVERSIONINFOEXW.ptr);
+ let winVer = OSVERSIONINFOEXW();
+ winVer.dwOSVersionInfoSize = OSVERSIONINFOEXW.size;
+
+ if (0 === GetVersionEx(winVer.address())) {
+ // Using "throw" instead of "do_throw" (see NOTE above)
+ throw ("Failure in GetVersionEx (returned 0)");
+ }
+
+ return winVer.wServicePackMajor + "." + winVer.wServicePackMinor;
+ } finally {
+ kernel32.close();
+ }
+}
+
+function getProcArchitecture() {
+ // NOTE: This function is a helper function and not a test. Thus,
+ // it uses throw() instead of do_throw(). Any tests that use this function
+ // should catch exceptions thrown in this function and deal with them
+ // appropriately (usually by calling do_throw).
+ const WORD = ctypes.uint16_t;
+ const DWORD = ctypes.uint32_t;
+
+ // This structure is described at:
+ // http://msdn.microsoft.com/en-us/library/ms724958%28v=vs.85%29.aspx
+ const SYSTEM_INFO = new ctypes.StructType('SYSTEM_INFO',
+ [
+ {wProcessorArchitecture: WORD},
+ {wReserved: WORD},
+ {dwPageSize: DWORD},
+ {lpMinimumApplicationAddress: ctypes.voidptr_t},
+ {lpMaximumApplicationAddress: ctypes.voidptr_t},
+ {dwActiveProcessorMask: DWORD.ptr},
+ {dwNumberOfProcessors: DWORD},
+ {dwProcessorType: DWORD},
+ {dwAllocationGranularity: DWORD},
+ {wProcessorLevel: WORD},
+ {wProcessorRevision: WORD}
+ ]);
+
+ let kernel32 = ctypes.open("kernel32");
+ try {
+ let GetNativeSystemInfo = kernel32.declare("GetNativeSystemInfo",
+ ctypes.default_abi,
+ ctypes.void_t,
+ SYSTEM_INFO.ptr);
+ let sysInfo = SYSTEM_INFO();
+ // Default to unknown
+ sysInfo.wProcessorArchitecture = 0xffff;
+
+ GetNativeSystemInfo(sysInfo.address());
+ switch (sysInfo.wProcessorArchitecture) {
+ case 9:
+ return "x64";
+ case 6:
+ return "IA64";
+ case 0:
+ return "x86";
+ default:
+ // Using "throw" instead of "do_throw" (see NOTE above)
+ throw ("Unknown architecture returned from GetNativeSystemInfo: " + sysInfo.wProcessorArchitecture);
+ }
+ } finally {
+ kernel32.close();
+ }
+}
+
+// Helper function for formatting a url and getting the result we're
+// interested in
+function getResult(url) {
+ url = UpdateUtils.formatUpdateURL(url);
+ return url.substr(URL_PREFIX.length).split("/")[0];
+}
+
+// url constructed with %PRODUCT%
+add_task(function* test_product() {
+ let url = URL_PREFIX + "%PRODUCT%/";
+ Assert.equal(getResult(url), gAppInfo.name,
+ "the url param for %PRODUCT%" + MSG_SHOULD_EQUAL);
+});
+
+// url constructed with %VERSION%
+add_task(function* test_version() {
+ let url = URL_PREFIX + "%VERSION%/";
+ Assert.equal(getResult(url), gAppInfo.version,
+ "the url param for %VERSION%" + MSG_SHOULD_EQUAL);
+});
+
+// url constructed with %BUILD_ID%
+add_task(function* test_build_id() {
+ let url = URL_PREFIX + "%BUILD_ID%/";
+ Assert.equal(getResult(url), gAppInfo.appBuildID,
+ "the url param for %BUILD_ID%" + MSG_SHOULD_EQUAL);
+});
+
+// url constructed with %BUILD_TARGET%
+// XXX TODO - it might be nice if we tested the actual ABI
+add_task(function* test_build_target() {
+ let url = URL_PREFIX + "%BUILD_TARGET%/";
+
+ let abi;
+ try {
+ abi = gAppInfo.XPCOMABI;
+ } catch (e) {
+ do_throw("nsIXULAppInfo:XPCOMABI not defined\n");
+ }
+
+ if (AppConstants.platform == "macosx") {
+ // Mac universal build should report a different ABI than either macppc
+ // or mactel. This is necessary since nsUpdateService.js will set the ABI to
+ // Universal-gcc3 for Mac universal builds.
+ let macutils = Cc["@mozilla.org/xpcom/mac-utils;1"].
+ getService(Ci.nsIMacUtils);
+
+ if (macutils.isUniversalBinary) {
+ abi += "-u-" + macutils.architecturesInBinary;
+ }
+ } else if (AppConstants.platform == "win") {
+ // Windows build should report the CPU architecture that it's running on.
+ abi += "-" + getProcArchitecture();
+ }
+
+ Assert.equal(getResult(url), gAppInfo.OS + "_" + abi,
+ "the url param for %BUILD_TARGET%" + MSG_SHOULD_EQUAL);
+});
+
+// url constructed with %LOCALE%
+// Bug 488936 added the update.locale file that stores the update locale
+add_task(function* test_locale() {
+ // The code that gets the locale accesses the profile which is only available
+ // after calling do_get_profile in xpcshell tests. This prevents an error from
+ // being logged.
+ do_get_profile();
+
+ let url = URL_PREFIX + "%LOCALE%/";
+ Assert.equal(getResult(url), AppConstants.INSTALL_LOCALE,
+ "the url param for %LOCALE%" + MSG_SHOULD_EQUAL);
+});
+
+// url constructed with %CHANNEL%
+add_task(function* test_channel() {
+ let url = URL_PREFIX + "%CHANNEL%/";
+ setUpdateChannel("test_channel");
+ Assert.equal(getResult(url), "test_channel",
+ "the url param for %CHANNEL%" + MSG_SHOULD_EQUAL);
+});
+
+// url constructed with %CHANNEL% with distribution partners
+add_task(function* test_channel_distribution() {
+ let url = URL_PREFIX + "%CHANNEL%/";
+ gDefaultPrefBranch.setCharPref(PREF_APP_PARTNER_BRANCH + "test_partner1",
+ "test_partner1");
+ gDefaultPrefBranch.setCharPref(PREF_APP_PARTNER_BRANCH + "test_partner2",
+ "test_partner2");
+ Assert.equal(getResult(url),
+ "test_channel-cck-test_partner1-test_partner2",
+ "the url param for %CHANNEL%" + MSG_SHOULD_EQUAL);
+});
+
+// url constructed with %PLATFORM_VERSION%
+add_task(function* test_platform_version() {
+ let url = URL_PREFIX + "%PLATFORM_VERSION%/";
+ Assert.equal(getResult(url), gAppInfo.platformVersion,
+ "the url param for %PLATFORM_VERSION%" + MSG_SHOULD_EQUAL);
+});
+
+// url constructed with %OS_VERSION%
+add_task(function* test_os_version() {
+ let url = URL_PREFIX + "%OS_VERSION%/";
+ let osVersion;
+ let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
+ osVersion = sysInfo.getProperty("name") + " " + sysInfo.getProperty("version");
+
+ if (AppConstants.platform == "win") {
+ try {
+ let servicePack = getServicePack();
+ osVersion += "." + servicePack;
+ } catch (e) {
+ do_throw("Failure obtaining service pack: " + e);
+ }
+
+ if ("5.0" === sysInfo.getProperty("version")) { // Win2K
+ osVersion += " (unknown)";
+ } else {
+ try {
+ osVersion += " (" + getProcArchitecture() + ")";
+ } catch (e) {
+ do_throw("Failed to obtain processor architecture: " + e);
+ }
+ }
+ }
+
+ if (osVersion) {
+ try {
+ osVersion += " (" + sysInfo.getProperty("secondaryLibrary") + ")";
+ } catch (e) {
+ // Not all platforms have a secondary widget library, so an error is
+ // nothing to worry about.
+ }
+ osVersion = encodeURIComponent(osVersion);
+ }
+
+ Assert.equal(getResult(url), osVersion,
+ "the url param for %OS_VERSION%" + MSG_SHOULD_EQUAL);
+});
+
+// url constructed with %DISTRIBUTION%
+add_task(function* test_distribution() {
+ let url = URL_PREFIX + "%DISTRIBUTION%/";
+ gDefaultPrefBranch.setCharPref(PREF_DISTRIBUTION_ID, "test_distro");
+ Assert.equal(getResult(url), "test_distro",
+ "the url param for %DISTRIBUTION%" + MSG_SHOULD_EQUAL);
+});
+
+// url constructed with %DISTRIBUTION_VERSION%
+add_task(function* test_distribution_version() {
+ let url = URL_PREFIX + "%DISTRIBUTION_VERSION%/";
+ gDefaultPrefBranch.setCharPref(PREF_DISTRIBUTION_VERSION, "test_distro_version");
+ Assert.equal(getResult(url), "test_distro_version",
+ "the url param for %DISTRIBUTION_VERSION%" + MSG_SHOULD_EQUAL);
+});
+
+add_task(function* test_custom() {
+ Services.prefs.setCharPref("app.update.custom", "custom");
+ let url = URL_PREFIX + "%CUSTOM%/";
+ Assert.equal(getResult(url), "custom",
+ "the url query string for %CUSTOM%" + MSG_SHOULD_EQUAL);
+});
diff --git a/toolkit/modules/tests/xpcshell/test_ZipUtils.js b/toolkit/modules/tests/xpcshell/test_ZipUtils.js
new file mode 100644
index 000000000..71c6884d4
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_ZipUtils.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const ARCHIVE = "zips/zen.zip";
+const SUBDIR = "zen";
+const SYMLINK = "beyond_link";
+const ENTRIES = ["beyond.txt", SYMLINK, "waterwood.txt"];
+
+Components.utils.import("resource://gre/modules/ZipUtils.jsm");
+Components.utils.import("resource://gre/modules/FileUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const archive = do_get_file(ARCHIVE, false);
+const dir = do_get_profile().clone();
+dir.append("test_ZipUtils");
+
+function run_test() {
+ run_next_test();
+}
+
+function ensureExtracted(target) {
+ target.append(SUBDIR);
+ do_check_true(target.exists());
+
+ for (let i = 0; i < ENTRIES.length; i++) {
+ let entry = target.clone();
+ entry.append(ENTRIES[i]);
+ do_print("ENTRY " + entry.path);
+ do_check_true(entry.exists());
+ }
+}
+
+function ensureHasSymlink(target) {
+ // Just bail out if running on Windows, since symlinks do not exists there.
+ if (Services.appinfo.OS === "WINNT") {
+ return;
+ }
+
+ let entry = target.clone();
+ entry.append(SYMLINK);
+
+ do_print("ENTRY " + entry.path);
+ do_check_true(entry.exists());
+ do_check_true(entry.isSymlink());
+}
+
+add_task(function test_extractFiles() {
+ let target = dir.clone();
+ target.append("test_extractFiles");
+
+ try {
+ ZipUtils.extractFiles(archive, target);
+ } catch (e) {
+ do_throw("Failed to extract synchronously!");
+ }
+
+ ensureExtracted(target);
+ ensureHasSymlink(target);
+});
+
+add_task(function* test_extractFilesAsync() {
+ let target = dir.clone();
+ target.append("test_extractFilesAsync");
+ target.create(Components.interfaces.nsIFile.DIRECTORY_TYPE,
+ FileUtils.PERMS_DIRECTORY);
+
+ yield ZipUtils.extractFilesAsync(archive, target).then(
+ function success() {
+ do_print("SUCCESS");
+ ensureExtracted(target);
+ },
+ function failure() {
+ do_print("FAILURE");
+ do_throw("Failed to extract asynchronously!");
+ }
+ );
+});
diff --git a/toolkit/modules/tests/xpcshell/test_client_id.js b/toolkit/modules/tests/xpcshell/test_client_id.js
new file mode 100644
index 000000000..10ef2a3ea
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_client_id.js
@@ -0,0 +1,95 @@
+/* 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;
+
+Cu.import("resource://gre/modules/ClientID.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+function run_test() {
+ do_get_profile();
+ run_next_test();
+}
+
+add_task(function* () {
+ const drsPath = OS.Path.join(OS.Constants.Path.profileDir, "datareporting", "state.json");
+ const fhrDir = OS.Path.join(OS.Constants.Path.profileDir, "healthreport");
+ const fhrPath = OS.Path.join(fhrDir, "state.json");
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+ const invalidIDs = [-1, 0.5, "INVALID-UUID", true, "", "3d1e1560-682a-4043-8cf2-aaaaaaaaaaaZ"];
+ const PREF_CACHED_CLIENTID = "toolkit.telemetry.cachedClientID";
+
+ yield OS.File.makeDir(fhrDir);
+
+ // Check that we are importing the FHR client ID.
+ let clientID = CommonUtils.generateUUID();
+ yield CommonUtils.writeJSON({clientID: clientID}, fhrPath);
+ Assert.equal(clientID, yield ClientID.getClientID());
+
+ // We should persist the ID in DRS now and not pick up a differing ID from FHR.
+ yield ClientID._reset();
+ yield CommonUtils.writeJSON({clientID: CommonUtils.generateUUID()}, fhrPath);
+ Assert.equal(clientID, yield ClientID.getClientID());
+
+ // We should be guarded against broken FHR data.
+ for (let invalidID of invalidIDs) {
+ yield ClientID._reset();
+ yield OS.File.remove(drsPath);
+ yield CommonUtils.writeJSON({clientID: invalidID}, fhrPath);
+ clientID = yield ClientID.getClientID();
+ Assert.equal(typeof(clientID), 'string');
+ Assert.ok(uuidRegex.test(clientID));
+ }
+
+ // We should be guarded against invalid FHR json.
+ yield ClientID._reset();
+ yield OS.File.remove(drsPath);
+ yield OS.File.writeAtomic(fhrPath, "abcd", {encoding: "utf-8", tmpPath: fhrPath + ".tmp"});
+ clientID = yield ClientID.getClientID();
+ Assert.equal(typeof(clientID), 'string');
+ Assert.ok(uuidRegex.test(clientID));
+
+ // We should be guarded against broken DRS data too and fall back to loading
+ // the FHR ID.
+ for (let invalidID of invalidIDs) {
+ yield ClientID._reset();
+ clientID = CommonUtils.generateUUID();
+ yield CommonUtils.writeJSON({clientID: clientID}, fhrPath);
+ yield CommonUtils.writeJSON({clientID: invalidID}, drsPath);
+ Assert.equal(clientID, yield ClientID.getClientID());
+ }
+
+ // We should be guarded against invalid DRS json too.
+ yield ClientID._reset();
+ yield OS.File.remove(fhrPath);
+ yield OS.File.writeAtomic(drsPath, "abcd", {encoding: "utf-8", tmpPath: drsPath + ".tmp"});
+ clientID = yield ClientID.getClientID();
+ Assert.equal(typeof(clientID), 'string');
+ Assert.ok(uuidRegex.test(clientID));
+
+ // If both the FHR and DSR data are broken, we should end up with a new client ID.
+ for (let invalidID of invalidIDs) {
+ yield ClientID._reset();
+ yield CommonUtils.writeJSON({clientID: invalidID}, fhrPath);
+ yield CommonUtils.writeJSON({clientID: invalidID}, drsPath);
+ clientID = yield ClientID.getClientID();
+ Assert.equal(typeof(clientID), 'string');
+ Assert.ok(uuidRegex.test(clientID));
+ }
+
+ // Assure that cached IDs are being checked for validity.
+ for (let invalidID of invalidIDs) {
+ yield ClientID._reset();
+ Preferences.set(PREF_CACHED_CLIENTID, invalidID);
+ let cachedID = ClientID.getCachedClientID();
+ Assert.strictEqual(cachedID, null, "ClientID should ignore invalid cached IDs");
+ let prefID = Preferences.get(PREF_CACHED_CLIENTID, null);
+ Assert.strictEqual(prefID, null, "ClientID should reset invalid cached IDs");
+ }
+});
diff --git a/toolkit/modules/tests/xpcshell/test_jsesc.js b/toolkit/modules/tests/xpcshell/test_jsesc.js
new file mode 100644
index 000000000..0c6cbba69
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_jsesc.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/third_party/jsesc/jsesc.js");
+
+function run_test() {
+ do_check_eq(jsesc("teééést", {lowercaseHex: true}), "te\\xe9\\xe9\\xe9st");
+ do_check_eq(jsesc("teééést", {lowercaseHex: false}), "te\\xE9\\xE9\\xE9st");
+}
diff --git a/toolkit/modules/tests/xpcshell/test_propertyListsUtils.js b/toolkit/modules/tests/xpcshell/test_propertyListsUtils.js
new file mode 100644
index 000000000..9ccf50b73
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_propertyListsUtils.js
@@ -0,0 +1,106 @@
+/* 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/. */
+"use strict";
+
+Components.utils.import("resource://gre/modules/PropertyListUtils.jsm");
+
+function checkValue(aPropertyListObject, aType, aValue) {
+ do_check_eq(PropertyListUtils.getObjectType(aPropertyListObject), aType);
+ if (aValue !== undefined) {
+ // Perform strict equality checks until Bug 714467 is fixed.
+ let strictEqualityCheck = function(a, b) {
+ do_check_eq(typeof(a), typeof(b));
+ do_check_eq(a, b);
+ };
+
+ if (typeof(aPropertyListObject) == "object")
+ strictEqualityCheck(aPropertyListObject.valueOf(), aValue.valueOf());
+ else
+ strictEqualityCheck(aPropertyListObject, aValue);
+ }
+}
+
+function checkLazyGetterValue(aObject, aPropertyName, aType, aValue) {
+ let descriptor = Object.getOwnPropertyDescriptor(aObject, aPropertyName);
+ do_check_eq(typeof(descriptor.get), "function");
+ do_check_eq(typeof(descriptor.value), "undefined");
+ checkValue(aObject[aPropertyName], aType, aValue);
+ descriptor = Object.getOwnPropertyDescriptor(aObject, aPropertyName);
+ do_check_eq(typeof(descriptor.get), "undefined");
+ do_check_neq(typeof(descriptor.value), "undefined");
+}
+
+function checkMainPropertyList(aPropertyListRoot) {
+ const PRIMITIVE = PropertyListUtils.TYPE_PRIMITIVE;
+
+ checkValue(aPropertyListRoot, PropertyListUtils.TYPE_DICTIONARY);
+
+ // Check .has()
+ Assert.ok(aPropertyListRoot.has("Boolean"));
+ Assert.ok(!aPropertyListRoot.has("Nonexistent"));
+
+ checkValue(aPropertyListRoot.get("Boolean"), PRIMITIVE, false);
+
+ let array = aPropertyListRoot.get("Array");
+ checkValue(array, PropertyListUtils.TYPE_ARRAY);
+ do_check_eq(array.length, 8);
+
+ // Test both long and short values, since binary property lists store
+ // long values a little bit differently (see readDataLengthAndOffset).
+
+ // Short ASCII string
+ checkLazyGetterValue(array, 0, PRIMITIVE, "abc");
+ // Long ASCII string
+ checkLazyGetterValue(array, 1, PRIMITIVE, new Array(1001).join("a"));
+ // Short unicode string
+ checkLazyGetterValue(array, 2, PRIMITIVE, "\u05D0\u05D0\u05D0");
+ // Long unicode string
+ checkLazyGetterValue(array, 3, PRIMITIVE, new Array(1001).join("\u05D0"));
+ // Unicode surrogate pair
+ checkLazyGetterValue(array, 4, PRIMITIVE,
+ "\uD800\uDC00\uD800\uDC00\uD800\uDC00");
+
+ // Date
+ checkLazyGetterValue(array, 5, PropertyListUtils.TYPE_DATE,
+ new Date("2011-12-31T11:15:23Z"));
+
+ // Data
+ checkLazyGetterValue(array, 6, PropertyListUtils.TYPE_UINT8_ARRAY);
+ let dataAsString = Array.from(array[6]).map(b => String.fromCharCode(b)).join("");
+ do_check_eq(dataAsString, "2011-12-31T11:15:33Z");
+
+ // Dict
+ let dict = array[7];
+ checkValue(dict, PropertyListUtils.TYPE_DICTIONARY);
+ checkValue(dict.get("Negative Number"), PRIMITIVE, -400);
+ checkValue(dict.get("Real Number"), PRIMITIVE, 2.71828183);
+ checkValue(dict.get("Big Int"),
+ PropertyListUtils.TYPE_INT64,
+ "9007199254740993");
+ checkValue(dict.get("Negative Big Int"),
+ PropertyListUtils.TYPE_INT64,
+ "-9007199254740993");
+}
+
+function readPropertyList(aFile, aCallback) {
+ PropertyListUtils.read(aFile, function(aPropertyListRoot) {
+ // Null root indicates failure to read property list.
+ // Note: It is important not to run do_check_n/eq directly on Dict and array
+ // objects, because it cases their toString to get invoked, doing away with
+ // all the lazy getter we'd like to test later.
+ do_check_true(aPropertyListRoot !== null);
+ aCallback(aPropertyListRoot);
+ run_next_test();
+ });
+}
+
+function run_test() {
+ add_test(readPropertyList.bind(this,
+ do_get_file("propertyLists/bug710259_propertyListBinary.plist", false),
+ checkMainPropertyList));
+ add_test(readPropertyList.bind(this,
+ do_get_file("propertyLists/bug710259_propertyListXML.plist", false),
+ checkMainPropertyList));
+ run_next_test();
+}
diff --git a/toolkit/modules/tests/xpcshell/test_readCertPrefs.js b/toolkit/modules/tests/xpcshell/test_readCertPrefs.js
new file mode 100644
index 000000000..837a9912a
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_readCertPrefs.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/CertUtils.jsm");
+
+const PREF_PREFIX = "certutils.certs.";
+
+function run_test() {
+ run_next_test();
+}
+
+function resetPrefs() {
+ var prefs = Services.prefs.getChildList(PREF_PREFIX);
+ prefs.forEach(Services.prefs.clearUserPref);
+}
+
+function attributes_match(aCert, aExpected) {
+ if (Object.keys(aCert).length != Object.keys(aExpected).length)
+ return false;
+
+ for (var attribute in aCert) {
+ if (!(attribute in aExpected))
+ return false;
+ if (aCert[attribute] != aExpected[attribute])
+ return false;
+ }
+
+ return true;
+}
+
+function test_results(aCerts, aExpected) {
+ do_check_eq(aCerts.length, aExpected.length);
+
+ for (var i = 0; i < aCerts.length; i++) {
+ if (!attributes_match(aCerts[i], aExpected[i])) {
+ dump("Attributes for certificate " + (i + 1) + " did not match expected attributes\n");
+ dump("Saw: " + aCerts[i].toSource() + "\n");
+ dump("Expected: " + aExpected[i].toSource() + "\n");
+ do_check_true(false);
+ }
+ }
+}
+
+add_test(function test_singleCert() {
+ Services.prefs.setCharPref(PREF_PREFIX + "1.attribute1", "foo");
+ Services.prefs.setCharPref(PREF_PREFIX + "1.attribute2", "bar");
+
+ var certs = readCertPrefs(PREF_PREFIX);
+ test_results(certs, [{
+ attribute1: "foo",
+ attribute2: "bar"
+ }]);
+
+ resetPrefs();
+ run_next_test();
+});
+
+add_test(function test_multipleCert() {
+ Services.prefs.setCharPref(PREF_PREFIX + "1.md5Fingerprint", "cf84a9a2a804e021f27cb5128fe151f4");
+ Services.prefs.setCharPref(PREF_PREFIX + "1.nickname", "1st cert");
+ Services.prefs.setCharPref(PREF_PREFIX + "2.md5Fingerprint", "9441051b7eb50e5ca2226095af710c1a");
+ Services.prefs.setCharPref(PREF_PREFIX + "2.nickname", "2nd cert");
+
+ var certs = readCertPrefs(PREF_PREFIX);
+ test_results(certs, [{
+ md5Fingerprint: "cf84a9a2a804e021f27cb5128fe151f4",
+ nickname: "1st cert"
+ }, {
+ md5Fingerprint: "9441051b7eb50e5ca2226095af710c1a",
+ nickname: "2nd cert"
+ }]);
+
+ resetPrefs();
+ run_next_test();
+});
+
+add_test(function test_skippedCert() {
+ Services.prefs.setCharPref(PREF_PREFIX + "1.issuerName", "Mozilla");
+ Services.prefs.setCharPref(PREF_PREFIX + "1.nickname", "1st cert");
+ Services.prefs.setCharPref(PREF_PREFIX + "2.issuerName", "Top CA");
+ Services.prefs.setCharPref(PREF_PREFIX + "2.nickname", "2nd cert");
+ Services.prefs.setCharPref(PREF_PREFIX + "4.issuerName", "Unknown CA");
+ Services.prefs.setCharPref(PREF_PREFIX + "4.nickname", "Ignored cert");
+
+ var certs = readCertPrefs(PREF_PREFIX);
+ test_results(certs, [{
+ issuerName: "Mozilla",
+ nickname: "1st cert"
+ }, {
+ issuerName: "Top CA",
+ nickname: "2nd cert"
+ }]);
+
+ resetPrefs();
+ run_next_test();
+});
diff --git a/toolkit/modules/tests/xpcshell/test_servicerequest_xhr.js b/toolkit/modules/tests/xpcshell/test_servicerequest_xhr.js
new file mode 100644
index 000000000..b3c8a443e
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_servicerequest_xhr.js
@@ -0,0 +1,25 @@
+/* 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;
+
+Cu.import("resource://gre/modules/ServiceRequest.jsm");
+
+add_task(function* test_tls_conservative() {
+ const request = new ServiceRequest();
+ request.open("GET", "http://example.com", false);
+
+ const sr_channel = request.channel.QueryInterface(Ci.nsIHttpChannelInternal);
+ ok(("beConservative" in sr_channel), "TLS setting is present in SR channel");
+ ok(sr_channel.beConservative, "TLS setting in request channel is set to conservative for SR");
+
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", "http://example.com", false);
+
+ const xhr_channel = xhr.channel.QueryInterface(Ci.nsIHttpChannelInternal);
+ ok(("beConservative" in xhr_channel), "TLS setting is present in XHR channel");
+ ok(!xhr_channel.beConservative, "TLS setting in request channel is not set to conservative for XHR");
+
+});
diff --git a/toolkit/modules/tests/xpcshell/test_session_recorder.js b/toolkit/modules/tests/xpcshell/test_session_recorder.js
new file mode 100644
index 000000000..dd9159c6e
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_session_recorder.js
@@ -0,0 +1,306 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/SessionRecorder.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://services-common/utils.js");
+
+
+function run_test() {
+ run_next_test();
+}
+
+function monkeypatchStartupInfo(recorder, start=new Date(), offset=500) {
+ Object.defineProperty(recorder, "_getStartupInfo", {
+ value: function _getStartupInfo() {
+ return {
+ process: start,
+ main: new Date(start.getTime() + offset),
+ firstPaint: new Date(start.getTime() + 2 * offset),
+ sessionRestored: new Date(start.getTime() + 3 * offset),
+ };
+ }
+ });
+}
+
+function sleep(wait) {
+ let deferred = Promise.defer();
+
+ let timer = CommonUtils.namedTimer(function onTimer() {
+ deferred.resolve();
+ }, wait, deferred.promise, "_sleepTimer");
+
+ return deferred.promise;
+}
+
+function getRecorder(name, start, offset) {
+ let recorder = new SessionRecorder("testing." + name + ".");
+ monkeypatchStartupInfo(recorder, start, offset);
+
+ return recorder;
+}
+
+add_test(function test_basic() {
+ let recorder = getRecorder("basic");
+ recorder.onStartup();
+ recorder.onShutdown();
+
+ run_next_test();
+});
+
+add_task(function* test_current_properties() {
+ let now = new Date();
+ let recorder = getRecorder("current_properties", now);
+ yield sleep(25);
+ recorder.onStartup();
+
+ do_check_eq(recorder.startDate.getTime(), now.getTime());
+ do_check_eq(recorder.activeTicks, 0);
+ do_check_true(recorder.fineTotalTime > 0);
+ do_check_eq(recorder.main, 500);
+ do_check_eq(recorder.firstPaint, 1000);
+ do_check_eq(recorder.sessionRestored, 1500);
+
+ recorder.incrementActiveTicks();
+ do_check_eq(recorder.activeTicks, 1);
+
+ recorder._startDate = new Date(Date.now() - 1000);
+ recorder.updateTotalTime();
+ do_check_eq(recorder.totalTime, 1);
+
+ recorder.onShutdown();
+});
+
+// If startup info isn't present yet, we should install a timer and get
+// it eventually.
+add_task(function* test_current_availability() {
+ let recorder = new SessionRecorder("testing.current_availability.");
+ let now = new Date();
+
+ Object.defineProperty(recorder, "_getStartupInfo", {
+ value: function _getStartupInfo() {
+ return {
+ process: now,
+ main: new Date(now.getTime() + 500),
+ firstPaint: new Date(now.getTime() + 1000),
+ };
+ },
+ writable: true,
+ });
+
+ Object.defineProperty(recorder, "STARTUP_RETRY_INTERVAL_MS", {
+ value: 100,
+ });
+
+ let oldRecord = recorder.recordStartupFields;
+ let recordCount = 0;
+
+ Object.defineProperty(recorder, "recordStartupFields", {
+ value: function () {
+ recordCount++;
+ return oldRecord.call(recorder);
+ }
+ });
+
+ do_check_null(recorder._timer);
+ recorder.onStartup();
+ do_check_eq(recordCount, 1);
+ do_check_eq(recorder.sessionRestored, -1);
+ do_check_neq(recorder._timer, null);
+
+ yield sleep(125);
+ do_check_eq(recordCount, 2);
+ yield sleep(100);
+ do_check_eq(recordCount, 3);
+ do_check_eq(recorder.sessionRestored, -1);
+
+ monkeypatchStartupInfo(recorder, now);
+ yield sleep(100);
+ do_check_eq(recordCount, 4);
+ do_check_eq(recorder.sessionRestored, 1500);
+
+ // The timer should be removed and we should not fire again.
+ do_check_null(recorder._timer);
+ yield sleep(100);
+ do_check_eq(recordCount, 4);
+
+ recorder.onShutdown();
+});
+
+add_test(function test_timer_clear_on_shutdown() {
+ let recorder = new SessionRecorder("testing.timer_clear_on_shutdown.");
+ let now = new Date();
+
+ Object.defineProperty(recorder, "_getStartupInfo", {
+ value: function _getStartupInfo() {
+ return {
+ process: now,
+ main: new Date(now.getTime() + 500),
+ firstPaint: new Date(now.getTime() + 1000),
+ };
+ },
+ });
+
+ do_check_null(recorder._timer);
+ recorder.onStartup();
+ do_check_neq(recorder._timer, null);
+
+ recorder.onShutdown();
+ do_check_null(recorder._timer);
+
+ run_next_test();
+});
+
+add_task(function* test_previous_clean() {
+ let now = new Date();
+ let recorder = getRecorder("previous_clean", now);
+ yield sleep(25);
+ recorder.onStartup();
+
+ recorder.incrementActiveTicks();
+ recorder.incrementActiveTicks();
+
+ yield sleep(25);
+ recorder.onShutdown();
+
+ let total = recorder.totalTime;
+
+ yield sleep(25);
+ let now2 = new Date();
+ let recorder2 = getRecorder("previous_clean", now2, 100);
+ yield sleep(25);
+ recorder2.onStartup();
+
+ do_check_eq(recorder2.startDate.getTime(), now2.getTime());
+ do_check_eq(recorder2.main, 100);
+ do_check_eq(recorder2.firstPaint, 200);
+ do_check_eq(recorder2.sessionRestored, 300);
+
+ let sessions = recorder2.getPreviousSessions();
+ do_check_eq(Object.keys(sessions).length, 1);
+ do_check_true(0 in sessions);
+ let session = sessions[0];
+ do_check_true(session.clean);
+ do_check_eq(session.startDate.getTime(), now.getTime());
+ do_check_eq(session.main, 500);
+ do_check_eq(session.firstPaint, 1000);
+ do_check_eq(session.sessionRestored, 1500);
+ do_check_eq(session.totalTime, total);
+ do_check_eq(session.activeTicks, 2);
+
+ recorder2.onShutdown();
+});
+
+add_task(function* test_previous_abort() {
+ let now = new Date();
+ let recorder = getRecorder("previous_abort", now);
+ yield sleep(25);
+ recorder.onStartup();
+ recorder.incrementActiveTicks();
+ yield sleep(25);
+ let total = recorder.totalTime;
+ yield sleep(25);
+
+ let now2 = new Date();
+ let recorder2 = getRecorder("previous_abort", now2);
+ yield sleep(25);
+ recorder2.onStartup();
+
+ let sessions = recorder2.getPreviousSessions();
+ do_check_eq(Object.keys(sessions).length, 1);
+ do_check_true(0 in sessions);
+ let session = sessions[0];
+ do_check_false(session.clean);
+ do_check_eq(session.totalTime, total);
+
+ recorder.onShutdown();
+ recorder2.onShutdown();
+});
+
+add_task(function* test_multiple_sessions() {
+ for (let i = 0; i < 10; i++) {
+ let recorder = getRecorder("multiple_sessions");
+ yield sleep(25);
+ recorder.onStartup();
+ for (let j = 0; j < i; j++) {
+ recorder.incrementActiveTicks();
+ }
+ yield sleep(25);
+ recorder.onShutdown();
+ yield sleep(25);
+ }
+
+ let recorder = getRecorder("multiple_sessions");
+ recorder.onStartup();
+
+ let sessions = recorder.getPreviousSessions();
+ do_check_eq(Object.keys(sessions).length, 10);
+
+ for (let [i, session] of Object.entries(sessions)) {
+ do_check_eq(session.activeTicks, i);
+
+ if (i > 0) {
+ do_check_true(session.startDate.getTime() > sessions[i-1].startDate.getTime());
+ }
+ }
+
+ // #6 is preserved since >=.
+ let threshold = sessions[6].startDate;
+ recorder.pruneOldSessions(threshold);
+
+ sessions = recorder.getPreviousSessions();
+ do_check_eq(Object.keys(sessions).length, 4);
+
+ recorder.pruneOldSessions(threshold);
+ sessions = recorder.getPreviousSessions();
+ do_check_eq(Object.keys(sessions).length, 4);
+ do_check_eq(recorder._prunedIndex, 5);
+
+ recorder.onShutdown();
+});
+
+add_task(function* test_record_activity() {
+ let recorder = getRecorder("record_activity");
+ yield sleep(25);
+ recorder.onStartup();
+ let total = recorder.totalTime;
+ yield sleep(25);
+
+ for (let i = 0; i < 3; i++) {
+ Services.obs.notifyObservers(null, "user-interaction-active", null);
+ yield sleep(25);
+ do_check_true(recorder.fineTotalTime > total);
+ total = recorder.fineTotalTime;
+ }
+
+ do_check_eq(recorder.activeTicks, 3);
+
+ // Now send inactive. We should increment total time but not active.
+ Services.obs.notifyObservers(null, "user-interaction-inactive", null);
+ do_check_eq(recorder.activeTicks, 3);
+ do_check_true(recorder.fineTotalTime > total);
+ total = recorder.fineTotalTime;
+ yield sleep(25);
+
+ // If we send active again, this should be counted as inactive.
+ Services.obs.notifyObservers(null, "user-interaction-active", null);
+ do_check_eq(recorder.activeTicks, 3);
+ do_check_true(recorder.fineTotalTime > total);
+ total = recorder.fineTotalTime;
+ yield sleep(25);
+
+ // If we send active again, this should be counted as active.
+ Services.obs.notifyObservers(null, "user-interaction-active", null);
+ do_check_eq(recorder.activeTicks, 4);
+
+ Services.obs.notifyObservers(null, "user-interaction-active", null);
+ do_check_eq(recorder.activeTicks, 5);
+
+ recorder.onShutdown();
+});
+
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);
+});
diff --git a/toolkit/modules/tests/xpcshell/test_sqlite_shutdown.js b/toolkit/modules/tests/xpcshell/test_sqlite_shutdown.js
new file mode 100644
index 000000000..b97fd8558
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_sqlite_shutdown.js
@@ -0,0 +1,122 @@
+/* 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/osfile.jsm");
+ // OS.File doesn't like to be first imported during shutdown
+Cu.import("resource://gre/modules/Sqlite.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AsyncShutdown.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+
+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 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;
+}
+
+function run_test() {
+ run_next_test();
+}
+
+
+//
+// ----------- Don't add a test after this one, as it shuts down Sqlite.jsm
+//
+add_task(function* test_shutdown_clients() {
+ do_print("Ensuring that Sqlite.jsm doesn't shutdown before its clients");
+
+ let assertions = [];
+
+ let sleepStarted = false;
+ let sleepComplete = false;
+ Sqlite.shutdown.addBlocker("test_sqlite.js shutdown blocker (sleep)",
+ Task.async(function*() {
+ sleepStarted = true;
+ yield sleep(100);
+ sleepComplete = true;
+ }));
+ assertions.push({name: "sleepStarted", value: () => sleepStarted});
+ assertions.push({name: "sleepComplete", value: () => sleepComplete});
+
+ Sqlite.shutdown.addBlocker("test_sqlite.js shutdown blocker (immediate)",
+ true);
+
+ let dbOpened = false;
+ let dbClosed = false;
+
+ Sqlite.shutdown.addBlocker("test_sqlite.js shutdown blocker (open a connection during shutdown)",
+ Task.async(function*() {
+ let db = yield getDummyDatabase("opened during shutdown");
+ dbOpened = true;
+ db.close().then(
+ () => dbClosed = true
+ ); // Don't wait for this task to complete, Sqlite.jsm must wait automatically
+ }));
+
+ assertions.push({name: "dbOpened", value: () => dbOpened});
+ assertions.push({name: "dbClosed", value: () => dbClosed});
+
+ do_print("Now shutdown Sqlite.jsm synchronously");
+ Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
+ AsyncShutdown.profileBeforeChange._trigger();
+ Services.prefs.clearUserPref("toolkit.asyncshutdown.testing");
+
+
+ for (let {name, value} of assertions) {
+ do_print("Checking: " + name);
+ do_check_true(value());
+ }
+
+ do_print("Ensure that we cannot open databases anymore");
+ let exn;
+ try {
+ yield getDummyDatabase("opened after shutdown");
+ } catch (ex) {
+ exn = ex;
+ }
+ do_check_true(!!exn);
+ do_check_true(exn.message.indexOf("Sqlite.jsm has been shutdown") != -1);
+});
diff --git a/toolkit/modules/tests/xpcshell/test_task.js b/toolkit/modules/tests/xpcshell/test_task.js
new file mode 100644
index 000000000..fdcd56514
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_task.js
@@ -0,0 +1,642 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the Task.jsm module.
+ */
+
+////////////////////////////////////////////////////////////////////////////////
+/// Globals
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+/**
+ * Returns a promise that will be resolved with the given value, when an event
+ * posted on the event loop of the main thread is processed.
+ */
+function promiseResolvedLater(aValue) {
+ let deferred = Promise.defer();
+ Services.tm.mainThread.dispatch(() => deferred.resolve(aValue),
+ Ci.nsIThread.DISPATCH_NORMAL);
+ return deferred.promise;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+/// Tests
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_test(function test_normal()
+{
+ Task.spawn(function () {
+ let result = yield Promise.resolve("Value");
+ for (let i = 0; i < 3; i++) {
+ result += yield promiseResolvedLater("!");
+ }
+ throw new Task.Result("Task result: " + result);
+ }).then(function (result) {
+ do_check_eq("Task result: Value!!!", result);
+ run_next_test();
+ }, function (ex) {
+ do_throw("Unexpected error: " + ex);
+ });
+});
+
+add_test(function test_exceptions()
+{
+ Task.spawn(function () {
+ try {
+ yield Promise.reject("Rejection result by promise.");
+ do_throw("Exception expected because the promise was rejected.");
+ } catch (ex) {
+ // We catch this exception now, we will throw a different one later.
+ do_check_eq("Rejection result by promise.", ex);
+ }
+ throw new Error("Exception uncaught by task.");
+ }).then(function (result) {
+ do_throw("Unexpected success!");
+ }, function (ex) {
+ do_check_eq("Exception uncaught by task.", ex.message);
+ run_next_test();
+ });
+});
+
+add_test(function test_recursion()
+{
+ function task_fibonacci(n) {
+ throw new Task.Result(n < 2 ? n : (yield task_fibonacci(n - 1)) +
+ (yield task_fibonacci(n - 2)));
+ };
+
+ Task.spawn(task_fibonacci(6)).then(function (result) {
+ do_check_eq(8, result);
+ run_next_test();
+ }, function (ex) {
+ do_throw("Unexpected error: " + ex);
+ });
+});
+
+add_test(function test_spawn_primitive()
+{
+ function fibonacci(n) {
+ return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
+ };
+
+ // Polymorphism between task and non-task functions (see "test_recursion").
+ Task.spawn(fibonacci(6)).then(function (result) {
+ do_check_eq(8, result);
+ run_next_test();
+ }, function (ex) {
+ do_throw("Unexpected error: " + ex);
+ });
+});
+
+add_test(function test_spawn_function()
+{
+ Task.spawn(function () {
+ return "This is not a generator.";
+ }).then(function (result) {
+ do_check_eq("This is not a generator.", result);
+ run_next_test();
+ }, function (ex) {
+ do_throw("Unexpected error: " + ex);
+ });
+});
+
+add_test(function test_spawn_function_this()
+{
+ Task.spawn(function () {
+ return this;
+ }).then(function (result) {
+ // Since the task function wasn't defined in strict mode, its "this" object
+ // should be the same as the "this" object in this function, i.e. the global
+ // object.
+ do_check_eq(result, this);
+ run_next_test();
+ }, function (ex) {
+ do_throw("Unexpected error: " + ex);
+ });
+});
+
+add_test(function test_spawn_function_this_strict()
+{
+ "use strict";
+ Task.spawn(function () {
+ return this;
+ }).then(function (result) {
+ // Since the task function was defined in strict mode, its "this" object
+ // should be undefined.
+ do_check_eq(typeof(result), "undefined");
+ run_next_test();
+ }, function (ex) {
+ do_throw("Unexpected error: " + ex);
+ });
+});
+
+add_test(function test_spawn_function_returning_promise()
+{
+ Task.spawn(function () {
+ return promiseResolvedLater("Resolution value.");
+ }).then(function (result) {
+ do_check_eq("Resolution value.", result);
+ run_next_test();
+ }, function (ex) {
+ do_throw("Unexpected error: " + ex);
+ });
+});
+
+add_test(function test_spawn_function_exceptions()
+{
+ Task.spawn(function () {
+ throw new Error("Exception uncaught by task.");
+ }).then(function (result) {
+ do_throw("Unexpected success!");
+ }, function (ex) {
+ do_check_eq("Exception uncaught by task.", ex.message);
+ run_next_test();
+ });
+});
+
+add_test(function test_spawn_function_taskresult()
+{
+ Task.spawn(function () {
+ throw new Task.Result("Task result");
+ }).then(function (result) {
+ do_check_eq("Task result", result);
+ run_next_test();
+ }, function (ex) {
+ do_throw("Unexpected error: " + ex);
+ });
+});
+
+add_test(function test_yielded_undefined()
+{
+ Task.spawn(function () {
+ yield;
+ throw new Task.Result("We continued correctly.");
+ }).then(function (result) {
+ do_check_eq("We continued correctly.", result);
+ run_next_test();
+ }, function (ex) {
+ do_throw("Unexpected error: " + ex);
+ });
+});
+
+add_test(function test_yielded_primitive()
+{
+ Task.spawn(function () {
+ throw new Task.Result("Primitive " + (yield "value."));
+ }).then(function (result) {
+ do_check_eq("Primitive value.", result);
+ run_next_test();
+ }, function (ex) {
+ do_throw("Unexpected error: " + ex);
+ });
+});
+
+add_test(function test_star_normal()
+{
+ Task.spawn(function* () {
+ let result = yield Promise.resolve("Value");
+ for (let i = 0; i < 3; i++) {
+ result += yield promiseResolvedLater("!");
+ }
+ return "Task result: " + result;
+ }).then(function (result) {
+ do_check_eq("Task result: Value!!!", result);
+ run_next_test();
+ }, function (ex) {
+ do_throw("Unexpected error: " + ex);
+ });
+});
+
+add_test(function test_star_exceptions()
+{
+ Task.spawn(function* () {
+ try {
+ yield Promise.reject("Rejection result by promise.");
+ do_throw("Exception expected because the promise was rejected.");
+ } catch (ex) {
+ // We catch this exception now, we will throw a different one later.
+ do_check_eq("Rejection result by promise.", ex);
+ }
+ throw new Error("Exception uncaught by task.");
+ }).then(function (result) {
+ do_throw("Unexpected success!");
+ }, function (ex) {
+ do_check_eq("Exception uncaught by task.", ex.message);
+ run_next_test();
+ });
+});
+
+add_test(function test_star_recursion()
+{
+ function* task_fibonacci(n) {
+ return n < 2 ? n : (yield task_fibonacci(n - 1)) +
+ (yield task_fibonacci(n - 2));
+ };
+
+ Task.spawn(task_fibonacci(6)).then(function (result) {
+ do_check_eq(8, result);
+ run_next_test();
+ }, function (ex) {
+ do_throw("Unexpected error: " + ex);
+ });
+});
+
+add_test(function test_mixed_legacy_and_star()
+{
+ Task.spawn(function* () {
+ return yield (function() {
+ throw new Task.Result(yield 5);
+ })();
+ }).then(function (result) {
+ do_check_eq(5, result);
+ run_next_test();
+ }, function (ex) {
+ do_throw("Unexpected error: " + ex);
+ });
+});
+
+add_test(function test_async_function_from_generator()
+{
+ Task.spawn(function* () {
+ let object = {
+ asyncFunction: Task.async(function* (param) {
+ do_check_eq(this, object);
+ return param;
+ })
+ };
+
+ // Ensure the async function returns a promise that resolves as expected.
+ do_check_eq((yield object.asyncFunction(1)), 1);
+
+ // Ensure a second call to the async function also returns such a promise.
+ do_check_eq((yield object.asyncFunction(3)), 3);
+ }).then(function () {
+ run_next_test();
+ }, function (ex) {
+ do_throw("Unexpected error: " + ex);
+ });
+});
+
+add_test(function test_async_function_from_function()
+{
+ Task.spawn(function* () {
+ return Task.spawn(function* () {
+ let object = {
+ asyncFunction: Task.async(function (param) {
+ do_check_eq(this, object);
+ return param;
+ })
+ };
+
+ // Ensure the async function returns a promise that resolves as expected.
+ do_check_eq((yield object.asyncFunction(5)), 5);
+
+ // Ensure a second call to the async function also returns such a promise.
+ do_check_eq((yield object.asyncFunction(7)), 7);
+ });
+ }).then(function () {
+ run_next_test();
+ }, function (ex) {
+ do_throw("Unexpected error: " + ex);
+ });
+});
+
+add_test(function test_async_function_that_throws_rejects_promise()
+{
+ Task.spawn(function* () {
+ let object = {
+ asyncFunction: Task.async(function* () {
+ throw "Rejected!";
+ })
+ };
+
+ yield object.asyncFunction();
+ }).then(function () {
+ do_throw("unexpected success calling async function that throws error");
+ }, function (ex) {
+ do_check_eq(ex, "Rejected!");
+ run_next_test();
+ });
+});
+
+add_test(function test_async_return_function()
+{
+ Task.spawn(function* () {
+ // Ensure an async function that returns a function resolves to the function
+ // itself instead of calling the function and resolving to its return value.
+ return Task.spawn(function* () {
+ let returnValue = function () {
+ return "These aren't the droids you're looking for.";
+ };
+
+ let asyncFunction = Task.async(function () {
+ return returnValue;
+ });
+
+ do_check_eq((yield asyncFunction()), returnValue);
+ });
+ }).then(function () {
+ run_next_test();
+ }, function (ex) {
+ do_throw("Unexpected error: " + ex);
+ });
+});
+
+add_test(function test_async_throw_argument_not_function()
+{
+ Task.spawn(function* () {
+ // Ensure Task.async throws if its aTask argument is not a function.
+ Assert.throws(() => Task.async("not a function"),
+ /aTask argument must be a function/);
+ }).then(function () {
+ run_next_test();
+ }, function (ex) {
+ do_throw("Unexpected error: " + ex);
+ });
+});
+
+add_test(function test_async_throw_on_function_in_place_of_promise()
+{
+ Task.spawn(function* () {
+ // Ensure Task.spawn throws if passed an async function.
+ Assert.throws(() => Task.spawn(Task.async(function* () {})),
+ /Cannot use an async function in place of a promise/);
+ }).then(function () {
+ run_next_test();
+ }, function (ex) {
+ do_throw("Unexpected error: " + ex);
+ });
+});
+
+
+////////////////// Test rewriting of stack traces
+
+// Backup Task.Debuggin.maintainStack.
+// Will be restored by `exit_stack_tests`.
+var maintainStack;
+add_test(function enter_stack_tests() {
+ maintainStack = Task.Debugging.maintainStack;
+ Task.Debugging.maintainStack = true;
+ run_next_test();
+});
+
+
+/**
+ * Ensure that a list of frames appear in a stack, in the right order
+ */
+function do_check_rewritten_stack(frames, ex) {
+ do_print("Checking that the expected frames appear in the right order");
+ do_print(frames.join(", "));
+ let stack = ex.stack;
+ do_print(stack);
+
+ let framesFound = 0;
+ let lineNumber = 0;
+ let reLine = /([^\r\n])+/g;
+ let match;
+ while (framesFound < frames.length && (match = reLine.exec(stack))) {
+ let line = match[0];
+ let frame = frames[framesFound];
+ do_print("Searching for " + frame + " in line " + line);
+ if (line.indexOf(frame) != -1) {
+ do_print("Found " + frame);
+ ++framesFound;
+ } else {
+ do_print("Didn't find " + frame);
+ }
+ }
+
+ if (framesFound >= frames.length) {
+ return;
+ }
+ do_throw("Did not find: " + frames.slice(framesFound).join(", ") +
+ " in " + stack.substr(reLine.lastIndex));
+
+ do_print("Ensuring that we have removed Task.jsm, Promise.jsm");
+ do_check_true(stack.indexOf("Task.jsm") == -1);
+ do_check_true(stack.indexOf("Promise.jsm") == -1);
+ do_check_true(stack.indexOf("Promise-backend.js") == -1);
+}
+
+
+// Test that we get an acceptable rewritten stack when we launch
+// an error in a Task.spawn.
+add_test(function test_spawn_throw_stack() {
+ Task.spawn(function* task_spawn_throw_stack() {
+ for (let i = 0; i < 5; ++i) {
+ yield Promise.resolve(); // Without stack rewrite, this would lose valuable information
+ }
+ throw new Error("BOOM");
+ }).then(do_throw, function(ex) {
+ do_check_rewritten_stack(["task_spawn_throw_stack",
+ "test_spawn_throw_stack"],
+ ex);
+ run_next_test();
+ });
+});
+
+// Test that we get an acceptable rewritten stack when we yield
+// a rejection in a Task.spawn.
+add_test(function test_spawn_yield_reject_stack() {
+ Task.spawn(function* task_spawn_yield_reject_stack() {
+ for (let i = 0; i < 5; ++i) {
+ yield Promise.resolve(); // Without stack rewrite, this would lose valuable information
+ }
+ yield Promise.reject(new Error("BOOM"));
+ }).then(do_throw, function(ex) {
+ do_check_rewritten_stack(["task_spawn_yield_reject_stack",
+ "test_spawn_yield_reject_stack"],
+ ex);
+ run_next_test();
+ });
+});
+
+// Test that we get an acceptable rewritten stack when we launch
+// an error in a Task.async function.
+add_test(function test_async_function_throw_stack() {
+ let task_async_function_throw_stack = Task.async(function*() {
+ for (let i = 0; i < 5; ++i) {
+ yield Promise.resolve(); // Without stack rewrite, this would lose valuable information
+ }
+ throw new Error("BOOM");
+ })().then(do_throw, function(ex) {
+ do_check_rewritten_stack(["task_async_function_throw_stack",
+ "test_async_function_throw_stack"],
+ ex);
+ run_next_test();
+ });
+});
+
+// Test that we get an acceptable rewritten stack when we launch
+// an error in a Task.async function.
+add_test(function test_async_function_yield_reject_stack() {
+ let task_async_function_yield_reject_stack = Task.async(function*() {
+ for (let i = 0; i < 5; ++i) {
+ yield Promise.resolve(); // Without stack rewrite, this would lose valuable information
+ }
+ yield Promise.reject(new Error("BOOM"));
+ })().then(do_throw, function(ex) {
+ do_check_rewritten_stack(["task_async_function_yield_reject_stack",
+ "test_async_function_yield_reject_stack"],
+ ex);
+ run_next_test();
+ });
+});
+
+// Test that we get an acceptable rewritten stack when we launch
+// an error in a Task.async function.
+add_test(function test_async_method_throw_stack() {
+ let object = {
+ task_async_method_throw_stack: Task.async(function*() {
+ for (let i = 0; i < 5; ++i) {
+ yield Promise.resolve(); // Without stack rewrite, this would lose valuable information
+ }
+ throw new Error("BOOM");
+ })
+ };
+ object.task_async_method_throw_stack().then(do_throw, function(ex) {
+ do_check_rewritten_stack(["task_async_method_throw_stack",
+ "test_async_method_throw_stack"],
+ ex);
+ run_next_test();
+ });
+});
+
+// Test that we get an acceptable rewritten stack when we launch
+// an error in a Task.async function.
+add_test(function test_async_method_yield_reject_stack() {
+ let object = {
+ task_async_method_yield_reject_stack: Task.async(function*() {
+ for (let i = 0; i < 5; ++i) {
+ yield Promise.resolve(); // Without stack rewrite, this would lose valuable information
+ }
+ yield Promise.reject(new Error("BOOM"));
+ })
+ };
+ object.task_async_method_yield_reject_stack().then(do_throw, function(ex) {
+ do_check_rewritten_stack(["task_async_method_yield_reject_stack",
+ "test_async_method_yield_reject_stack"],
+ ex);
+ run_next_test();
+ });
+});
+
+// Test that two tasks whose execution takes place interleaved do not capture each other's stack.
+add_test(function test_throw_stack_do_not_capture_the_wrong_task() {
+ for (let iter_a of [3, 4, 5]) { // Vary the interleaving
+ for (let iter_b of [3, 4, 5]) {
+ Task.spawn(function* task_a() {
+ for (let i = 0; i < iter_a; ++i) {
+ yield Promise.resolve();
+ }
+ throw new Error("BOOM");
+ }).then(do_throw, function(ex) {
+ do_check_rewritten_stack(["task_a",
+ "test_throw_stack_do_not_capture_the_wrong_task"],
+ ex);
+ do_check_true(!ex.stack.includes("task_b"));
+ run_next_test();
+ });
+ Task.spawn(function* task_b() {
+ for (let i = 0; i < iter_b; ++i) {
+ yield Promise.resolve();
+ }
+ });
+ }
+ }
+});
+
+// Put things together
+add_test(function test_throw_complex_stack()
+{
+ // Setup the following stack:
+ // inner_method()
+ // task_3()
+ // task_2()
+ // task_1()
+ // function_3()
+ // function_2()
+ // function_1()
+ // test_throw_complex_stack()
+ (function function_1() {
+ return (function function_2() {
+ return (function function_3() {
+ return Task.spawn(function* task_1() {
+ yield Promise.resolve();
+ try {
+ yield Task.spawn(function* task_2() {
+ yield Promise.resolve();
+ yield Task.spawn(function* task_3() {
+ yield Promise.resolve();
+ let inner_object = {
+ inner_method: Task.async(function*() {
+ throw new Error("BOOM");
+ })
+ };
+ yield Promise.resolve();
+ yield inner_object.inner_method();
+ });
+ });
+ } catch (ex) {
+ yield Promise.resolve();
+ throw ex;
+ }
+ });
+ })();
+ })();
+ })().then(
+ () => do_throw("Shouldn't have succeeded"),
+ (ex) => {
+ let expect = ["inner_method",
+ "task_3",
+ "task_2",
+ "task_1",
+ "function_3",
+ "function_2",
+ "function_1",
+ "test_throw_complex_stack"];
+ do_check_rewritten_stack(expect, ex);
+
+ run_next_test();
+ });
+});
+
+add_test(function test_without_maintainStack() {
+ do_print("Calling generateReadableStack without a Task");
+ Task.Debugging.generateReadableStack(new Error("Not a real error"));
+
+ Task.Debugging.maintainStack = false;
+
+ do_print("Calling generateReadableStack with neither a Task nor maintainStack");
+ Task.Debugging.generateReadableStack(new Error("Not a real error"));
+
+ do_print("Calling generateReadableStack without maintainStack");
+ Task.spawn(function*() {
+ Task.Debugging.generateReadableStack(new Error("Not a real error"));
+ run_next_test();
+ });
+});
+
+add_test(function exit_stack_tests() {
+ Task.Debugging.maintainStack = maintainStack;
+ run_next_test();
+});
+
diff --git a/toolkit/modules/tests/xpcshell/test_timer.js b/toolkit/modules/tests/xpcshell/test_timer.js
new file mode 100644
index 000000000..57e300663
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_timer.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests exports from Timer.jsm
+
+var imported = {};
+Components.utils.import("resource://gre/modules/Timer.jsm", imported);
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_setTimeout() {
+ let timeout1 = imported.setTimeout(() => do_throw("Should not be called"), 100);
+ do_check_eq(typeof timeout1, "number", "setTimeout returns a number");
+ do_check_true(timeout1 > 0, "setTimeout returns a positive number");
+
+ imported.clearTimeout(timeout1);
+
+ yield new Promise((resolve) => {
+ let timeout2 = imported.setTimeout((param1, param2) => {
+ do_check_true(true, "Should be called");
+ do_check_eq(param1, 5, "first parameter is correct");
+ do_check_eq(param2, "test", "second parameter is correct");
+ resolve();
+ }, 100, 5, "test");
+
+ do_check_eq(typeof timeout2, "number", "setTimeout returns a number");
+ do_check_true(timeout2 > 0, "setTimeout returns a positive number");
+ do_check_neq(timeout1, timeout2, "Calling setTimeout again returns a different value");
+ });
+});
+
+add_task(function* test_setInterval() {
+ let interval1 = imported.setInterval(() => do_throw("Should not be called!"), 100);
+ do_check_eq(typeof interval1, "number", "setInterval returns a number");
+ do_check_true(interval1 > 0, "setTimeout returns a positive number");
+
+ imported.clearInterval(interval1);
+
+ const EXPECTED_CALLS = 5;
+ let calls = 0;
+
+ yield new Promise((resolve) => {
+ let interval2 = imported.setInterval((param1, param2) => {
+ do_check_true(true, "Should be called");
+ do_check_eq(param1, 15, "first parameter is correct");
+ do_check_eq(param2, "hola", "second parameter is correct");
+ if (calls >= EXPECTED_CALLS) {
+ resolve();
+ }
+ calls++;
+ }, 100, 15, "hola");
+ });
+});
diff --git a/toolkit/modules/tests/xpcshell/test_web_channel.js b/toolkit/modules/tests/xpcshell/test_web_channel.js
new file mode 100644
index 000000000..05f1bc03d
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_web_channel.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/WebChannel.jsm");
+
+const ERROR_ID_ORIGIN_REQUIRED = "WebChannel id and originOrPermission are required.";
+const VALID_WEB_CHANNEL_ID = "id";
+const URL_STRING = "http://example.com";
+const VALID_WEB_CHANNEL_ORIGIN = Services.io.newURI(URL_STRING, null, null);
+const TEST_PERMISSION_NAME = "test-webchannel-permissions";
+
+var MockWebChannelBroker = {
+ _channelMap: new Map(),
+ registerChannel: function(channel) {
+ if (!this._channelMap.has(channel)) {
+ this._channelMap.set(channel);
+ }
+ },
+ unregisterChannel: function (channelToRemove) {
+ this._channelMap.delete(channelToRemove)
+ }
+};
+
+function run_test() {
+ run_next_test();
+}
+
+/**
+ * Web channel tests
+ */
+
+/**
+ * Test channel listening with originOrPermission being an nsIURI.
+ */
+add_task(function test_web_channel_listen() {
+ return new Promise((resolve, reject) => {
+ let channel = new WebChannel(VALID_WEB_CHANNEL_ID, VALID_WEB_CHANNEL_ORIGIN, {
+ broker: MockWebChannelBroker
+ });
+ let delivered = 0;
+ do_check_eq(channel.id, VALID_WEB_CHANNEL_ID);
+ do_check_eq(channel._originOrPermission.spec, VALID_WEB_CHANNEL_ORIGIN.spec);
+ do_check_eq(channel._deliverCallback, null);
+
+ channel.listen(function(id, message, target) {
+ do_check_eq(id, VALID_WEB_CHANNEL_ID);
+ do_check_true(message);
+ do_check_true(message.command);
+ do_check_true(target.sender);
+ delivered++;
+ // 2 messages should be delivered
+ if (delivered === 2) {
+ channel.stopListening();
+ do_check_eq(channel._deliverCallback, null);
+ resolve();
+ }
+ });
+
+ // send two messages
+ channel.deliver({
+ id: VALID_WEB_CHANNEL_ID,
+ message: {
+ command: "one"
+ }
+ }, { sender: true });
+
+ channel.deliver({
+ id: VALID_WEB_CHANNEL_ID,
+ message: {
+ command: "two"
+ }
+ }, { sender: true });
+ });
+});
+
+/**
+ * Test channel listening with originOrPermission being a permission string.
+ */
+add_task(function test_web_channel_listen_permission() {
+ return new Promise((resolve, reject) => {
+ // add a new permission
+ Services.perms.add(VALID_WEB_CHANNEL_ORIGIN, TEST_PERMISSION_NAME, Services.perms.ALLOW_ACTION);
+ do_register_cleanup(() => Services.perms.remove(VALID_WEB_CHANNEL_ORIGIN, TEST_PERMISSION_NAME));
+ let channel = new WebChannel(VALID_WEB_CHANNEL_ID, TEST_PERMISSION_NAME, {
+ broker: MockWebChannelBroker
+ });
+ let delivered = 0;
+ do_check_eq(channel.id, VALID_WEB_CHANNEL_ID);
+ do_check_eq(channel._originOrPermission, TEST_PERMISSION_NAME);
+ do_check_eq(channel._deliverCallback, null);
+
+ channel.listen(function(id, message, target) {
+ do_check_eq(id, VALID_WEB_CHANNEL_ID);
+ do_check_true(message);
+ do_check_true(message.command);
+ do_check_true(target.sender);
+ delivered++;
+ // 2 messages should be delivered
+ if (delivered === 2) {
+ channel.stopListening();
+ do_check_eq(channel._deliverCallback, null);
+ resolve();
+ }
+ });
+
+ // send two messages
+ channel.deliver({
+ id: VALID_WEB_CHANNEL_ID,
+ message: {
+ command: "one"
+ }
+ }, { sender: true });
+
+ channel.deliver({
+ id: VALID_WEB_CHANNEL_ID,
+ message: {
+ command: "two"
+ }
+ }, { sender: true });
+ });
+});
+
+
+/**
+ * Test constructor
+ */
+add_test(function test_web_channel_constructor() {
+ do_check_eq(constructorTester(), ERROR_ID_ORIGIN_REQUIRED);
+ do_check_eq(constructorTester(undefined), ERROR_ID_ORIGIN_REQUIRED);
+ do_check_eq(constructorTester(undefined, VALID_WEB_CHANNEL_ORIGIN), ERROR_ID_ORIGIN_REQUIRED);
+ do_check_eq(constructorTester(VALID_WEB_CHANNEL_ID, undefined), ERROR_ID_ORIGIN_REQUIRED);
+ do_check_false(constructorTester(VALID_WEB_CHANNEL_ID, VALID_WEB_CHANNEL_ORIGIN));
+
+ run_next_test();
+});
+
+function constructorTester(id, origin) {
+ try {
+ new WebChannel(id, origin);
+ } catch (e) {
+ return e.message;
+ }
+ return false;
+}
diff --git a/toolkit/modules/tests/xpcshell/test_web_channel_broker.js b/toolkit/modules/tests/xpcshell/test_web_channel_broker.js
new file mode 100644
index 000000000..132597c20
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/test_web_channel_broker.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/WebChannel.jsm");
+
+const VALID_WEB_CHANNEL_ID = "id";
+const URL_STRING = "http://example.com";
+const VALID_WEB_CHANNEL_ORIGIN = Services.io.newURI(URL_STRING, null, null);
+
+function run_test() {
+ run_next_test();
+}
+
+/**
+ * Test WebChannelBroker channel map
+ */
+add_test(function test_web_channel_broker_channel_map() {
+ let channel = {};
+ let channel2 = {};
+
+ do_check_eq(WebChannelBroker._channelMap.size, 0);
+ do_check_false(WebChannelBroker._messageListenerAttached);
+
+ // make sure _channelMap works correctly
+ WebChannelBroker.registerChannel(channel);
+ do_check_eq(WebChannelBroker._channelMap.size, 1);
+ do_check_true(WebChannelBroker._messageListenerAttached);
+
+ WebChannelBroker.registerChannel(channel2);
+ do_check_eq(WebChannelBroker._channelMap.size, 2);
+
+ WebChannelBroker.unregisterChannel(channel);
+ do_check_eq(WebChannelBroker._channelMap.size, 1);
+
+ // make sure the correct channel is unregistered
+ do_check_false(WebChannelBroker._channelMap.has(channel));
+ do_check_true(WebChannelBroker._channelMap.has(channel2));
+
+ WebChannelBroker.unregisterChannel(channel2);
+ do_check_eq(WebChannelBroker._channelMap.size, 0);
+
+ run_next_test();
+});
+
+
+/**
+ * Test WebChannelBroker _listener test
+ */
+add_task(function test_web_channel_broker_listener() {
+ return new Promise((resolve, reject) => {
+ var channel = {
+ id: VALID_WEB_CHANNEL_ID,
+ _originCheckCallback: requestPrincipal => {
+ return VALID_WEB_CHANNEL_ORIGIN.prePath === requestPrincipal.origin;
+ },
+ deliver: function(data, sender) {
+ do_check_eq(data.id, VALID_WEB_CHANNEL_ID);
+ do_check_eq(data.message.command, "hello");
+ do_check_neq(sender, undefined);
+ WebChannelBroker.unregisterChannel(channel);
+ resolve();
+ }
+ };
+
+ WebChannelBroker.registerChannel(channel);
+
+ var mockEvent = {
+ data: {
+ id: VALID_WEB_CHANNEL_ID,
+ message: {
+ command: "hello"
+ }
+ },
+ principal: {
+ origin: URL_STRING
+ },
+ objects: {
+ },
+ };
+
+ WebChannelBroker._listener(mockEvent);
+ });
+});
diff --git a/toolkit/modules/tests/xpcshell/xpcshell.ini b/toolkit/modules/tests/xpcshell/xpcshell.ini
new file mode 100644
index 000000000..65d7c45e9
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,75 @@
+[DEFAULT]
+head =
+tail =
+support-files =
+ propertyLists/bug710259_propertyListBinary.plist
+ propertyLists/bug710259_propertyListXML.plist
+ chromeappsstore.sqlite
+ zips/zen.zip
+
+[test_BinarySearch.js]
+skip-if = toolkit == 'android'
+[test_CanonicalJSON.js]
+[test_client_id.js]
+skip-if = toolkit == 'android'
+[test_Color.js]
+[test_DeferredTask.js]
+skip-if = toolkit == 'android'
+[test_FileUtils.js]
+skip-if = toolkit == 'android'
+[test_FinderIterator.js]
+[test_GMPInstallManager.js]
+skip-if = toolkit == 'android'
+[test_Http.js]
+skip-if = toolkit == 'android'
+[test_Integration.js]
+[test_jsesc.js]
+skip-if = toolkit == 'android'
+[test_JSONFile.js]
+skip-if = toolkit == 'android'
+[test_Log.js]
+skip-if = toolkit == 'android'
+[test_MatchPattern.js]
+skip-if = toolkit == 'android'
+[test_MatchGlobs.js]
+skip-if = toolkit == 'android'
+[test_MatchURLFilters.js]
+skip-if = toolkit == 'android'
+[test_NewTabUtils.js]
+skip-if = toolkit == 'android'
+[test_ObjectUtils.js]
+skip-if = toolkit == 'android'
+[test_ObjectUtils_strict.js]
+skip-if = toolkit == 'android'
+[test_PermissionsUtils.js]
+skip-if = toolkit == 'android'
+[test_Preferences.js]
+skip-if = toolkit == 'android'
+[test_Promise.js]
+skip-if = toolkit == 'android'
+[test_PromiseUtils.js]
+skip-if = toolkit == 'android'
+[test_propertyListsUtils.js]
+skip-if = toolkit == 'android'
+[test_readCertPrefs.js]
+skip-if = toolkit == 'android'
+[test_Services.js]
+skip-if = toolkit == 'android'
+[test_session_recorder.js]
+skip-if = toolkit == 'android'
+[test_sqlite.js]
+skip-if = toolkit == 'android'
+[test_sqlite_shutdown.js]
+skip-if = toolkit == 'android'
+[test_task.js]
+skip-if = toolkit == 'android'
+[test_timer.js]
+skip-if = toolkit == 'android'
+[test_UpdateUtils_url.js]
+[test_UpdateUtils_updatechannel.js]
+[test_web_channel.js]
+[test_web_channel_broker.js]
+[test_ZipUtils.js]
+skip-if = toolkit == 'android'
+[test_Log_stackTrace.js]
+[test_servicerequest_xhr.js]
diff --git a/toolkit/modules/tests/xpcshell/zips/zen.zip b/toolkit/modules/tests/xpcshell/zips/zen.zip
new file mode 100644
index 000000000..475624793
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/zips/zen.zip
Binary files differ