summaryrefslogtreecommitdiffstats
path: root/toolkit/components/utils/test/unit
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/utils/test/unit
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/utils/test/unit')
-rw-r--r--toolkit/components/utils/test/unit/test_ClientEnvironment.js109
-rw-r--r--toolkit/components/utils/test/unit/test_FilterExpressions.js366
-rw-r--r--toolkit/components/utils/test/unit/test_Sampling.js128
-rw-r--r--toolkit/components/utils/test/unit/xpcshell.ini3
4 files changed, 606 insertions, 0 deletions
diff --git a/toolkit/components/utils/test/unit/test_ClientEnvironment.js b/toolkit/components/utils/test/unit/test_ClientEnvironment.js
new file mode 100644
index 0000000000..74f7994e68
--- /dev/null
+++ b/toolkit/components/utils/test/unit/test_ClientEnvironment.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { ClientEnvironmentBase } = ChromeUtils.import(
+ "resource://gre/modules/components-utils/ClientEnvironment.jsm"
+);
+const { TelemetryController } = ChromeUtils.import(
+ "resource://gre/modules/TelemetryController.jsm"
+);
+const { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+// OS Data
+add_task(async () => {
+ const os = ClientEnvironmentBase.os;
+ ok(os !== undefined, "OS data should be available in the context");
+
+ let osCount = 0;
+ if (os.isWindows) {
+ osCount += 1;
+ }
+ if (os.isMac) {
+ osCount += 1;
+ }
+ if (os.isLinux) {
+ osCount += 1;
+ }
+ ok(osCount <= 1, "At most one OS should match");
+
+ // if on Windows, Windows versions should be set, and Mac versions should not be
+ if (os.isWindows) {
+ equal(
+ typeof os.windowsVersion,
+ "number",
+ "Windows version should be a number"
+ );
+ equal(
+ typeof os.windowsBuildNumber,
+ "number",
+ "Windows build number should be a number"
+ );
+ equal(os.macVersion, null, "Mac version should not be set");
+ equal(os.darwinVersion, null, "Darwin version should not be set");
+ }
+
+ // if on Mac, Mac versions should be set, and Windows versions should not be
+ if (os.isMac) {
+ equal(typeof os.macVersion, "number", "Mac version should be a number");
+ equal(
+ typeof os.darwinVersion,
+ "number",
+ "Darwin version should be a number"
+ );
+ equal(os.windowsVersion, null, "Windows version should not be set");
+ equal(
+ os.windowsBuildNumber,
+ null,
+ "Windows build number version should not be set"
+ );
+ }
+
+ // if on Linux, no versions should be set
+ if (os.isLinux) {
+ equal(os.macVersion, null, "Mac version should not be set");
+ equal(os.darwinVersion, null, "Darwin version should not be set");
+ equal(os.windowsVersion, null, "Windows version should not be set");
+ equal(
+ os.windowsBuildNumber,
+ null,
+ "Windows build number version should not be set"
+ );
+ }
+});
+
+add_task(async () => {
+ try {
+ await ClientEnvironmentBase.attribution;
+ } catch (ex) {
+ equal(
+ ex.name,
+ "NS_ERROR_FILE_NOT_FOUND",
+ "Test environment does not have attribution data"
+ );
+ }
+});
+
+add_task(async function testLiveTelemetry() {
+ // Setup telemetry so we can read from it
+ do_get_profile(true);
+ await TelemetryController.testSetup();
+
+ equal(
+ ClientEnvironmentBase.liveTelemetry.main.environment.build.displayVersion,
+ AppConstants.MOZ_APP_VERSION_DISPLAY,
+ "Telemetry data is available"
+ );
+
+ Assert.throws(
+ () => ClientEnvironmentBase.liveTelemetry.anotherPingType,
+ /Live telemetry.*anotherPingType/,
+ "Non-main pings should raise an error if accessed"
+ );
+
+ // Put things back the way we found them
+ await TelemetryController.testShutdown();
+});
diff --git a/toolkit/components/utils/test/unit/test_FilterExpressions.js b/toolkit/components/utils/test/unit/test_FilterExpressions.js
new file mode 100644
index 0000000000..9089b8d76c
--- /dev/null
+++ b/toolkit/components/utils/test/unit/test_FilterExpressions.js
@@ -0,0 +1,366 @@
+"use strict";
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "FilterExpressions",
+ "resource://gre/modules/components-utils/FilterExpressions.jsm"
+);
+
+// Basic JEXL tests
+add_task(async function() {
+ let val;
+ // Test that basic expressions work
+ val = await FilterExpressions.eval("2+2");
+ equal(val, 4, "basic expression works");
+
+ // Test that multiline expressions work
+ val = await FilterExpressions.eval(`
+ 2
+ +
+ 2
+ `);
+ equal(val, 4, "multiline expression works");
+
+ // Test that it reads from the context correctly.
+ val = await FilterExpressions.eval("first + second + 3", {
+ first: 1,
+ second: 2,
+ });
+ equal(val, 6, "context is available to filter expressions");
+});
+
+// Date tests
+add_task(async function() {
+ let val;
+ // Test has a date transform
+ val = await FilterExpressions.eval('"2016-04-22"|date');
+ const d = new Date(Date.UTC(2016, 3, 22)); // months are 0 based
+ equal(val.toString(), d.toString(), "Date transform works");
+
+ // Test dates are comparable
+ const context = { someTime: Date.UTC(2016, 0, 1) };
+ val = await FilterExpressions.eval('"2015-01-01"|date < someTime', context);
+ ok(val, "dates are comparable with less-than");
+ val = await FilterExpressions.eval('"2017-01-01"|date > someTime', context);
+ ok(val, "dates are comparable with greater-than");
+});
+
+// Sampling tests
+add_task(async function() {
+ let val;
+ // Test stable sample returns true for matching samples
+ val = await FilterExpressions.eval('["test"]|stableSample(1)');
+ ok(val, "Stable sample returns true for 100% sample");
+
+ // Test stable sample returns true for matching samples
+ val = await FilterExpressions.eval('["test"]|stableSample(0)');
+ ok(!val, "Stable sample returns false for 0% sample");
+
+ // Test stable sample for known samples
+ val = await FilterExpressions.eval('["test-1"]|stableSample(0.5)');
+ ok(val, "Stable sample returns true for a known sample");
+ val = await FilterExpressions.eval('["test-4"]|stableSample(0.5)');
+ ok(!val, "Stable sample returns false for a known sample");
+
+ // Test bucket sample for known samples
+ val = await FilterExpressions.eval('["test-1"]|bucketSample(0, 5, 10)');
+ ok(val, "Bucket sample returns true for a known sample");
+ val = await FilterExpressions.eval('["test-4"]|bucketSample(0, 5, 10)');
+ ok(!val, "Bucket sample returns false for a known sample");
+});
+
+// Preference tests
+add_task(async function() {
+ let val;
+ // Compare the value of the preference
+ Services.prefs.setIntPref("normandy.test.value", 3);
+ registerCleanupFunction(() =>
+ Services.prefs.clearUserPref("normandy.test.value")
+ );
+
+ val = await FilterExpressions.eval(
+ '"normandy.test.value"|preferenceValue == 3'
+ );
+ ok(val, "preferenceValue expression compares against preference values");
+ val = await FilterExpressions.eval(
+ '"normandy.test.value"|preferenceValue == "test"'
+ );
+ ok(!val, "preferenceValue expression fails value checks appropriately");
+
+ // preferenceValue can take a default value as an optional argument, which
+ // defaults to `undefined`.
+ val = await FilterExpressions.eval(
+ '"normandy.test.default"|preferenceValue(false) == false'
+ );
+ ok(
+ val,
+ "preferenceValue takes optional 'default value' param for prefs without set values"
+ );
+ val = await FilterExpressions.eval(
+ '"normandy.test.value"|preferenceValue(5) == 5'
+ );
+ ok(
+ !val,
+ "preferenceValue default param is not returned for prefs with set values"
+ );
+
+ // Compare if the preference is user set
+ val = await FilterExpressions.eval(
+ '"normandy.test.isSet"|preferenceIsUserSet == true'
+ );
+ ok(
+ !val,
+ "preferenceIsUserSet expression determines if preference is set at all"
+ );
+ val = await FilterExpressions.eval(
+ '"normandy.test.value"|preferenceIsUserSet == true'
+ );
+ ok(
+ val,
+ "preferenceIsUserSet expression determines if user's preference has been set"
+ );
+
+ // Compare if the preference has _any_ value, whether it's user-set or default,
+ val = await FilterExpressions.eval(
+ '"normandy.test.nonexistant"|preferenceExists == true'
+ );
+ ok(
+ !val,
+ "preferenceExists expression determines if preference exists at all"
+ );
+ val = await FilterExpressions.eval(
+ '"normandy.test.value"|preferenceExists == true'
+ );
+ ok(val, "preferenceExists expression fails existence check appropriately");
+});
+
+// keys tests
+add_task(async function testKeys() {
+ let val;
+
+ // Test an object defined in JEXL
+ val = await FilterExpressions.eval("{foo: 1, bar: 2}|keys");
+ Assert.deepEqual(
+ new Set(val),
+ new Set(["foo", "bar"]),
+ "keys returns the keys from an object in JEXL"
+ );
+
+ // Test an object in the context
+ let context = { ctxObject: { baz: "string", biff: NaN } };
+ val = await FilterExpressions.eval("ctxObject|keys", context);
+
+ Assert.deepEqual(
+ new Set(val),
+ new Set(["baz", "biff"]),
+ "keys returns the keys from an object in the context"
+ );
+
+ // Test that values from the prototype are not included
+ context = { ctxObject: Object.create({ fooProto: 7 }) };
+ context.ctxObject.baz = 8;
+ context.ctxObject.biff = 5;
+ equal(
+ await FilterExpressions.eval("ctxObject.fooProto", context),
+ 7,
+ "Prototype properties are accessible via property access"
+ );
+ val = await FilterExpressions.eval("ctxObject|keys", context);
+ Assert.deepEqual(
+ new Set(val),
+ new Set(["baz", "biff"]),
+ "keys does not return properties from the object's prototype chain"
+ );
+
+ // Return undefined for non-objects
+ equal(
+ await FilterExpressions.eval("ctxObject|keys", { ctxObject: 45 }),
+ undefined,
+ "keys returns undefined for numbers"
+ );
+ equal(
+ await FilterExpressions.eval("ctxObject|keys", { ctxObject: null }),
+ undefined,
+ "keys returns undefined for null"
+ );
+
+ // Object properties are not cached
+ let pong = 0;
+ context = {
+ ctxObject: {
+ get ping() {
+ return ++pong;
+ },
+ },
+ };
+ await FilterExpressions.eval(
+ "ctxObject.ping == 0 || ctxObject.ping == 1",
+ context
+ );
+ equal(pong, 2, "Properties are not reifed");
+});
+
+add_task(async function testLength() {
+ equal(
+ await FilterExpressions.eval("[1, null, {a: 2, b: 3}, Infinity]|length"),
+ 4,
+ "length returns the length of the array it's applied to"
+ );
+
+ equal(
+ await FilterExpressions.eval("[]|length"),
+ 0,
+ "length is zero for an empty array"
+ );
+
+ // Should be undefined for non-Arrays
+ equal(
+ await FilterExpressions.eval("5|length"),
+ undefined,
+ "length is undefined when applied to numbers"
+ );
+ equal(
+ await FilterExpressions.eval("null|length"),
+ undefined,
+ "length is undefined when applied to null"
+ );
+ equal(
+ await FilterExpressions.eval("undefined|length"),
+ undefined,
+ "length is undefined when applied to undefined"
+ );
+ equal(
+ await FilterExpressions.eval("{a: 1, b: 2, c: 3}|length"),
+ undefined,
+ "length is undefined when applied to non-Array objects"
+ );
+});
+
+add_task(async function testMapToProperty() {
+ Assert.deepEqual(
+ await FilterExpressions.eval(
+ '[{a: 1}, {a: {b: 10}}, {a: [5,6,7,8]}]|mapToProperty("a")'
+ ),
+ [1, { b: 10 }, [5, 6, 7, 8]],
+ "mapToProperty returns an array of values when applied to an array of objects all with the property defined"
+ );
+
+ Assert.deepEqual(
+ await FilterExpressions.eval('[]|mapToProperty("a")'),
+ [],
+ "mapToProperty returns an empty array when applied to an empty array"
+ );
+
+ Assert.deepEqual(
+ await FilterExpressions.eval('[{a: 1}, {b: 2}, {a: 3}]|mapToProperty("a")'),
+ [1, undefined, 3],
+ "mapToProperty returns an array with undefined entries where the property is undefined"
+ );
+
+ // Should be undefined for non-Arrays
+ equal(
+ await FilterExpressions.eval('5|mapToProperty("a")'),
+ undefined,
+ "mapToProperty returns undefined when applied numbers"
+ );
+ equal(
+ await FilterExpressions.eval('null|mapToProperty("a")'),
+ undefined,
+ "mapToProperty returns undefined when applied null"
+ );
+ equal(
+ await FilterExpressions.eval('undefined|mapToProperty("a")'),
+ undefined,
+ "mapToProperty returns undefined when applied undefined"
+ );
+ equal(
+ await FilterExpressions.eval('{a: 1, b: 2, c: 3}|mapToProperty("a")'),
+ undefined,
+ "mapToProperty returns undefined when applied non-Array objects"
+ );
+});
+
+// intersect tests
+add_task(async function testIntersect() {
+ let val;
+
+ val = await FilterExpressions.eval("[1, 2, 3] intersect [4, 2, 6, 7, 3]");
+ Assert.deepEqual(
+ new Set(val),
+ new Set([2, 3]),
+ "intersect finds the common elements between two lists in JEXL"
+ );
+
+ const context = { left: [5, 7], right: [4, 5, 3] };
+ val = await FilterExpressions.eval("left intersect right", context);
+ Assert.deepEqual(
+ new Set(val),
+ new Set([5]),
+ "intersect finds the common elements between two lists in the context"
+ );
+
+ val = await FilterExpressions.eval(
+ "['string', 2] intersect [4, 'string', 'other', 3]"
+ );
+ Assert.deepEqual(
+ new Set(val),
+ new Set(["string"]),
+ "intersect can compare strings"
+ );
+
+ // Return undefined when intersecting things that aren't lists.
+ equal(
+ await FilterExpressions.eval("5 intersect 7"),
+ undefined,
+ "intersect returns undefined for numbers"
+ );
+ equal(
+ await FilterExpressions.eval("val intersect other", {
+ val: null,
+ other: null,
+ }),
+ undefined,
+ "intersect returns undefined for null"
+ );
+ equal(
+ await FilterExpressions.eval("5 intersect [1, 2, 5]"),
+ undefined,
+ "intersect returns undefined if only one operand is a list"
+ );
+});
+
+add_task(async function test_regExpMatch() {
+ let val;
+
+ val = await FilterExpressions.eval('"foobar"|regExpMatch("^foo(.+?)$")');
+ Assert.deepEqual(
+ new Set(val),
+ new Set(["foobar", "bar"]),
+ "regExpMatch returns the matches in an array"
+ );
+
+ val = await FilterExpressions.eval('"FOObar"|regExpMatch("^foo(.+?)$", "i")');
+ Assert.deepEqual(
+ new Set(val),
+ new Set(["FOObar", "bar"]),
+ "regExpMatch accepts flags for matching"
+ );
+
+ val = await FilterExpressions.eval('"F00bar"|regExpMatch("^foo(.+?)$", "i")');
+ Assert.equal(val, null, "regExpMatch returns null if there are no matches");
+});
+
+add_task(async function test_versionCompare() {
+ let val;
+
+ val = await FilterExpressions.eval('"1.0.0"|versionCompare("1")');
+ ok(val === 0);
+
+ val = await FilterExpressions.eval('"1.0.0"|versionCompare("1.1")');
+ ok(val < 0);
+
+ val = await FilterExpressions.eval('"1.0.0"|versionCompare("0.1")');
+ ok(val > 0);
+});
diff --git a/toolkit/components/utils/test/unit/test_Sampling.js b/toolkit/components/utils/test/unit/test_Sampling.js
new file mode 100644
index 0000000000..6f46182cc5
--- /dev/null
+++ b/toolkit/components/utils/test/unit/test_Sampling.js
@@ -0,0 +1,128 @@
+"use strict";
+
+ChromeUtils.import(
+ "resource://gre/modules/components-utils/Sampling.jsm",
+ this
+);
+
+add_task(async function testStableSample() {
+ // Absolute samples
+ equal(
+ await Sampling.stableSample("test", 1),
+ true,
+ "stableSample returns true for 100% sample"
+ );
+ equal(
+ await Sampling.stableSample("test", 0),
+ false,
+ "stableSample returns false for 0% sample"
+ );
+
+ // Known samples. The numbers are nonces to make the tests pass
+ equal(
+ await Sampling.stableSample("test-0", 0.5),
+ true,
+ "stableSample returns true for known matching sample"
+ );
+ equal(
+ await Sampling.stableSample("test-1", 0.5),
+ false,
+ "stableSample returns false for known non-matching sample"
+ );
+});
+
+add_task(async function testBucketSample() {
+ // Absolute samples
+ equal(
+ await Sampling.bucketSample("test", 0, 10, 10),
+ true,
+ "bucketSample returns true for 100% sample"
+ );
+ equal(
+ await Sampling.bucketSample("test", 0, 0, 10),
+ false,
+ "bucketSample returns false for 0% sample"
+ );
+
+ // Known samples. The numbers are nonces to make the tests pass
+ equal(
+ await Sampling.bucketSample("test-0", 0, 5, 10),
+ true,
+ "bucketSample returns true for known matching sample"
+ );
+ equal(
+ await Sampling.bucketSample("test-1", 0, 5, 10),
+ false,
+ "bucketSample returns false for known non-matching sample"
+ );
+});
+
+add_task(async function testRatioSample() {
+ // Invalid input
+ await Assert.rejects(
+ Sampling.ratioSample("test", []),
+ /ratios must be at least 1 element long/,
+ "ratioSample rejects for a list with no ratios"
+ );
+
+ // Absolute samples
+ equal(
+ await Sampling.ratioSample("test", [1]),
+ 0,
+ "ratioSample returns 0 for a list with only 1 ratio"
+ );
+ equal(
+ await Sampling.ratioSample("test", [0, 0, 1, 0]),
+ 2,
+ "ratioSample returns the only non-zero bucket if all other buckets are zero"
+ );
+
+ // Known samples. The numbers are nonces to make the tests pass
+ equal(
+ await Sampling.ratioSample("test-0", [1, 1]),
+ 0,
+ "ratioSample returns the correct index for known matching sample"
+ );
+ equal(
+ await Sampling.ratioSample("test-1", [1, 1]),
+ 1,
+ "ratioSample returns the correct index for known non-matching sample"
+ );
+});
+
+add_task(async function testFractionToKey() {
+ // Test that results are always 12 character hexadecimal strings.
+ const expected_regex = /[0-9a-f]{12}/;
+ const count = 100;
+ let successes = 0;
+ for (let i = 0; i < count; i++) {
+ const p = Sampling.fractionToKey(Math.random());
+ if (expected_regex.test(p)) {
+ successes++;
+ }
+ }
+ equal(successes, count, "fractionToKey makes keys the right length");
+});
+
+add_task(async function testTruncatedHash() {
+ const expected_regex = /[0-9a-f]{12}/;
+ const count = 100;
+ let successes = 0;
+ for (let i = 0; i < count; i++) {
+ const h = await Sampling.truncatedHash(Math.random());
+ if (expected_regex.test(h)) {
+ successes++;
+ }
+ }
+ equal(successes, count, "truncatedHash makes hashes the right length");
+});
+
+add_task(async function testBufferToHex() {
+ const data = new ArrayBuffer(4);
+ const view = new DataView(data);
+ view.setUint8(0, 0xff);
+ view.setUint8(1, 0x7f);
+ view.setUint8(2, 0x3f);
+ view.setUint8(3, 0x1f);
+ equal(Sampling.bufferToHex(data), "ff7f3f1f");
+});
diff --git a/toolkit/components/utils/test/unit/xpcshell.ini b/toolkit/components/utils/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..ad002859bb
--- /dev/null
+++ b/toolkit/components/utils/test/unit/xpcshell.ini
@@ -0,0 +1,3 @@
+[test_ClientEnvironment.js]
+[test_FilterExpressions.js]
+[test_Sampling.js]