summaryrefslogtreecommitdiffstats
path: root/toolkit/components/glean/tests/xpcshell
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/glean/tests/xpcshell
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/glean/tests/xpcshell')
-rw-r--r--toolkit/components/glean/tests/xpcshell/head.js6
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_FOGIPCLimit.js51
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_FOGInit.js49
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_FOGPrefs.js47
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_GIFFT.js523
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_GIFFTIPC.js315
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_Glean.js458
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_GleanExperiments.js28
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_GleanIPC.js157
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_GleanServerKnobs.js164
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_JOG.js728
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_JOGIPC.js266
-rw-r--r--toolkit/components/glean/tests/xpcshell/test_MillionQ.js19
-rw-r--r--toolkit/components/glean/tests/xpcshell/xpcshell.toml32
14 files changed, 2843 insertions, 0 deletions
diff --git a/toolkit/components/glean/tests/xpcshell/head.js b/toolkit/components/glean/tests/xpcshell/head.js
new file mode 100644
index 0000000000..f42bd02822
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/head.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
diff --git a/toolkit/components/glean/tests/xpcshell/test_FOGIPCLimit.js b/toolkit/components/glean/tests/xpcshell/test_FOGIPCLimit.js
new file mode 100644
index 0000000000..10ab9f5bc3
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_FOGIPCLimit.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+
+add_setup(
+ /* on Android FOG is set up through head.js */
+ { skip_if: () => !runningInParent || AppConstants.platform == "android" },
+ function test_setup() {
+ // Give FOG a temp profile to init within.
+ do_get_profile();
+
+ // We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
+ Services.fog.initializeFOG();
+ }
+);
+
+// Keep in sync with ipc.rs.
+// "Why no -1?" Because the limit's 100k. The -1 is because of atomic ops.
+const FOG_IPC_PAYLOAD_ACCESS_LIMIT = 100000;
+
+add_task({ skip_if: () => runningInParent }, async function run_child_stuff() {
+ for (let i = 0; i < FOG_IPC_PAYLOAD_ACCESS_LIMIT + 1; i++) {
+ Glean.testOnly.badCode.add(1);
+ }
+});
+
+add_task(
+ { skip_if: () => !runningInParent },
+ async function test_fog_ipc_limit() {
+ await run_test_in_child("test_FOGIPCLimit.js");
+
+ await ContentTaskUtils.waitForCondition(() => {
+ return !!Glean.testOnly.badCode.testGetValue();
+ }, "Waiting for IPC.");
+
+ // The child exceeded the number of accesses to trigger an IPC flush.
+ Assert.greater(
+ Glean.testOnly.badCode.testGetValue(),
+ FOG_IPC_PAYLOAD_ACCESS_LIMIT
+ );
+ }
+);
diff --git a/toolkit/components/glean/tests/xpcshell/test_FOGInit.js b/toolkit/components/glean/tests/xpcshell/test_FOGInit.js
new file mode 100644
index 0000000000..e5aaced074
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_FOGInit.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+add_setup(
+ /* on Android FOG is set up through head.js */
+ { skip_if: () => AppConstants.platform == "android" },
+ function test_setup() {
+ // FOG needs a profile directory to put its data in.
+ do_get_profile();
+
+ // Glean init (via `chrono`) gets the timezone via unprotected write.
+ // This is being worked around:
+ // https://github.com/chronotope/chrono/pull/677
+ // Until that reaches a release and we update to it (bug 1780401), ensure
+ // local time has been loaded by JS before we kick of Glean init.
+ new Date().getHours(); // used for its side effect.
+
+ // We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
+ Services.fog.initializeFOG();
+ }
+);
+
+add_task(function test_fog_init_works() {
+ if (new Date().getHours() >= 3 && new Date().getHours() <= 4) {
+ // We skip this test if it's too close to 4AM, when we might send a
+ // "metrics" ping between init and this test being run.
+ Assert.ok(true, "Too close to 'metrics' ping send window. Skipping test.");
+ return;
+ }
+ Assert.greater(
+ Glean.fog.initialization.testGetValue(),
+ 0,
+ "FOG init happened, and its time was measured."
+ );
+});
+
+add_task(function test_fog_initialized_with_correct_rate_limit() {
+ Assert.greater(
+ Glean.fog.maxPingsPerMinute.testGetValue(),
+ 0,
+ "FOG has been initialized with a ping rate limit of greater than 0."
+ );
+});
diff --git a/toolkit/components/glean/tests/xpcshell/test_FOGPrefs.js b/toolkit/components/glean/tests/xpcshell/test_FOGPrefs.js
new file mode 100644
index 0000000000..943ffb3186
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_FOGPrefs.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TELEMETRY_SERVER_PREF = "toolkit.telemetry.server";
+const UPLOAD_PREF = "datareporting.healthreport.uploadEnabled";
+const LOCALHOST_PREF = "telemetry.fog.test.localhost_port";
+
+// FOG needs a profile directory to put its data in.
+do_get_profile();
+
+// We want Glean to use a localhost server so we can be SURE not to send data to the outside world.
+// Yes, the port spells GLEAN on a T9 keyboard, why do you ask?
+Services.prefs.setIntPref(LOCALHOST_PREF, 45326);
+// We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
+Services.fog.initializeFOG();
+
+add_task(function test_fog_upload_only() {
+ // Don't forget to point the telemetry server to localhost, or Telemetry
+ // might make a non-local connection during the test run.
+ Services.prefs.setStringPref(
+ TELEMETRY_SERVER_PREF,
+ "http://localhost/telemetry-fake/"
+ );
+ // Be sure to set port=-1 for faking success _before_ enabling upload.
+ // Or else there's a short window where we might send something.
+ Services.prefs.setIntPref(LOCALHOST_PREF, -1);
+ Services.prefs.setBoolPref(UPLOAD_PREF, true);
+
+ const value = 42;
+ Glean.testOnly.meaningOfLife.set(value);
+ // We specifically look at the custom ping's value because we know it
+ // won't be reset by being sent.
+ Assert.equal(value, Glean.testOnly.meaningOfLife.testGetValue("test-ping"));
+
+ // Despite upload being disabled, we keep the old values around.
+ Services.prefs.setBoolPref(UPLOAD_PREF, false);
+ Assert.equal(value, Glean.testOnly.meaningOfLife.testGetValue("test-ping"));
+
+ // Now, when we turn the fake upload off, we clear the stores
+ Services.prefs.setIntPref(LOCALHOST_PREF, 0);
+ Assert.equal(
+ undefined,
+ Glean.testOnly.meaningOfLife.testGetValue("test-ping")
+ );
+});
diff --git a/toolkit/components/glean/tests/xpcshell/test_GIFFT.js b/toolkit/components/glean/tests/xpcshell/test_GIFFT.js
new file mode 100644
index 0000000000..015b4d4e38
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_GIFFT.js
@@ -0,0 +1,523 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const Telemetry = Services.telemetry;
+
+function sleep(ms) {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+function scalarValue(aScalarName) {
+ let snapshot = Telemetry.getSnapshotForScalars();
+ return "parent" in snapshot ? snapshot.parent[aScalarName] : undefined;
+}
+
+function keyedScalarValue(aScalarName) {
+ let snapshot = Telemetry.getSnapshotForKeyedScalars();
+ return "parent" in snapshot ? snapshot.parent[aScalarName] : undefined;
+}
+
+add_setup(function test_setup() {
+ // FOG needs a profile directory to put its data in.
+ do_get_profile();
+
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+
+ // We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
+ // On Android FOG is set up through head.js.
+ if (AppConstants.platform != "android") {
+ Services.fog.initializeFOG();
+ }
+});
+
+add_task(function test_gifft_counter() {
+ Glean.testOnlyIpc.aCounter.add(20);
+ Assert.equal(20, Glean.testOnlyIpc.aCounter.testGetValue());
+ Assert.equal(20, scalarValue("telemetry.test.mirror_for_counter"));
+});
+
+add_task(function test_gifft_boolean() {
+ Glean.testOnlyIpc.aBool.set(false);
+ Assert.equal(false, Glean.testOnlyIpc.aBool.testGetValue());
+ Assert.equal(false, scalarValue("telemetry.test.boolean_kind"));
+});
+
+add_task(function test_gifft_datetime() {
+ const dateStr = "2021-03-22T16:06:00";
+ const value = new Date(dateStr);
+ Glean.testOnlyIpc.aDate.set(value.getTime() * 1000);
+
+ let received = Glean.testOnlyIpc.aDate.testGetValue();
+ Assert.equal(value.getTime(), received.getTime());
+ Assert.ok(scalarValue("telemetry.test.mirror_for_date").startsWith(dateStr));
+});
+
+add_task(function test_gifft_string() {
+ const value = "a string!";
+ Glean.testOnlyIpc.aString.set(value);
+
+ Assert.equal(value, Glean.testOnlyIpc.aString.testGetValue());
+ Assert.equal(value, scalarValue("telemetry.test.multiple_stores_string"));
+});
+
+add_task(function test_gifft_memory_dist() {
+ Glean.testOnlyIpc.aMemoryDist.accumulate(7);
+ Glean.testOnlyIpc.aMemoryDist.accumulate(17);
+
+ let data = Glean.testOnlyIpc.aMemoryDist.testGetValue();
+ // `data.sum` is in bytes, but the metric is in KB.
+ Assert.equal(24 * 1024, data.sum, "Sum's correct");
+ for (let [bucket, count] of Object.entries(data.values)) {
+ Assert.ok(
+ count == 0 || (count == 1 && (bucket == 6888 || bucket == 17109)),
+ `Only two buckets have a sample ${bucket} ${count}`
+ );
+ }
+
+ data = Telemetry.getHistogramById("TELEMETRY_TEST_LINEAR").snapshot();
+ Telemetry.getHistogramById("TELEMETRY_TEST_LINEAR").clear();
+ Assert.equal(24, data.sum, "Histogram's in `memory_unit` units");
+ Assert.equal(2, data.values["1"], "Both samples in a low bucket");
+
+ // MemoryDistribution's Accumulate method to takes
+ // a platform specific type (size_t).
+ // Glean's, however, is i64, and, glean_memory_dist is uint64_t
+ // What happens when we give accumulate dubious values?
+ // This may occur on some uncommon platforms.
+ // Note: there are issues in JS with numbers above 2**53
+ Glean.testOnlyIpc.aMemoryDist.accumulate(36893488147419103232);
+ let dubiousValue = Object.entries(
+ Glean.testOnlyIpc.aMemoryDist.testGetValue().values
+ )[0][1];
+ Assert.equal(
+ dubiousValue,
+ 1,
+ "Greater than 64-Byte number did not accumulate correctly"
+ );
+
+ // Values lower than the out-of-range value are not clamped
+ // resulting in an exception being thrown from the glean side
+ // when the value exceeds the glean maximum allowed value
+ Glean.testOnlyIpc.aMemoryDist.accumulate(Math.pow(2, 31));
+ Assert.throws(
+ () => Glean.testOnlyIpc.aMemoryDist.testGetValue(),
+ /DataError/,
+ "Did not accumulate correctly"
+ );
+});
+
+add_task(function test_gifft_custom_dist() {
+ Glean.testOnlyIpc.aCustomDist.accumulateSamples([7, 268435458]);
+
+ let data = Glean.testOnlyIpc.aCustomDist.testGetValue();
+ Assert.equal(7 + 268435458, data.sum, "Sum's correct");
+ for (let [bucket, count] of Object.entries(data.values)) {
+ Assert.ok(
+ count == 0 || (count == 1 && (bucket == 1 || bucket == 268435456)),
+ `Only two buckets have a sample ${bucket} ${count}`
+ );
+ }
+
+ data = Telemetry.getHistogramById(
+ "TELEMETRY_TEST_MIRROR_FOR_CUSTOM"
+ ).snapshot();
+ Telemetry.getHistogramById("TELEMETRY_TEST_MIRROR_FOR_CUSTOM").clear();
+ Assert.equal(7 + 268435458, data.sum, "Sum in histogram is correct");
+ Assert.equal(1, data.values["1"], "One sample in the low bucket");
+ // Yes, the bucket is off-by-one compared to Glean.
+ Assert.equal(1, data.values["268435457"], "One sample in the next bucket");
+});
+
+add_task(async function test_gifft_timing_dist() {
+ let t1 = Glean.testOnlyIpc.aTimingDist.start();
+ // Interleave some other metric's samples. bug 1768636.
+ let ot1 = Glean.testOnly.whatTimeIsIt.start();
+ let t2 = Glean.testOnlyIpc.aTimingDist.start();
+ let ot2 = Glean.testOnly.whatTimeIsIt.start();
+ Glean.testOnly.whatTimeIsIt.cancel(ot1);
+ Glean.testOnly.whatTimeIsIt.cancel(ot2);
+
+ await sleep(5);
+
+ let t3 = Glean.testOnlyIpc.aTimingDist.start();
+ Glean.testOnlyIpc.aTimingDist.cancel(t1);
+
+ await sleep(5);
+
+ Glean.testOnlyIpc.aTimingDist.stopAndAccumulate(t2); // 10ms
+ Glean.testOnlyIpc.aTimingDist.stopAndAccumulate(t3); // 5ms
+
+ let data = Glean.testOnlyIpc.aTimingDist.testGetValue();
+ const NANOS_IN_MILLIS = 1e6;
+ // bug 1701949 - Sleep gets close, but sometimes doesn't wait long enough.
+ const EPSILON = 40000;
+
+ // Variance in timing makes getting the sum impossible to know.
+ // 10 and 5 input value can be trunacted to 4. + 9. >= 13. from cast
+ Assert.greater(data.sum, 13 * NANOS_IN_MILLIS - EPSILON);
+
+ // No guarantees from timers means no guarantees on buckets.
+ // But we can guarantee it's only two samples.
+ Assert.equal(
+ 2,
+ Object.entries(data.values).reduce(
+ (acc, [bucket, count]) => acc + count,
+ 0
+ ),
+ "Only two buckets with samples"
+ );
+
+ data = Telemetry.getHistogramById("TELEMETRY_TEST_EXPONENTIAL").snapshot();
+ // Suffers from same cast truncation issue of 9.... and 4.... values
+ Assert.greaterOrEqual(data.sum, 13, "Histogram's in milliseconds");
+ Assert.equal(
+ 2,
+ Object.entries(data.values).reduce(
+ (acc, [bucket, count]) => acc + count,
+ 0
+ ),
+ "Only two samples"
+ );
+});
+
+add_task(function test_gifft_string_list_works() {
+ const value = "a string!";
+ const value2 = "another string!";
+ const value3 = "yet another string.";
+
+ // `set` doesn't work in the mirror, so use `add`
+ Glean.testOnlyIpc.aStringList.add(value);
+ Glean.testOnlyIpc.aStringList.add(value2);
+ Glean.testOnlyIpc.aStringList.add(value3);
+
+ let val = Glean.testOnlyIpc.aStringList.testGetValue();
+ // Note: This is incredibly fragile and will break if we ever rearrange items
+ // in the string list.
+ Assert.deepEqual([value, value2, value3], val);
+
+ val = keyedScalarValue("telemetry.test.keyed_boolean_kind");
+ // This too may be fragile.
+ Assert.deepEqual(
+ {
+ [value]: true,
+ [value2]: true,
+ [value3]: true,
+ },
+ val
+ );
+});
+
+add_task(function test_gifft_events() {
+ Telemetry.setEventRecordingEnabled("telemetry.test", true);
+
+ Glean.testOnlyIpc.noExtraEvent.record();
+ var events = Glean.testOnlyIpc.noExtraEvent.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("test_only.ipc", events[0].category);
+ Assert.equal("no_extra_event", events[0].name);
+
+ let extra = { extra1: "can set extras", extra2: "passing more data" };
+ Glean.testOnlyIpc.anEvent.record(extra);
+ events = Glean.testOnlyIpc.anEvent.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("test_only.ipc", events[0].category);
+ Assert.equal("an_event", events[0].name);
+ Assert.deepEqual(extra, events[0].extra);
+
+ TelemetryTestUtils.assertEvents(
+ [
+ ["telemetry.test", "not_expired_optout", "object1", undefined, undefined],
+ ["telemetry.test", "mirror_with_extra", "object1", null, extra],
+ ],
+ { category: "telemetry.test" }
+ );
+});
+
+add_task(function test_gifft_uuid() {
+ const kTestUuid = "decafdec-afde-cafd-ecaf-decafdecafde";
+ Glean.testOnlyIpc.aUuid.set(kTestUuid);
+ Assert.equal(kTestUuid, Glean.testOnlyIpc.aUuid.testGetValue());
+ Assert.equal(kTestUuid, scalarValue("telemetry.test.string_kind"));
+});
+
+add_task(function test_gifft_labeled_counter() {
+ Assert.equal(
+ undefined,
+ Glean.testOnlyIpc.aLabeledCounter.a_label.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.testOnlyIpc.aLabeledCounter.a_label.add(1);
+ Glean.testOnlyIpc.aLabeledCounter.another_label.add(2);
+ Glean.testOnlyIpc.aLabeledCounter.a_label.add(3);
+ Assert.equal(4, Glean.testOnlyIpc.aLabeledCounter.a_label.testGetValue());
+ Assert.equal(
+ 2,
+ Glean.testOnlyIpc.aLabeledCounter.another_label.testGetValue()
+ );
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.testOnlyIpc.aLabeledCounter.__other__.testGetValue()
+ );
+ Glean.testOnlyIpc.aLabeledCounter["1".repeat(72)].add(3);
+ Assert.throws(
+ () => Glean.testOnlyIpc.aLabeledCounter.__other__.testGetValue(),
+ /DataError/,
+ "Can't get the value when you're error'd"
+ );
+
+ let value = keyedScalarValue(
+ "telemetry.test.another_mirror_for_labeled_counter"
+ );
+ Assert.deepEqual(
+ {
+ a_label: 4,
+ another_label: 2,
+ ["1".repeat(72)]: 3,
+ },
+ value
+ );
+});
+
+add_task(async function test_gifft_timespan() {
+ // We start, briefly sleep and then stop.
+ // That guarantees some time to measure.
+ Glean.testOnly.mirrorTime.start();
+ await sleep(10);
+ Glean.testOnly.mirrorTime.stop();
+
+ const NANOS_IN_MILLIS = 1e6;
+ // bug 1701949 - Sleep gets close, but sometimes doesn't wait long enough.
+ const EPSILON = 40000;
+ Assert.greater(
+ Glean.testOnly.mirrorTime.testGetValue(),
+ 10 * NANOS_IN_MILLIS - EPSILON
+ );
+ // Mirrored to milliseconds.
+ Assert.greaterOrEqual(scalarValue("telemetry.test.mirror_for_timespan"), 9);
+});
+
+add_task(async function test_gifft_timespan_raw() {
+ Glean.testOnly.mirrorTimeNanos.setRaw(15 /*ns*/);
+
+ Assert.equal(15, Glean.testOnly.mirrorTimeNanos.testGetValue());
+ // setRaw, unlike start/stop, mirrors the raw value directly.
+ Assert.equal(scalarValue("telemetry.test.mirror_for_timespan_nanos"), 15);
+});
+
+add_task(async function test_gifft_labeled_boolean() {
+ Assert.equal(
+ undefined,
+ Glean.testOnly.mirrorsForLabeledBools.a_label.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.testOnly.mirrorsForLabeledBools.a_label.set(true);
+ Glean.testOnly.mirrorsForLabeledBools.another_label.set(false);
+ Assert.equal(
+ true,
+ Glean.testOnly.mirrorsForLabeledBools.a_label.testGetValue()
+ );
+ Assert.equal(
+ false,
+ Glean.testOnly.mirrorsForLabeledBools.another_label.testGetValue()
+ );
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.testOnly.mirrorsForLabeledBools.__other__.testGetValue()
+ );
+ Glean.testOnly.mirrorsForLabeledBools["1".repeat(72)].set(true);
+ Assert.throws(
+ () => Glean.testOnly.mirrorsForLabeledBools.__other__.testGetValue(),
+ /DataError/,
+ "Should throw because of a recording error."
+ );
+
+ // In Telemetry there is no invalid label
+ let value = keyedScalarValue("telemetry.test.mirror_for_labeled_bool");
+ Assert.deepEqual(
+ {
+ a_label: true,
+ another_label: false,
+ ["1".repeat(72)]: true,
+ },
+ value
+ );
+});
+
+add_task(function test_gifft_boolean() {
+ Glean.testOnly.meaningOfLife.set(42);
+ Assert.equal(42, Glean.testOnly.meaningOfLife.testGetValue());
+ Assert.equal(42, scalarValue("telemetry.test.mirror_for_quantity"));
+});
+
+add_task(function test_gifft_rate() {
+ Glean.testOnlyIpc.irate.addToNumerator(22);
+ Glean.testOnlyIpc.irate.addToDenominator(7);
+ Assert.deepEqual(
+ { numerator: 22, denominator: 7 },
+ Glean.testOnlyIpc.irate.testGetValue()
+ );
+ Assert.deepEqual(
+ { numerator: 22, denominator: 7 },
+ keyedScalarValue("telemetry.test.mirror_for_rate")
+ );
+});
+
+add_task(function test_gifft_numeric_limits() {
+ // Glean and Telemetry don't share the same storage sizes or signedness.
+ // Check the edges.
+
+ // 0) Reset everything
+ Services.fog.testResetFOG();
+ Services.telemetry.getSnapshotForHistograms("main", true /* aClearStore */);
+ Services.telemetry.getSnapshotForScalars("main", true /* aClearStore */);
+ Services.telemetry.getSnapshotForKeyedScalars("main", true /* aClearStore */);
+
+ // 1) Counter: i32 (saturates), mirrored to uint Scalar: u32 (overflows)
+ // 1.1) Negative parameters refused.
+ Glean.testOnlyIpc.aCounter.add(-20);
+ // Unfortunately we can't check what the error was, due to API design.
+ // (chutten blames chutten for his shortsightedness)
+ Assert.throws(
+ () => Glean.testOnlyIpc.aCounter.testGetValue(),
+ /DataError/,
+ "Can't get the value when you're error'd"
+ );
+ Assert.equal(undefined, scalarValue("telemetry.test.mirror_for_counter"));
+ // Clear the error state
+ Services.fog.testResetFOG();
+
+ // 1.2) Values that sum larger than u32::max saturate (counter) and overflow (Scalar)
+ // Sums to 2^32 + 1
+ Glean.testOnlyIpc.aCounter.add(Math.pow(2, 31) - 1);
+ Glean.testOnlyIpc.aCounter.add(1);
+ Glean.testOnlyIpc.aCounter.add(Math.pow(2, 31) - 1);
+ Glean.testOnlyIpc.aCounter.add(2);
+ // Glean doesn't actually throw on saturation (bug 1751469),
+ // so we can just check the saturation value.
+ Assert.equal(Math.pow(2, 31) - 1, Glean.testOnlyIpc.aCounter.testGetValue());
+ // Telemetry will have wrapped around to 1
+ Assert.equal(1, scalarValue("telemetry.test.mirror_for_counter"));
+
+ // 2) Quantity: i64 (saturates), mirrored to uint Scalar: u32 (overflows)
+ // 2.1) Negative parameters refused.
+ Glean.testOnly.meaningOfLife.set(-42);
+ // Glean will error on this.
+ Assert.throws(
+ () => Glean.testOnly.meaningOfLife.testGetValue(),
+ /DataError/,
+ "Can't get the value when you're error'd"
+ );
+ // GIFFT doesn't tell Telemetry about the weird value at all.
+ Assert.equal(undefined, scalarValue("telemetry.test.mirror_for_quantity"));
+ // Clear the error state
+ Services.fog.testResetFOG();
+
+ // 2.2) A parameter larger than u32::max is passed to Glean unchanged,
+ // but is clamped to u32::max before being passed to Telemetry.
+ Glean.testOnly.meaningOfLife.set(Math.pow(2, 32));
+ Assert.equal(Math.pow(2, 32), Glean.testOnly.meaningOfLife.testGetValue());
+ Assert.equal(
+ Math.pow(2, 32) - 1,
+ scalarValue("telemetry.test.mirror_for_quantity")
+ );
+
+ // 3) Rate: two i32 (saturates), mirrored to keyed uint Scalar: u32s (overflow)
+ // 3.1) Negative parameters refused.
+ Glean.testOnlyIpc.irate.addToNumerator(-22);
+ Glean.testOnlyIpc.irate.addToDenominator(7);
+ Assert.throws(
+ () => Glean.testOnlyIpc.irate.testGetValue(),
+ /DataError/,
+ "Can't get the value when you're error'd"
+ );
+ Assert.deepEqual(
+ { denominator: 7 },
+ keyedScalarValue("telemetry.test.mirror_for_rate")
+ );
+ // Clear the error state
+ Services.fog.testResetFOG();
+ // Clear the partial Telemetry value
+ Services.telemetry.getSnapshotForKeyedScalars("main", true /* aClearStore */);
+
+ // Now the denominator:
+ Glean.testOnlyIpc.irate.addToNumerator(22);
+ Glean.testOnlyIpc.irate.addToDenominator(-7);
+ Assert.throws(
+ () => Glean.testOnlyIpc.irate.testGetValue(),
+ /DataError/,
+ "Can't get the value when you're error'd"
+ );
+ Assert.deepEqual(
+ { numerator: 22 },
+ keyedScalarValue("telemetry.test.mirror_for_rate")
+ );
+
+ // 4) Timespan
+ // ( Can't overflow time without finding a way to get TimeStamp to think
+ // we're 2^32 milliseconds later without waiting a month )
+
+ // 5) TimingDistribution
+ // ( Can't overflow time with start() and stopAndAccumulate() without
+ // waiting for ages. But we _do_ have a test-only raw API...)
+ // The max sample for timing_distribution is 600000000000.
+ // The type for timing_distribution samples is i64.
+ // This means when we explore the edges of GIFFT's limits, we're well past
+ // Glean's limits. All we can get out of Glean is errors.
+ // (Which is good for data, difficult for tests.)
+ // But GIFFT should properly saturate in Telemetry at i32::max,
+ // so we shall test that.
+ Glean.testOnlyIpc.aTimingDist.testAccumulateRawMillis(Math.pow(2, 31) + 1);
+ Glean.testOnlyIpc.aTimingDist.testAccumulateRawMillis(Math.pow(2, 32) + 1);
+ Assert.throws(
+ () => Glean.testOnlyIpc.aTimingDist.testGetValue(),
+ /DataError/,
+ "Can't get the value when you're error'd"
+ );
+ let snapshot = Telemetry.getHistogramById(
+ "TELEMETRY_TEST_EXPONENTIAL"
+ ).snapshot();
+ Assert.equal(
+ snapshot.values["2147483646"],
+ 2,
+ "samples > i32::max should end up in the top bucket"
+ );
+});
+
+add_task(function test_gifft_url() {
+ const value = "https://www.example.com";
+ Glean.testOnlyIpc.aUrl.set(value);
+
+ Assert.equal(value, Glean.testOnlyIpc.aUrl.testGetValue());
+ Assert.equal(value, scalarValue("telemetry.test.mirror_for_url"));
+});
+
+add_task(function test_gifft_url_cropped() {
+ const value = `https://example.com${"/test".repeat(47)}`;
+ Glean.testOnlyIpc.aUrl.set(value);
+
+ Assert.equal(value, Glean.testOnlyIpc.aUrl.testGetValue());
+ // We expect the mirrored URL to be truncated at the maximum
+ // length supported by string scalars.
+ Assert.equal(
+ value.substring(0, 50),
+ scalarValue("telemetry.test.mirror_for_url")
+ );
+});
diff --git a/toolkit/components/glean/tests/xpcshell/test_GIFFTIPC.js b/toolkit/components/glean/tests/xpcshell/test_GIFFTIPC.js
new file mode 100644
index 0000000000..a318d57b9c
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_GIFFTIPC.js
@@ -0,0 +1,315 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+const Telemetry = Services.telemetry;
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+function sleep(ms) {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+function scalarValue(aScalarName, aProcessName) {
+ let snapshot = Telemetry.getSnapshotForScalars();
+ return aProcessName in snapshot
+ ? snapshot[aProcessName][aScalarName]
+ : undefined;
+}
+
+function keyedScalarValue(aScalarName, aProcessName) {
+ let snapshot = Telemetry.getSnapshotForKeyedScalars();
+ return aProcessName in snapshot
+ ? snapshot[aProcessName][aScalarName]
+ : undefined;
+}
+
+add_setup({ skip_if: () => !runningInParent }, function test_setup() {
+ // Give FOG a temp profile to init within.
+ do_get_profile();
+
+ // Allows these tests to properly run on e.g. Thunderbird
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+
+ // We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
+ // on Android FOG is set up through head.js
+ if (AppConstants.platform != "android") {
+ Services.fog.initializeFOG();
+ }
+});
+
+const COUNT = 42;
+const CHEESY_STRING = "a very cheesy string!";
+const CHEESIER_STRING = "a much cheesier string!";
+const CUSTOM_SAMPLES = [3, 4];
+const EVENT_EXTRA = { extra1: "so very extra" };
+const MEMORIES = [13, 31];
+const MEMORY_BUCKETS = ["13193", "31378"]; // buckets are strings : |
+const A_LABEL_COUNT = 3;
+const ANOTHER_LABEL_COUNT = 5;
+const INVALID_COUNTERS = 7;
+const IRATE_NUMERATOR = 44;
+const IRATE_DENOMINATOR = 14;
+
+add_task({ skip_if: () => runningInParent }, async function run_child_stuff() {
+ let oldCanRecordBase = Telemetry.canRecordBase;
+ Telemetry.canRecordBase = true; // Ensure we're able to record things.
+
+ Glean.testOnlyIpc.aCounter.add(COUNT);
+ Glean.testOnlyIpc.aStringList.add(CHEESY_STRING);
+ Glean.testOnlyIpc.aStringList.add(CHEESIER_STRING);
+
+ Glean.testOnlyIpc.noExtraEvent.record();
+ Glean.testOnlyIpc.anEvent.record(EVENT_EXTRA);
+
+ for (let memory of MEMORIES) {
+ Glean.testOnlyIpc.aMemoryDist.accumulate(memory);
+ }
+
+ let t1 = Glean.testOnlyIpc.aTimingDist.start();
+ let t2 = Glean.testOnlyIpc.aTimingDist.start();
+
+ await sleep(5);
+
+ let t3 = Glean.testOnlyIpc.aTimingDist.start();
+ Glean.testOnlyIpc.aTimingDist.cancel(t1);
+
+ await sleep(5);
+
+ Glean.testOnlyIpc.aTimingDist.stopAndAccumulate(t2); // 10ms
+ Glean.testOnlyIpc.aTimingDist.stopAndAccumulate(t3); // 5ms
+
+ Glean.testOnlyIpc.aCustomDist.accumulateSamples(CUSTOM_SAMPLES);
+
+ Glean.testOnlyIpc.aLabeledCounter.a_label.add(A_LABEL_COUNT);
+ Glean.testOnlyIpc.aLabeledCounter.another_label.add(ANOTHER_LABEL_COUNT);
+
+ // Has to be different from aLabeledCounter so the error we record doesn't
+ // get in the way.
+ Glean.testOnlyIpc.anotherLabeledCounter["1".repeat(72)].add(INVALID_COUNTERS);
+
+ Glean.testOnlyIpc.irate.addToNumerator(IRATE_NUMERATOR);
+ Glean.testOnlyIpc.irate.addToDenominator(IRATE_DENOMINATOR);
+ Telemetry.canRecordBase = oldCanRecordBase;
+});
+
+add_task(
+ { skip_if: () => !runningInParent },
+ async function test_child_metrics() {
+ Telemetry.setEventRecordingEnabled("telemetry.test", true);
+
+ // Clear any stray Telemetry data
+ Telemetry.clearScalars();
+ Telemetry.getSnapshotForHistograms("main", true);
+ Telemetry.clearEvents();
+
+ await run_test_in_child("test_GIFFTIPC.js");
+
+ // Wait for both IPC mechanisms to flush.
+ await Services.fog.testFlushAllChildren();
+ await ContentTaskUtils.waitForCondition(() => {
+ let snapshot = Telemetry.getSnapshotForKeyedScalars();
+ return (
+ "content" in snapshot &&
+ "telemetry.test.mirror_for_rate" in snapshot.content
+ );
+ }, "failed to find content telemetry in parent");
+
+ // boolean
+ // Doesn't work over IPC
+
+ // counter
+ Assert.equal(Glean.testOnlyIpc.aCounter.testGetValue(), COUNT);
+ Assert.equal(
+ scalarValue("telemetry.test.mirror_for_counter", "content"),
+ COUNT,
+ "content-process Scalar has expected count"
+ );
+
+ // custom_distribution
+ const customSampleSum = CUSTOM_SAMPLES.reduce((acc, a) => acc + a, 0);
+ const customData = Glean.testOnlyIpc.aCustomDist.testGetValue("store1");
+ Assert.equal(customSampleSum, customData.sum, "Sum's correct");
+ for (let [bucket, count] of Object.entries(customData.values)) {
+ Assert.ok(
+ count == 0 || (count == CUSTOM_SAMPLES.length && bucket == 1), // both values in the low bucket
+ `Only two buckets have a sample ${bucket} ${count}`
+ );
+ }
+ const histSnapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ false,
+ false
+ );
+ const histData = histSnapshot.content.TELEMETRY_TEST_MIRROR_FOR_CUSTOM;
+ Assert.equal(customSampleSum, histData.sum, "Sum in histogram's correct");
+ Assert.equal(2, histData.values["1"], "Two samples in the first bucket");
+
+ // datetime
+ // Doesn't work over IPC
+
+ // event
+ var events = Glean.testOnlyIpc.noExtraEvent.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("test_only.ipc", events[0].category);
+ Assert.equal("no_extra_event", events[0].name);
+
+ events = Glean.testOnlyIpc.anEvent.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("test_only.ipc", events[0].category);
+ Assert.equal("an_event", events[0].name);
+ Assert.deepEqual(EVENT_EXTRA, events[0].extra);
+
+ TelemetryTestUtils.assertEvents(
+ [
+ [
+ "telemetry.test",
+ "not_expired_optout",
+ "object1",
+ undefined,
+ undefined,
+ ],
+ ["telemetry.test", "mirror_with_extra", "object1", null, EVENT_EXTRA],
+ ],
+ { category: "telemetry.test" },
+ { process: "content" }
+ );
+
+ // labeled_boolean
+ // Doesn't work over IPC
+
+ // labeled_counter
+ const counters = Glean.testOnlyIpc.aLabeledCounter;
+ Assert.equal(counters.a_label.testGetValue(), A_LABEL_COUNT);
+ Assert.equal(counters.another_label.testGetValue(), ANOTHER_LABEL_COUNT);
+
+ Assert.throws(
+ () => Glean.testOnlyIpc.anotherLabeledCounter.__other__.testGetValue(),
+ /DataError/,
+ "Invalid labels record errors, which throw"
+ );
+
+ let value = keyedScalarValue(
+ "telemetry.test.another_mirror_for_labeled_counter",
+ "content"
+ );
+ Assert.deepEqual(
+ {
+ a_label: A_LABEL_COUNT,
+ another_label: ANOTHER_LABEL_COUNT,
+ },
+ value
+ );
+ value = keyedScalarValue(
+ "telemetry.test.mirror_for_labeled_counter",
+ "content"
+ );
+ Assert.deepEqual(
+ {
+ ["1".repeat(72)]: INVALID_COUNTERS,
+ },
+ value
+ );
+
+ // labeled_string
+ // Doesn't work over IPC
+
+ // memory_distribution
+ const memoryData = Glean.testOnlyIpc.aMemoryDist.testGetValue();
+ const memorySum = MEMORIES.reduce((acc, a) => acc + a, 0);
+ // The sum's in bytes, but the metric's in KB
+ Assert.equal(memorySum * 1024, memoryData.sum);
+ for (let [bucket, count] of Object.entries(memoryData.values)) {
+ // We could assert instead, but let's skip to save the logspam.
+ if (count == 0) {
+ continue;
+ }
+ Assert.ok(count == 1 && MEMORY_BUCKETS.includes(bucket));
+ }
+
+ const memoryHist = histSnapshot.content.TELEMETRY_TEST_LINEAR;
+ Assert.equal(
+ memorySum,
+ memoryHist.sum,
+ "Histogram's in `memory_unit` units"
+ );
+ Assert.equal(2, memoryHist.values["1"], "Samples are in the right bucket");
+
+ // quantity
+ // Doesn't work over IPC
+
+ // rate
+ Assert.deepEqual(
+ { numerator: IRATE_NUMERATOR, denominator: IRATE_DENOMINATOR },
+ Glean.testOnlyIpc.irate.testGetValue()
+ );
+ Assert.deepEqual(
+ { numerator: IRATE_NUMERATOR, denominator: IRATE_DENOMINATOR },
+ keyedScalarValue("telemetry.test.mirror_for_rate", "content")
+ );
+
+ // string
+ // Doesn't work over IPC
+
+ // string_list
+ // Note: this will break if string list ever rearranges its items.
+ const cheesyStrings = Glean.testOnlyIpc.aStringList.testGetValue();
+ Assert.deepEqual(cheesyStrings, [CHEESY_STRING, CHEESIER_STRING]);
+ // Note: this will break if keyed scalars rearrange their items.
+ Assert.deepEqual(
+ {
+ [CHEESY_STRING]: true,
+ [CHEESIER_STRING]: true,
+ },
+ keyedScalarValue("telemetry.test.keyed_boolean_kind", "content")
+ );
+
+ // timespan
+ // Doesn't work over IPC
+
+ // timing_distribution
+ const NANOS_IN_MILLIS = 1e6;
+ const EPSILON = 40000; // bug 1701949
+ const times = Glean.testOnlyIpc.aTimingDist.testGetValue();
+ Assert.greater(times.sum, 15 * NANOS_IN_MILLIS - EPSILON);
+ // We can't guarantee any specific time values (thank you clocks),
+ // but we can assert there are only two samples.
+ Assert.equal(
+ 2,
+ Object.entries(times.values).reduce(
+ (acc, [bucket, count]) => acc + count,
+ 0
+ )
+ );
+ const timingHist = histSnapshot.content.TELEMETRY_TEST_EXPONENTIAL;
+ Assert.greaterOrEqual(timingHist.sum, 13, "Histogram's in milliseconds.");
+ // Both values, 10 and 5, are truncated by a cast in AccumulateTimeDelta
+ // Minimally downcast 9. + 4. could realistically result in 13.
+ Assert.equal(
+ 2,
+ Object.entries(timingHist.values).reduce(
+ (acc, [bucket, count]) => acc + count,
+ 0
+ ),
+ "Only two samples"
+ );
+
+ // uuid
+ // Doesn't work over IPC
+ }
+);
diff --git a/toolkit/components/glean/tests/xpcshell/test_Glean.js b/toolkit/components/glean/tests/xpcshell/test_Glean.js
new file mode 100644
index 0000000000..8375c22a3e
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_Glean.js
@@ -0,0 +1,458 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+function sleep(ms) {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+add_setup(
+ /* on Android FOG is set up through head.js */
+ { skip_if: () => AppConstants.platform == "android" },
+ function test_setup() {
+ // FOG needs a profile directory to put its data in.
+ do_get_profile();
+
+ // We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
+ Services.fog.initializeFOG();
+ }
+);
+
+add_task(function test_fog_counter_works() {
+ Glean.testOnly.badCode.add(31);
+ Assert.equal(31, Glean.testOnly.badCode.testGetValue("test-ping"));
+});
+
+add_task(async function test_fog_string_works() {
+ Assert.equal(null, Glean.testOnly.cheesyString.testGetValue());
+
+ // Setting `undefined` will be ignored.
+ Glean.testOnly.cheesyString.set(undefined);
+ Assert.equal(null, Glean.testOnly.cheesyString.testGetValue());
+
+ const value = "a cheesy string!";
+ Glean.testOnly.cheesyString.set(value);
+
+ Assert.equal(value, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+});
+
+add_task(async function test_fog_string_list_works() {
+ const value = "a cheesy string!";
+ const value2 = "a cheesier string!";
+ const value3 = "the cheeziest of strings.";
+
+ const cheeseList = [value, value2];
+ Glean.testOnly.cheesyStringList.set(cheeseList);
+
+ let val = Glean.testOnly.cheesyStringList.testGetValue();
+ // Note: This is incredibly fragile and will break if we ever rearrange items
+ // in the string list.
+ Assert.deepEqual(cheeseList, val);
+
+ Glean.testOnly.cheesyStringList.add(value3);
+ Assert.ok(Glean.testOnly.cheesyStringList.testGetValue().includes(value3));
+});
+
+add_task(async function test_fog_timespan_works() {
+ Glean.testOnly.canWeTimeIt.start();
+ Glean.testOnly.canWeTimeIt.cancel();
+ Assert.equal(undefined, Glean.testOnly.canWeTimeIt.testGetValue());
+
+ // We start, briefly sleep and then stop.
+ // That guarantees some time to measure.
+ Glean.testOnly.canWeTimeIt.start();
+ await sleep(10);
+ Glean.testOnly.canWeTimeIt.stop();
+
+ Assert.ok(Glean.testOnly.canWeTimeIt.testGetValue("test-ping") > 0);
+});
+
+add_task(async function test_fog_timespan_throws_on_stop_wout_start() {
+ Glean.testOnly.canWeTimeIt.stop();
+ Assert.throws(
+ () => Glean.testOnly.canWeTimeIt.testGetValue(),
+ /DataError/,
+ "Should throw because stop was called without start."
+ );
+});
+
+add_task(async function test_fog_uuid_works() {
+ const kTestUuid = "decafdec-afde-cafd-ecaf-decafdecafde";
+ Glean.testOnly.whatIdIt.set(kTestUuid);
+ Assert.equal(kTestUuid, Glean.testOnly.whatIdIt.testGetValue("test-ping"));
+
+ Glean.testOnly.whatIdIt.generateAndSet();
+ // Since we generate v4 UUIDs, and the first character of the third group
+ // isn't 4, this won't ever collide with kTestUuid.
+ Assert.notEqual(kTestUuid, Glean.testOnly.whatIdIt.testGetValue("test-ping"));
+});
+
+add_task(function test_fog_datetime_works() {
+ const value = new Date("2020-06-11T12:00:00");
+
+ Glean.testOnly.whatADate.set(value.getTime() * 1000);
+
+ const received = Glean.testOnly.whatADate.testGetValue("test-ping");
+ Assert.equal(received.getTime(), value.getTime());
+});
+
+add_task(function test_fog_boolean_works() {
+ Glean.testOnly.canWeFlagIt.set(false);
+ Assert.equal(false, Glean.testOnly.canWeFlagIt.testGetValue("test-ping"));
+ // While you're here, might as well test that the ping name's optional.
+ Assert.equal(false, Glean.testOnly.canWeFlagIt.testGetValue());
+});
+
+add_task(async function test_fog_event_works() {
+ Glean.testOnlyIpc.noExtraEvent.record();
+ var events = Glean.testOnlyIpc.noExtraEvent.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("test_only.ipc", events[0].category);
+ Assert.equal("no_extra_event", events[0].name);
+
+ let extra = { extra1: "can set extras", extra2: "passing more data" };
+ Glean.testOnlyIpc.anEvent.record(extra);
+ events = Glean.testOnlyIpc.anEvent.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("test_only.ipc", events[0].category);
+ Assert.equal("an_event", events[0].name);
+ Assert.deepEqual(extra, events[0].extra);
+
+ // Corner case: Event with extra with `undefined` value.
+ // Should pretend that extra key isn't there.
+ extra = { extra1: undefined, extra2: "defined" };
+ Glean.testOnlyIpc.anEvent.record(extra);
+ events = Glean.testOnlyIpc.anEvent.testGetValue();
+ Assert.equal(2, events.length);
+ Assert.deepEqual({ extra2: "defined" }, events[1].extra);
+
+ let extra2 = {
+ extra1: "can set extras",
+ extra2: 37,
+ extra3_longer_name: false,
+ };
+ Glean.testOnlyIpc.eventWithExtra.record(extra2);
+ events = Glean.testOnlyIpc.eventWithExtra.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("test_only.ipc", events[0].category);
+ Assert.equal("event_with_extra", events[0].name);
+ let expectedExtra = {
+ extra1: "can set extras",
+ extra2: "37",
+ extra3_longer_name: "false",
+ };
+ Assert.deepEqual(expectedExtra, events[0].extra);
+
+ // camelCase extras work.
+ let extra5 = {
+ extra3LongerName: false,
+ };
+ Glean.testOnlyIpc.eventWithExtra.record(extra5);
+ events = Glean.testOnlyIpc.eventWithExtra.testGetValue();
+ Assert.equal(2, events.length, "Recorded one event too many.");
+ expectedExtra = {
+ extra3_longer_name: "false",
+ };
+ Assert.deepEqual(expectedExtra, events[1].extra);
+
+ // Invalid extra keys don't crash, the event is not recorded,
+ // but an error is recorded.
+ let extra3 = {
+ extra1_nonexistent_extra: "this does not crash",
+ };
+ Glean.testOnlyIpc.eventWithExtra.record(extra3);
+ Assert.throws(
+ () => Glean.testOnlyIpc.eventWithExtra.testGetValue(),
+ /DataError/,
+ "Should throw because of a recording error."
+ );
+});
+
+add_task(async function test_fog_memory_distribution_works() {
+ Glean.testOnly.doYouRemember.accumulate(7);
+ Glean.testOnly.doYouRemember.accumulate(17);
+
+ let data = Glean.testOnly.doYouRemember.testGetValue("test-ping");
+ // `data.sum` is in bytes, but the metric is in MB.
+ Assert.equal(24 * 1024 * 1024, data.sum, "Sum's correct");
+ for (let [bucket, count] of Object.entries(data.values)) {
+ Assert.ok(
+ count == 0 || (count == 1 && (bucket == 17520006 || bucket == 7053950)),
+ "Only two buckets have a sample"
+ );
+ }
+});
+
+add_task(async function test_fog_custom_distribution_works() {
+ Glean.testOnlyIpc.aCustomDist.accumulateSamples([7, 268435458]);
+
+ let data = Glean.testOnlyIpc.aCustomDist.testGetValue("store1");
+ Assert.equal(7 + 268435458, data.sum, "Sum's correct");
+ for (let [bucket, count] of Object.entries(data.values)) {
+ Assert.ok(
+ count == 0 || (count == 1 && (bucket == 1 || bucket == 268435456)),
+ `Only two buckets have a sample ${bucket} ${count}`
+ );
+ }
+
+ // Negative values will not be recorded, instead an error is recorded.
+ Glean.testOnlyIpc.aCustomDist.accumulateSamples([-7]);
+ Assert.throws(
+ () => Glean.testOnlyIpc.aCustomDist.testGetValue(),
+ /DataError/
+ );
+});
+
+add_task(function test_fog_custom_pings() {
+ Assert.ok("onePingOnly" in GleanPings);
+ let submitted = false;
+ Glean.testOnly.onePingOneBool.set(false);
+ GleanPings.onePingOnly.testBeforeNextSubmit(reason => {
+ submitted = true;
+ Assert.equal(false, Glean.testOnly.onePingOneBool.testGetValue());
+ });
+ GleanPings.onePingOnly.submit();
+ Assert.ok(submitted, "Ping was submitted, callback was called.");
+});
+
+add_task(function test_recursive_testBeforeNextSubmit() {
+ Assert.ok("onePingOnly" in GleanPings);
+ let submitted = 0;
+ let rec = reason => {
+ submitted++;
+ GleanPings.onePingOnly.testBeforeNextSubmit(rec);
+ };
+ GleanPings.onePingOnly.testBeforeNextSubmit(rec);
+ GleanPings.onePingOnly.submit();
+ GleanPings.onePingOnly.submit();
+ GleanPings.onePingOnly.submit();
+ Assert.equal(3, submitted, "Ping was submitted 3 times");
+ // Be kind and remove the callback.
+ GleanPings.onePingOnly.testBeforeNextSubmit(() => {});
+});
+
+add_task(async function test_fog_timing_distribution_works() {
+ let t1 = Glean.testOnly.whatTimeIsIt.start();
+ let t2 = Glean.testOnly.whatTimeIsIt.start();
+
+ await sleep(5);
+
+ let t3 = Glean.testOnly.whatTimeIsIt.start();
+ Glean.testOnly.whatTimeIsIt.cancel(t1);
+
+ await sleep(5);
+
+ Glean.testOnly.whatTimeIsIt.stopAndAccumulate(t2); // 10ms
+ Glean.testOnly.whatTimeIsIt.stopAndAccumulate(t3); // 5ms
+
+ let data = Glean.testOnly.whatTimeIsIt.testGetValue();
+ const NANOS_IN_MILLIS = 1e6;
+ // bug 1701949 - Sleep gets close, but sometimes doesn't wait long enough.
+ const EPSILON = 40000;
+
+ // Variance in timing makes getting the sum impossible to know.
+ Assert.greater(data.sum, 15 * NANOS_IN_MILLIS - EPSILON);
+
+ // No guarantees from timers means no guarantees on buckets.
+ // But we can guarantee it's only two samples.
+ Assert.equal(
+ 2,
+ Object.entries(data.values).reduce(
+ (acc, [bucket, count]) => acc + count,
+ 0
+ ),
+ "Only two buckets with samples"
+ );
+});
+
+add_task(async function test_fog_labels_conform() {
+ Glean.testOnly.mabelsLabelMaker.singleword.set("portmanteau");
+ Assert.equal(
+ "portmanteau",
+ Glean.testOnly.mabelsLabelMaker.singleword.testGetValue()
+ );
+ Glean.testOnly.mabelsLabelMaker.snake_case.set("snek");
+ Assert.equal(
+ "snek",
+ Glean.testOnly.mabelsLabelMaker.snake_case.testGetValue()
+ );
+ Glean.testOnly.mabelsLabelMaker["dash-character"].set("Dash Rendar");
+ Assert.equal(
+ "Dash Rendar",
+ Glean.testOnly.mabelsLabelMaker["dash-character"].testGetValue()
+ );
+ Glean.testOnly.mabelsLabelMaker["dot.separated"].set("dot product");
+ Assert.equal(
+ "dot product",
+ Glean.testOnly.mabelsLabelMaker["dot.separated"].testGetValue()
+ );
+ Glean.testOnly.mabelsLabelMaker.camelCase.set("wednesday");
+ Assert.equal(
+ "wednesday",
+ Glean.testOnly.mabelsLabelMaker.camelCase.testGetValue()
+ );
+ const veryLong = "1".repeat(72);
+ Glean.testOnly.mabelsLabelMaker[veryLong].set("seventy-two");
+ Assert.throws(
+ () => Glean.testOnly.mabelsLabelMaker[veryLong].testGetValue(),
+ /DataError/,
+ "Should throw because of an invalid label."
+ );
+ // This test should _now_ throw because we are calling data after an invalid
+ // label has been set.
+ Assert.throws(
+ () => Glean.testOnly.mabelsLabelMaker["dot.separated"].testGetValue(),
+ /DataError/,
+ "Should throw because of an invalid label."
+ );
+});
+
+add_task(async function test_fog_labeled_boolean_works() {
+ Assert.equal(
+ undefined,
+ Glean.testOnly.mabelsLikeBalloons.at_parties.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.testOnly.mabelsLikeBalloons.at_parties.set(true);
+ Glean.testOnly.mabelsLikeBalloons.at_funerals.set(false);
+ Assert.equal(
+ true,
+ Glean.testOnly.mabelsLikeBalloons.at_parties.testGetValue()
+ );
+ Assert.equal(
+ false,
+ Glean.testOnly.mabelsLikeBalloons.at_funerals.testGetValue()
+ );
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.testOnly.mabelsLikeBalloons.__other__.testGetValue()
+ );
+ Glean.testOnly.mabelsLikeBalloons["1".repeat(72)].set(true);
+ Assert.throws(
+ () => Glean.testOnly.mabelsLikeBalloons.__other__.testGetValue(),
+ /DataError/,
+ "Should throw because of a recording error."
+ );
+});
+
+add_task(async function test_fog_labeled_counter_works() {
+ Assert.equal(
+ undefined,
+ Glean.testOnly.mabelsKitchenCounters.near_the_sink.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.testOnly.mabelsKitchenCounters.near_the_sink.add(1);
+ Glean.testOnly.mabelsKitchenCounters.with_junk_on_them.add(2);
+ Assert.equal(
+ 1,
+ Glean.testOnly.mabelsKitchenCounters.near_the_sink.testGetValue()
+ );
+ Assert.equal(
+ 2,
+ Glean.testOnly.mabelsKitchenCounters.with_junk_on_them.testGetValue()
+ );
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.testOnly.mabelsKitchenCounters.__other__.testGetValue()
+ );
+ Glean.testOnly.mabelsKitchenCounters["1".repeat(72)].add(1);
+ Assert.throws(
+ () => Glean.testOnly.mabelsKitchenCounters.__other__.testGetValue(),
+ /DataError/,
+ "Should throw because of a recording error."
+ );
+});
+
+add_task(async function test_fog_labeled_string_works() {
+ Assert.equal(
+ undefined,
+ Glean.testOnly.mabelsBalloonStrings.colour_of_99.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.testOnly.mabelsBalloonStrings.colour_of_99.set("crimson");
+ Glean.testOnly.mabelsBalloonStrings.string_lengths.set("various");
+ Assert.equal(
+ "crimson",
+ Glean.testOnly.mabelsBalloonStrings.colour_of_99.testGetValue()
+ );
+ Assert.equal(
+ "various",
+ Glean.testOnly.mabelsBalloonStrings.string_lengths.testGetValue()
+ );
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.testOnly.mabelsBalloonStrings.__other__.testGetValue()
+ );
+ Glean.testOnly.mabelsBalloonStrings["1".repeat(72)].set("valid");
+ Assert.throws(
+ () => Glean.testOnly.mabelsBalloonStrings.__other__.testGetValue(),
+ /DataError/
+ );
+});
+
+add_task(function test_fog_quantity_works() {
+ Glean.testOnly.meaningOfLife.set(42);
+ Assert.equal(42, Glean.testOnly.meaningOfLife.testGetValue());
+});
+
+add_task(function test_fog_rate_works() {
+ // 1) Standard rate with internal denominator
+ Glean.testOnlyIpc.irate.addToNumerator(22);
+ Glean.testOnlyIpc.irate.addToDenominator(7);
+ Assert.deepEqual(
+ { numerator: 22, denominator: 7 },
+ Glean.testOnlyIpc.irate.testGetValue()
+ );
+
+ // 2) Rate with external denominator
+ Glean.testOnlyIpc.anExternalDenominator.add(11);
+ Glean.testOnlyIpc.rateWithExternalDenominator.addToNumerator(121);
+ Assert.equal(11, Glean.testOnlyIpc.anExternalDenominator.testGetValue());
+ Assert.deepEqual(
+ { numerator: 121, denominator: 11 },
+ Glean.testOnlyIpc.rateWithExternalDenominator.testGetValue()
+ );
+});
+
+add_task(async function test_fog_url_works() {
+ const value = "https://www.example.com/fog";
+ Glean.testOnlyIpc.aUrl.set(value);
+
+ Assert.equal(value, Glean.testOnlyIpc.aUrl.testGetValue("store1"));
+});
+
+add_task(async function test_fog_text_works() {
+ const value =
+ "Before the risin' sun, we fly, So many roads to choose, We'll start out walkin' and learn to run, (We've only just begun)";
+ Glean.testOnlyIpc.aText.set(value);
+
+ let rslt = Glean.testOnlyIpc.aText.testGetValue();
+
+ Assert.equal(value, rslt);
+
+ Assert.equal(121, rslt.length);
+});
+
+add_task(async function test_fog_text_works_unusual_character() {
+ const value =
+ "The secret to Dominique Ansel's viennoiserie is the use of Isigny Sainte-Mère butter and Les Grands Moulins de Paris flour";
+ Glean.testOnlyIpc.aText.set(value);
+
+ let rslt = Glean.testOnlyIpc.aText.testGetValue();
+
+ Assert.equal(value, rslt);
+
+ Assert.greater(rslt.length, 100);
+});
diff --git a/toolkit/components/glean/tests/xpcshell/test_GleanExperiments.js b/toolkit/components/glean/tests/xpcshell/test_GleanExperiments.js
new file mode 100644
index 0000000000..cf8871e9d1
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_GleanExperiments.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// FOG needs a profile directory to put its data in.
+do_get_profile();
+
+// We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
+Services.fog.initializeFOG();
+
+add_task(function test_fog_experiment_annotations() {
+ const id = "my-experiment-id";
+ const branch = "my-branch";
+ const extra = { extra_key: "extra_value" };
+ Services.fog.setExperimentActive(id, branch, extra);
+
+ let data = Services.fog.testGetExperimentData(id);
+ Assert.equal(data.branch, branch);
+ Assert.deepEqual(data.extra, extra);
+
+ // Unknown id gets nothing.
+ Assert.equal(undefined, Services.fog.testGetExperimentData(id + id));
+
+ // Inactive id gets nothing.
+ Services.fog.setExperimentInactive(id);
+ Assert.equal(undefined, Services.fog.testGetExperimentData(id));
+});
diff --git a/toolkit/components/glean/tests/xpcshell/test_GleanIPC.js b/toolkit/components/glean/tests/xpcshell/test_GleanIPC.js
new file mode 100644
index 0000000000..3d665c23b9
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_GleanIPC.js
@@ -0,0 +1,157 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+function sleep(ms) {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+add_setup(
+ /* on Android FOG is set up through head.js */
+ { skip_if: () => !runningInParent || AppConstants.platform == "android" },
+ function test_setup() {
+ // Give FOG a temp profile to init within.
+ do_get_profile();
+
+ // We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
+ Services.fog.initializeFOG();
+ }
+);
+
+const BAD_CODE_COUNT = 42;
+const CHEESY_STRING = "a very cheesy string!";
+const CHEESIER_STRING = "a much cheesier string!";
+const EVENT_EXTRA = { extra1: "so very extra" };
+const MEMORIES = [13, 31];
+const MEMORY_BUCKETS = ["13509772", "32131834"]; // buckets are strings : |
+const COUNTERS_NEAR_THE_SINK = 3;
+const COUNTERS_WITH_JUNK_ON_THEM = 5;
+const INVALID_COUNTERS = 7;
+
+add_task({ skip_if: () => runningInParent }, async function run_child_stuff() {
+ Glean.testOnly.badCode.add(BAD_CODE_COUNT);
+ Glean.testOnly.cheesyStringList.add(CHEESY_STRING);
+ Glean.testOnly.cheesyStringList.add(CHEESIER_STRING);
+
+ Glean.testOnlyIpc.noExtraEvent.record();
+ Glean.testOnlyIpc.anEvent.record(EVENT_EXTRA);
+
+ for (let memory of MEMORIES) {
+ Glean.testOnly.doYouRemember.accumulate(memory);
+ }
+
+ let t1 = Glean.testOnly.whatTimeIsIt.start();
+ let t2 = Glean.testOnly.whatTimeIsIt.start();
+
+ await sleep(5);
+
+ let t3 = Glean.testOnly.whatTimeIsIt.start();
+ Glean.testOnly.whatTimeIsIt.cancel(t1);
+
+ await sleep(5);
+
+ Glean.testOnly.whatTimeIsIt.stopAndAccumulate(t2); // 10ms
+ Glean.testOnly.whatTimeIsIt.stopAndAccumulate(t3); // 5ms
+
+ Glean.testOnlyIpc.aCustomDist.accumulateSamples([3, 4]);
+
+ Glean.testOnly.mabelsKitchenCounters.near_the_sink.add(
+ COUNTERS_NEAR_THE_SINK
+ );
+ Glean.testOnly.mabelsKitchenCounters.with_junk_on_them.add(
+ COUNTERS_WITH_JUNK_ON_THEM
+ );
+
+ Glean.testOnly.mabelsBathroomCounters["1".repeat(72)].add(INVALID_COUNTERS);
+
+ Glean.testOnlyIpc.irate.addToNumerator(44);
+ Glean.testOnlyIpc.irate.addToDenominator(14);
+});
+
+add_task(
+ { skip_if: () => !runningInParent },
+ async function test_child_metrics() {
+ await run_test_in_child("test_GleanIPC.js");
+ await Services.fog.testFlushAllChildren();
+
+ Assert.equal(Glean.testOnly.badCode.testGetValue(), BAD_CODE_COUNT);
+
+ // Note: this will break if string list ever rearranges its items.
+ const cheesyStrings = Glean.testOnly.cheesyStringList.testGetValue();
+ Assert.deepEqual(cheesyStrings, [CHEESY_STRING, CHEESIER_STRING]);
+
+ const data = Glean.testOnly.doYouRemember.testGetValue();
+ Assert.equal(MEMORIES.reduce((a, b) => a + b, 0) * 1024 * 1024, data.sum);
+ for (let [bucket, count] of Object.entries(data.values)) {
+ // We could assert instead, but let's skip to save the logspam.
+ if (count == 0) {
+ continue;
+ }
+ Assert.ok(count == 1 && MEMORY_BUCKETS.includes(bucket));
+ }
+
+ const customData = Glean.testOnlyIpc.aCustomDist.testGetValue("store1");
+ Assert.equal(3 + 4, customData.sum, "Sum's correct");
+ for (let [bucket, count] of Object.entries(customData.values)) {
+ Assert.ok(
+ count == 0 || (count == 2 && bucket == 1), // both values in the low bucket
+ `Only two buckets have a sample ${bucket} ${count}`
+ );
+ }
+
+ var events = Glean.testOnlyIpc.noExtraEvent.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("test_only.ipc", events[0].category);
+ Assert.equal("no_extra_event", events[0].name);
+
+ events = Glean.testOnlyIpc.anEvent.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("test_only.ipc", events[0].category);
+ Assert.equal("an_event", events[0].name);
+ Assert.deepEqual(EVENT_EXTRA, events[0].extra);
+
+ const NANOS_IN_MILLIS = 1e6;
+ const EPSILON = 40000; // bug 1701949
+ const times = Glean.testOnly.whatTimeIsIt.testGetValue();
+ Assert.greater(times.sum, 15 * NANOS_IN_MILLIS - EPSILON);
+ // We can't guarantee any specific time values (thank you clocks),
+ // but we can assert there are only two samples.
+ Assert.equal(
+ 2,
+ Object.entries(times.values).reduce(
+ (acc, [bucket, count]) => acc + count,
+ 0
+ )
+ );
+
+ const mabelsCounters = Glean.testOnly.mabelsKitchenCounters;
+ Assert.equal(
+ mabelsCounters.near_the_sink.testGetValue(),
+ COUNTERS_NEAR_THE_SINK
+ );
+ Assert.equal(
+ mabelsCounters.with_junk_on_them.testGetValue(),
+ COUNTERS_WITH_JUNK_ON_THEM
+ );
+
+ Assert.throws(
+ () => Glean.testOnly.mabelsBathroomCounters.__other__.testGetValue(),
+ /DataError/,
+ "Invalid labels record errors, which throw"
+ );
+
+ Assert.deepEqual(
+ { numerator: 44, denominator: 14 },
+ Glean.testOnlyIpc.irate.testGetValue()
+ );
+ }
+);
diff --git a/toolkit/components/glean/tests/xpcshell/test_GleanServerKnobs.js b/toolkit/components/glean/tests/xpcshell/test_GleanServerKnobs.js
new file mode 100644
index 0000000000..31ed262798
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_GleanServerKnobs.js
@@ -0,0 +1,164 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(function test_setup() {
+ // Give FOG a temp profile to init within.
+ do_get_profile();
+
+ // We need to initialize it once, otherwise operations will be stuck in
+ // the pre-init queue.
+ Services.fog.initializeFOG();
+});
+
+add_task(function test_fog_metrics_disabled_remotely() {
+ // Set a cheesy string in the test metric. This should record because the
+ // metric has `disabled: false` by default.
+ const str1 = "a cheesy string!";
+ Glean.testOnly.cheesyString.set(str1);
+ Assert.equal(str1, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+
+ // Create and set a feature configuration that disables the test metric.
+ const feature_config = {
+ "test_only.cheesy_string": false,
+ };
+ Services.fog.setMetricsFeatureConfig(JSON.stringify(feature_config));
+
+ // Attempt to set another cheesy string in the test metric. This should not
+ // record because of the override to the metric's default value in the
+ // feature configuration.
+ const str2 = "another cheesy string!";
+ Glean.testOnly.cheesyString.set(str2);
+ Assert.equal(str1, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+
+ // Reset everything so it doesn't interfere with other tests.
+ Services.fog.testResetFOG();
+});
+
+add_task(function test_fog_multiple_metrics_disabled_remotely() {
+ // Set some test metrics. This should record because the metrics are
+ // `disabled: false` by default.
+ const str1 = "yet another a cheesy string!";
+ Glean.testOnly.cheesyString.set(str1);
+ Assert.equal(str1, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+ const qty1 = 42;
+ Glean.testOnly.meaningOfLife.set(qty1);
+ Assert.equal(qty1, Glean.testOnly.meaningOfLife.testGetValue("test-ping"));
+
+ // Create and set a feature configuration that disables multiple test
+ // metrics.
+ var feature_config = {
+ "test_only.cheesy_string": false,
+ "test_only.meaning_of_life": false,
+ };
+ Services.fog.setMetricsFeatureConfig(JSON.stringify(feature_config));
+
+ // Attempt to set the metrics again. This should not record because of the
+ // override to the metrics' default value in the feature configuration.
+ const str2 = "another cheesy string v2!";
+ Glean.testOnly.cheesyString.set(str2);
+ Assert.equal(str1, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+ const qty2 = 52;
+ Glean.testOnly.meaningOfLife.set(qty2);
+ Assert.equal(qty1, Glean.testOnly.meaningOfLife.testGetValue("test-ping"));
+
+ // Change the feature configuration to re-enable the `cheesy_string` metric.
+ feature_config = {
+ "test_only.cheesy_string": true,
+ "test_only.meaning_of_life": false,
+ };
+ Services.fog.setMetricsFeatureConfig(JSON.stringify(feature_config));
+
+ // Attempt to set the metrics again. This should only record `cheesy_string`
+ // because of the most recent feature configuration.
+ const str3 = "another cheesy string v3!";
+ Glean.testOnly.cheesyString.set(str3);
+ Assert.equal(str3, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+ const qty3 = 62;
+ Glean.testOnly.meaningOfLife.set(qty3);
+ Assert.equal(qty1, Glean.testOnly.meaningOfLife.testGetValue("test-ping"));
+
+ // Reset everything so it doesn't interfere with other tests.
+ Services.fog.testResetFOG();
+
+ // Set some final metrics. This should record in both metrics because they
+ // are both `disabled: false` by default.
+ const str4 = "another a cheesy string v4";
+ Glean.testOnly.cheesyString.set(str4);
+ Assert.equal(str4, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+ const qty4 = 72;
+ Glean.testOnly.meaningOfLife.set(qty4);
+ Assert.equal(qty4, Glean.testOnly.meaningOfLife.testGetValue("test-ping"));
+});
+
+add_task(function test_fog_metrics_feature_config_api_handles_null_values() {
+ // Set a cheesy string in the test metric. This should record because the
+ // metric has `disabled: false` by default.
+ const str1 = "a cheesy string!";
+ Glean.testOnly.cheesyString.set(str1);
+ Assert.equal(str1, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+
+ // Create and set a feature configuration that disables the test metric.
+ const feature_config = {
+ "test_only.cheesy_string": false,
+ };
+ Services.fog.setMetricsFeatureConfig(JSON.stringify(feature_config));
+
+ // Attempt to set another cheesy string in the test metric. This should not
+ // record because of the override to the metric's default value in the
+ // feature configuration.
+ const str2 = "another cheesy string v2";
+ Glean.testOnly.cheesyString.set(str2);
+ Assert.equal(str1, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+
+ // Set the configuration to `null`.
+ Services.fog.setMetricsFeatureConfig(null);
+
+ // Attempt to set another cheesy string in the test metric. This should now
+ // record because `null` doesn't change already existing configuration.
+ Glean.testOnly.cheesyString.set(str2);
+ Assert.equal(str1, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+
+ // Set the configuration to `""` to replicate getting an empty string from
+ // Nimbus.
+ Services.fog.setMetricsFeatureConfig("");
+
+ // Attempt to set another cheesy string in the test metric. This should now
+ // record again because `""` doesn't change already existing configuration.
+ const str3 = "another cheesy string v3";
+ Glean.testOnly.cheesyString.set(str3);
+ Assert.equal(str1, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+});
+
+add_task(function test_fog_metrics_disabled_reset_fog_behavior() {
+ // Set a cheesy string in the test metric. This should record because the
+ // metric has `disabled: false` by default.
+ const str1 = "a cheesy string!";
+ Glean.testOnly.cheesyString.set(str1);
+ Assert.equal(str1, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+
+ // Create and set a feature configuration that disables the test metric.
+ const feature_config = {
+ "test_only.cheesy_string": false,
+ };
+ Services.fog.setMetricsFeatureConfig(JSON.stringify(feature_config));
+
+ // Attempt to set another cheesy string in the test metric. This should not
+ // record because of the override to the metric's default value in the
+ // feature configuration.
+ const str2 = "another cheesy string!";
+ Glean.testOnly.cheesyString.set(str2);
+ Assert.equal(str1, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+
+ // Now reset FOG to ensure that the feature configuration is also reset.
+ Services.fog.testResetFOG();
+
+ // Attempt to set the string again in the test metric. This should now
+ // record normally because we reset FOG.
+ Glean.testOnly.cheesyString.set(str2);
+ Assert.equal(str2, Glean.testOnly.cheesyString.testGetValue("test-ping"));
+
+ // Reset everything so it doesn't interfere with other tests.
+ Services.fog.testResetFOG();
+});
diff --git a/toolkit/components/glean/tests/xpcshell/test_JOG.js b/toolkit/components/glean/tests/xpcshell/test_JOG.js
new file mode 100644
index 0000000000..53b1d25962
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_JOG.js
@@ -0,0 +1,728 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+function sleep(ms) {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+add_task(
+ /* on Android FOG is set up through head.js */
+ { skip_if: () => AppConstants.platform == "android" },
+ function test_setup() {
+ // FOG needs a profile directory to put its data in.
+ do_get_profile();
+
+ // We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
+ Services.fog.initializeFOG();
+ }
+);
+
+add_task(function test_jog_counter_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "counter",
+ "jog_cat",
+ "jog_counter",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Glean.jogCat.jogCounter.add(53);
+ Assert.equal(53, Glean.jogCat.jogCounter.testGetValue());
+});
+
+add_task(async function test_jog_string_works() {
+ const value = "an active string!";
+ Services.fog.testRegisterRuntimeMetric(
+ "string",
+ "jog_cat",
+ "jog_string",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Glean.jogCat.jogString.set(value);
+
+ Assert.equal(value, Glean.jogCat.jogString.testGetValue());
+});
+
+add_task(async function test_jog_string_list_works() {
+ const value = "an active string!";
+ const value2 = "a more active string!";
+ const value3 = "the most active of strings.";
+ Services.fog.testRegisterRuntimeMetric(
+ "string_list",
+ "jog_cat",
+ "jog_string_list",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+
+ const jogList = [value, value2];
+ Glean.jogCat.jogStringList.set(jogList);
+
+ let val = Glean.jogCat.jogStringList.testGetValue();
+ // Note: This is incredibly fragile and will break if we ever rearrange items
+ // in the string list.
+ Assert.deepEqual(jogList, val);
+
+ Glean.jogCat.jogStringList.add(value3);
+ Assert.ok(Glean.jogCat.jogStringList.testGetValue().includes(value3));
+});
+
+add_task(async function test_jog_timespan_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "timespan",
+ "jog_cat",
+ "jog_timespan",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ time_unit: "millisecond" })
+ );
+ Glean.jogCat.jogTimespan.start();
+ Glean.jogCat.jogTimespan.cancel();
+ Assert.equal(undefined, Glean.jogCat.jogTimespan.testGetValue());
+
+ // We start, briefly sleep and then stop.
+ // That guarantees some time to measure.
+ Glean.jogCat.jogTimespan.start();
+ await sleep(10);
+ Glean.jogCat.jogTimespan.stop();
+
+ Assert.ok(Glean.jogCat.jogTimespan.testGetValue() > 0);
+});
+
+add_task(async function test_jog_uuid_works() {
+ const kTestUuid = "decafdec-afde-cafd-ecaf-decafdecafde";
+ Services.fog.testRegisterRuntimeMetric(
+ "uuid",
+ "jog_cat",
+ "jog_uuid",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Glean.jogCat.jogUuid.set(kTestUuid);
+ Assert.equal(kTestUuid, Glean.jogCat.jogUuid.testGetValue());
+
+ Glean.jogCat.jogUuid.generateAndSet();
+ // Since we generate v4 UUIDs, and the first character of the third group
+ // isn't 4, this won't ever collide with kTestUuid.
+ Assert.notEqual(kTestUuid, Glean.jogCat.jogUuid.testGetValue());
+});
+
+add_task(function test_jog_datetime_works() {
+ const value = new Date("2020-06-11T12:00:00");
+ Services.fog.testRegisterRuntimeMetric(
+ "datetime",
+ "jog_cat",
+ "jog_datetime",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ time_unit: "nanosecond" })
+ );
+
+ Glean.jogCat.jogDatetime.set(value.getTime() * 1000);
+
+ const received = Glean.jogCat.jogDatetime.testGetValue();
+ Assert.equal(received.getTime(), value.getTime());
+});
+
+add_task(function test_jog_boolean_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "boolean",
+ "jog_cat",
+ "jog_bool",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Glean.jogCat.jogBool.set(false);
+ Assert.equal(false, Glean.jogCat.jogBool.testGetValue());
+});
+
+add_task(async function test_jog_event_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "event",
+ "jog_cat",
+ "jog_event_no_extra",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Glean.jogCat.jogEventNoExtra.record();
+ var events = Glean.jogCat.jogEventNoExtra.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("jog_cat", events[0].category);
+ Assert.equal("jog_event_no_extra", events[0].name);
+
+ Services.fog.testRegisterRuntimeMetric(
+ "event",
+ "jog_cat",
+ "jog_event",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ allowed_extra_keys: ["extra1", "extra2"] })
+ );
+ let extra = { extra1: "can set extras", extra2: "passing more data" };
+ Glean.jogCat.jogEvent.record(extra);
+ events = Glean.jogCat.jogEvent.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("jog_cat", events[0].category);
+ Assert.equal("jog_event", events[0].name);
+ Assert.deepEqual(extra, events[0].extra);
+
+ Services.fog.testRegisterRuntimeMetric(
+ "event",
+ "jog_cat",
+ "jog_event_with_extra",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({
+ allowed_extra_keys: ["extra1", "extra2", "extra3_longer_name"],
+ })
+ );
+ let extra2 = {
+ extra1: "can set extras",
+ extra2: 37,
+ extra3_longer_name: false,
+ };
+ Glean.jogCat.jogEventWithExtra.record(extra2);
+ events = Glean.jogCat.jogEventWithExtra.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("jog_cat", events[0].category);
+ Assert.equal("jog_event_with_extra", events[0].name);
+ let expectedExtra = {
+ extra1: "can set extras",
+ extra2: "37",
+ extra3_longer_name: "false",
+ };
+ Assert.deepEqual(expectedExtra, events[0].extra);
+
+ // Invalid extra keys don't crash, the event is not recorded.
+ let extra3 = {
+ extra1_nonexistent_extra: "this does not crash",
+ };
+ Glean.jogCat.jogEventWithExtra.record(extra3);
+ // And test methods throw appropriately
+ Assert.throws(
+ () => Glean.jogCat.jogEventWithExtra.testGetValue(),
+ /DataError/
+ );
+});
+
+add_task(async function test_jog_memory_distribution_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "memory_distribution",
+ "jog_cat",
+ "jog_memory_dist",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ memory_unit: "megabyte" })
+ );
+ Glean.jogCat.jogMemoryDist.accumulate(7);
+ Glean.jogCat.jogMemoryDist.accumulate(17);
+
+ let data = Glean.jogCat.jogMemoryDist.testGetValue();
+ // `data.sum` is in bytes, but the metric is in MB.
+ Assert.equal(24 * 1024 * 1024, data.sum, "Sum's correct");
+ for (let [bucket, count] of Object.entries(data.values)) {
+ Assert.ok(
+ count == 0 || (count == 1 && (bucket == 17520006 || bucket == 7053950)),
+ "Only two buckets have a sample"
+ );
+ }
+});
+
+add_task(async function test_jog_custom_distribution_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "custom_distribution",
+ "jog_cat",
+ "jog_custom_dist",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({
+ range_min: 1,
+ range_max: 2147483646,
+ bucket_count: 10,
+ histogram_type: "linear",
+ })
+ );
+ Glean.jogCat.jogCustomDist.accumulateSamples([7, 268435458]);
+
+ let data = Glean.jogCat.jogCustomDist.testGetValue();
+ Assert.equal(7 + 268435458, data.sum, "Sum's correct");
+ for (let [bucket, count] of Object.entries(data.values)) {
+ Assert.ok(
+ count == 0 || (count == 1 && (bucket == 1 || bucket == 268435456)),
+ `Only two buckets have a sample ${bucket} ${count}`
+ );
+ }
+
+ // Negative values will not be recorded, instead an error is recorded.
+ Glean.jogCat.jogCustomDist.accumulateSamples([-7]);
+ Assert.throws(() => Glean.jogCat.jogCustomDist.testGetValue(), /DataError/);
+});
+
+add_task(async function test_jog_custom_pings() {
+ Services.fog.testRegisterRuntimeMetric(
+ "boolean",
+ "jog_cat",
+ "jog_ping_bool",
+ ["jog-ping"],
+ `"ping"`,
+ false
+ );
+ Services.fog.testRegisterRuntimePing("jog-ping", true, true, true, []);
+ Assert.ok("jogPing" in GleanPings);
+ let submitted = false;
+ Glean.jogCat.jogPingBool.set(false);
+ GleanPings.jogPing.testBeforeNextSubmit(reason => {
+ submitted = true;
+ Assert.equal(false, Glean.jogCat.jogPingBool.testGetValue());
+ });
+ GleanPings.jogPing.submit();
+ Assert.ok(submitted, "Ping was submitted, callback was called.");
+ // ping-lifetime value was cleared.
+ Assert.equal(undefined, Glean.jogCat.jogPingBool.testGetValue());
+});
+
+add_task(async function test_jog_timing_distribution_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "timing_distribution",
+ "jog_cat",
+ "jog_timing_dist",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ time_unit: "microsecond" })
+ );
+ let t1 = Glean.jogCat.jogTimingDist.start();
+ let t2 = Glean.jogCat.jogTimingDist.start();
+
+ await sleep(5);
+
+ let t3 = Glean.jogCat.jogTimingDist.start();
+ Glean.jogCat.jogTimingDist.cancel(t1);
+
+ await sleep(5);
+
+ Glean.jogCat.jogTimingDist.stopAndAccumulate(t2); // 10ms
+ Glean.jogCat.jogTimingDist.stopAndAccumulate(t3); // 5ms
+
+ let data = Glean.jogCat.jogTimingDist.testGetValue();
+ const NANOS_IN_MILLIS = 1e6;
+ // bug 1701949 - Sleep gets close, but sometimes doesn't wait long enough.
+ const EPSILON = 40000;
+
+ // Variance in timing makes getting the sum impossible to know.
+ Assert.greater(data.sum, 15 * NANOS_IN_MILLIS - EPSILON);
+
+ // No guarantees from timers means no guarantees on buckets.
+ // But we can guarantee it's only two samples.
+ Assert.equal(
+ 2,
+ Object.entries(data.values).reduce(
+ (acc, [bucket, count]) => acc + count,
+ 0
+ ),
+ "Only two buckets with samples"
+ );
+});
+
+add_task(async function test_jog_labeled_boolean_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "labeled_boolean",
+ "jog_cat",
+ "jog_labeled_bool",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledBool.label_1.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.jogCat.jogLabeledBool.label_1.set(true);
+ Glean.jogCat.jogLabeledBool.label_2.set(false);
+ Assert.equal(true, Glean.jogCat.jogLabeledBool.label_1.testGetValue());
+ Assert.equal(false, Glean.jogCat.jogLabeledBool.label_2.testGetValue());
+ // What about invalid/__other__?
+ Assert.equal(undefined, Glean.jogCat.jogLabeledBool.__other__.testGetValue());
+ Glean.jogCat.jogLabeledBool.NowValidLabel.set(true);
+ Assert.ok(Glean.jogCat.jogLabeledBool.NowValidLabel.testGetValue());
+ Glean.jogCat.jogLabeledBool["1".repeat(72)].set(true);
+ Assert.throws(
+ () => Glean.jogCat.jogLabeledBool.__other__.testGetValue(),
+ /DataError/,
+ "Should throw because of a recording error."
+ );
+});
+
+add_task(async function test_jog_labeled_boolean_with_static_labels_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "labeled_boolean",
+ "jog_cat",
+ "jog_labeled_bool_with_labels",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ ordered_labels: ["label_1", "label_2"] })
+ );
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledBoolWithLabels.label_1.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.jogCat.jogLabeledBoolWithLabels.label_1.set(true);
+ Glean.jogCat.jogLabeledBoolWithLabels.label_2.set(false);
+ Assert.equal(
+ true,
+ Glean.jogCat.jogLabeledBoolWithLabels.label_1.testGetValue()
+ );
+ Assert.equal(
+ false,
+ Glean.jogCat.jogLabeledBoolWithLabels.label_2.testGetValue()
+ );
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledBoolWithLabels.__other__.testGetValue()
+ );
+ Glean.jogCat.jogLabeledBoolWithLabels.label_3.set(true);
+ Assert.equal(
+ true,
+ Glean.jogCat.jogLabeledBoolWithLabels.__other__.testGetValue()
+ );
+ // TODO: Test that we have the right number and type of errors (bug 1683171)
+});
+
+add_task(async function test_jog_labeled_counter_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "labeled_counter",
+ "jog_cat",
+ "jog_labeled_counter",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledCounter.label_1.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.jogCat.jogLabeledCounter.label_1.add(1);
+ Glean.jogCat.jogLabeledCounter.label_2.add(2);
+ Assert.equal(1, Glean.jogCat.jogLabeledCounter.label_1.testGetValue());
+ Assert.equal(2, Glean.jogCat.jogLabeledCounter.label_2.testGetValue());
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledCounter.__other__.testGetValue()
+ );
+ Glean.jogCat.jogLabeledCounter["1".repeat(72)].add(1);
+ Assert.throws(
+ () => Glean.jogCat.jogLabeledCounter.__other__.testGetValue(),
+ /DataError/,
+ "Should throw because of a recording error."
+ );
+});
+
+add_task(async function test_jog_labeled_counter_with_static_labels_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "labeled_counter",
+ "jog_cat",
+ "jog_labeled_counter_with_labels",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ ordered_labels: ["label_1", "label_2"] })
+ );
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledCounterWithLabels.label_1.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.jogCat.jogLabeledCounterWithLabels.label_1.add(1);
+ Glean.jogCat.jogLabeledCounterWithLabels.label_2.add(2);
+ Assert.equal(
+ 1,
+ Glean.jogCat.jogLabeledCounterWithLabels.label_1.testGetValue()
+ );
+ Assert.equal(
+ 2,
+ Glean.jogCat.jogLabeledCounterWithLabels.label_2.testGetValue()
+ );
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledCounterWithLabels.__other__.testGetValue()
+ );
+ Glean.jogCat.jogLabeledCounterWithLabels["1".repeat(72)].add(1);
+ // TODO:(bug 1766515) - This should throw.
+ /*Assert.throws(
+ () => Glean.jogCat.jogLabeledCounterWithLabels.__other__.testGetValue(),
+ /DataError/,
+ "Should throw because of a recording error."
+ );*/
+ Assert.equal(
+ 1,
+ Glean.jogCat.jogLabeledCounterWithLabels.__other__.testGetValue()
+ );
+});
+
+add_task(async function test_jog_labeled_string_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "labeled_string",
+ "jog_cat",
+ "jog_labeled_string",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledString.label_1.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.jogCat.jogLabeledString.label_1.set("crimson");
+ Glean.jogCat.jogLabeledString.label_2.set("various");
+ Assert.equal("crimson", Glean.jogCat.jogLabeledString.label_1.testGetValue());
+ Assert.equal("various", Glean.jogCat.jogLabeledString.label_2.testGetValue());
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledString.__other__.testGetValue()
+ );
+ Glean.jogCat.jogLabeledString["1".repeat(72)].set("valid");
+ Assert.throws(
+ () => Glean.jogCat.jogLabeledString.__other__.testGetValue(),
+ /DataError/
+ );
+});
+
+add_task(async function test_jog_labeled_string_with_labels_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "labeled_string",
+ "jog_cat",
+ "jog_labeled_string_with_labels",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ ordered_labels: ["label_1", "label_2"] })
+ );
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledStringWithLabels.label_1.testGetValue(),
+ "New labels with no values should return undefined"
+ );
+ Glean.jogCat.jogLabeledStringWithLabels.label_1.set("crimson");
+ Glean.jogCat.jogLabeledStringWithLabels.label_2.set("various");
+ Assert.equal(
+ "crimson",
+ Glean.jogCat.jogLabeledStringWithLabels.label_1.testGetValue()
+ );
+ Assert.equal(
+ "various",
+ Glean.jogCat.jogLabeledStringWithLabels.label_2.testGetValue()
+ );
+ // What about invalid/__other__?
+ Assert.equal(
+ undefined,
+ Glean.jogCat.jogLabeledStringWithLabels.__other__.testGetValue()
+ );
+ Glean.jogCat.jogLabeledStringWithLabels["1".repeat(72)].set("valid");
+ // TODO:(bug 1766515) - This should throw.
+ /*Assert.throws(
+ () => Glean.jogCat.jogLabeledStringWithLabels.__other__.testGetValue(),
+ /DataError/
+ );*/
+ Assert.equal(
+ "valid",
+ Glean.jogCat.jogLabeledStringWithLabels.__other__.testGetValue()
+ );
+});
+
+add_task(function test_jog_quantity_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "quantity",
+ "jog_cat",
+ "jog_quantity",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Glean.jogCat.jogQuantity.set(42);
+ Assert.equal(42, Glean.jogCat.jogQuantity.testGetValue());
+});
+
+add_task(function test_jog_rate_works() {
+ Services.fog.testRegisterRuntimeMetric(
+ "rate",
+ "jog_cat",
+ "jog_rate",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ // 1) Standard rate with internal denominator
+ Glean.jogCat.jogRate.addToNumerator(22);
+ Glean.jogCat.jogRate.addToDenominator(7);
+ Assert.deepEqual(
+ { numerator: 22, denominator: 7 },
+ Glean.jogCat.jogRate.testGetValue()
+ );
+
+ Services.fog.testRegisterRuntimeMetric(
+ "denominator",
+ "jog_cat",
+ "jog_denominator",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({
+ numerators: [
+ {
+ name: "jog_rate_ext",
+ category: "jog_cat",
+ send_in_pings: ["test-only"],
+ lifetime: "ping",
+ disabled: false,
+ },
+ ],
+ })
+ );
+ Services.fog.testRegisterRuntimeMetric(
+ "rate",
+ "jog_cat",
+ "jog_rate_ext",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ // 2) Rate with external denominator
+ Glean.jogCat.jogDenominator.add(11);
+ Glean.jogCat.jogRateExt.addToNumerator(121);
+ Assert.equal(11, Glean.jogCat.jogDenominator.testGetValue());
+ Assert.deepEqual(
+ { numerator: 121, denominator: 11 },
+ Glean.jogCat.jogRateExt.testGetValue()
+ );
+});
+
+add_task(function test_jog_dotted_categories_work() {
+ Services.fog.testRegisterRuntimeMetric(
+ "counter",
+ "jog_cat.dotted",
+ "jog_counter",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Glean.jogCatDotted.jogCounter.add(314);
+ Assert.equal(314, Glean.jogCatDotted.jogCounter.testGetValue());
+});
+
+add_task(async function test_jog_ping_works() {
+ const kReason = "reason-1";
+ Services.fog.testRegisterRuntimePing("my-ping", true, true, true, [kReason]);
+ let submitted = false;
+ GleanPings.myPing.testBeforeNextSubmit(reason => {
+ submitted = true;
+ Assert.equal(kReason, reason);
+ });
+ GleanPings.myPing.submit("reason-1");
+ Assert.ok(submitted, "Ping must have been submitted");
+});
+
+add_task(function test_jog_name_collision() {
+ Assert.ok("aCounter" in Glean.testOnlyJog);
+ Assert.equal(undefined, Glean.testOnlyJog.aCounter.testGetValue());
+ const kValue = 42;
+ Glean.testOnlyJog.aCounter.add(kValue);
+ Assert.equal(kValue, Glean.testOnlyJog.aCounter.testGetValue());
+
+ // Let's overwrite the test_only.jog.a_counter counter.
+ Services.fog.testRegisterRuntimeMetric(
+ "counter",
+ "test_only.jog",
+ "a_counter",
+ ["store1"],
+ `"ping"`,
+ true // changing the metric to disabled.
+ );
+
+ Assert.ok("aCounter" in Glean.testOnlyJog);
+ Assert.equal(kValue, Glean.testOnlyJog.aCounter.testGetValue());
+ Glean.testOnlyJog.aCounter.add(kValue);
+ Assert.equal(
+ kValue,
+ Glean.testOnlyJog.aCounter.testGetValue(),
+ "value of now-disabled metric remains unchanged."
+ );
+
+ // Now let's mess with events:
+ Assert.ok("anEvent" in Glean.testOnlyJog);
+ Assert.equal(undefined, Glean.testOnlyJog.anEvent.testGetValue());
+ const extra12 = {
+ extra1: "a value",
+ extra2: "another value",
+ };
+ Glean.testOnlyJog.anEvent.record(extra12);
+ Assert.deepEqual(extra12, Glean.testOnlyJog.anEvent.testGetValue()[0].extra);
+ Services.fog.testRegisterRuntimeMetric(
+ "event",
+ "test_only.jog",
+ "an_event",
+ ["store1"],
+ `"ping"`,
+ false,
+ JSON.stringify({ allowed_extra_keys: ["extra1", "extra2", "extra3"] }) // New extra key just dropped
+ );
+ const extra123 = {
+ extra1: "different value",
+ extra2: "another different value",
+ extra3: 42,
+ };
+ Glean.testOnlyJog.anEvent.record(extra123);
+ Assert.deepEqual(extra123, Glean.testOnlyJog.anEvent.testGetValue()[1].extra);
+});
+
+add_task(function test_enumerable_names() {
+ Assert.ok(Object.keys(Glean).includes("testOnlyJog"));
+ Assert.ok(Object.keys(Glean.testOnlyJog).includes("aCounter"));
+ Assert.ok(Object.keys(GleanPings).includes("testPing"));
+});
+
+add_task(async function test_jog_text_works() {
+ const kValue =
+ "In the heart of the Opéra district in Paris, the Cédric Grolet Opéra bakery-pastry shop is a veritable temple of gourmet delights.";
+ Services.fog.testRegisterRuntimeMetric(
+ "text",
+ "test_only.jog",
+ "a_text",
+ ["test-only"],
+ `"ping"`,
+ false
+ );
+ Glean.testOnlyJog.aText.set(kValue);
+
+ Assert.equal(kValue, Glean.testOnlyJog.aText.testGetValue());
+});
diff --git a/toolkit/components/glean/tests/xpcshell/test_JOGIPC.js b/toolkit/components/glean/tests/xpcshell/test_JOGIPC.js
new file mode 100644
index 0000000000..505dba6825
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_JOGIPC.js
@@ -0,0 +1,266 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+function sleep(ms) {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+add_task(
+ /* on Android FOG is set up through head.js */
+ { skip_if: () => !runningInParent || AppConstants.platform == "android" },
+ function test_setup() {
+ // Give FOG a temp profile to init within.
+ do_get_profile();
+
+ // We need to initialize it once, otherwise operations will be stuck in the pre-init queue.
+ Services.fog.initializeFOG();
+ }
+);
+
+const COUNT = 42;
+const STRING = "a string!";
+const ANOTHER_STRING = "another string!";
+const EVENT_EXTRA = { extra1: "so very extra" };
+const MEMORIES = [13, 31];
+const MEMORY_BUCKETS = ["13509772", "32131834"]; // buckets are strings : |
+const COUNTERS_1 = 3;
+const COUNTERS_2 = 5;
+const INVALID_COUNTERS = 7;
+
+// It is CRUCIAL that we register metrics in the same order in the parent and
+// in the child or their metric ids will not line up and ALL WILL EXPLODE.
+const METRICS = [
+ ["counter", "jog_ipc", "jog_counter", ["test-only"], `"ping"`, false],
+ ["string_list", "jog_ipc", "jog_string_list", ["test-only"], `"ping"`, false],
+ ["event", "jog_ipc", "jog_event_no_extra", ["test-only"], `"ping"`, false],
+ [
+ "event",
+ "jog_ipc",
+ "jog_event",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ allowed_extra_keys: ["extra1"] }),
+ ],
+ [
+ "memory_distribution",
+ "jog_ipc",
+ "jog_memory_dist",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ memory_unit: "megabyte" }),
+ ],
+ [
+ "timing_distribution",
+ "jog_ipc",
+ "jog_timing_dist",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ time_unit: "nanosecond" }),
+ ],
+ [
+ "custom_distribution",
+ "jog_ipc",
+ "jog_custom_dist",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({
+ range_min: 1,
+ range_max: 2147483646,
+ bucket_count: 10,
+ histogram_type: "linear",
+ }),
+ ],
+ [
+ "labeled_counter",
+ "jog_ipc",
+ "jog_labeled_counter",
+ ["test-only"],
+ `"ping"`,
+ false,
+ ],
+ [
+ "labeled_counter",
+ "jog_ipc",
+ "jog_labeled_counter_err",
+ ["test-only"],
+ `"ping"`,
+ false,
+ ],
+ [
+ "labeled_counter",
+ "jog_ipc",
+ "jog_labeled_counter_with_labels",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ ordered_labels: ["label_1", "label_2"] }),
+ ],
+ [
+ "labeled_counter",
+ "jog_ipc",
+ "jog_labeled_counter_with_labels_err",
+ ["test-only"],
+ `"ping"`,
+ false,
+ JSON.stringify({ ordered_labels: ["label_1", "label_2"] }),
+ ],
+ ["rate", "jog_ipc", "jog_rate", ["test-only"], `"ping"`, false],
+];
+
+add_task({ skip_if: () => runningInParent }, async function run_child_stuff() {
+ // Ensure any _actual_ runtime metrics are registered first.
+ // Otherwise the jog_ipc.* ones will have incorrect ids.
+ Glean.testOnly.badCode;
+ for (let metric of METRICS) {
+ Services.fog.testRegisterRuntimeMetric(...metric);
+ }
+ Glean.jogIpc.jogCounter.add(COUNT);
+ Glean.jogIpc.jogStringList.add(STRING);
+ Glean.jogIpc.jogStringList.add(ANOTHER_STRING);
+
+ Glean.jogIpc.jogEventNoExtra.record();
+ Glean.jogIpc.jogEvent.record(EVENT_EXTRA);
+
+ for (let memory of MEMORIES) {
+ Glean.jogIpc.jogMemoryDist.accumulate(memory);
+ }
+
+ let t1 = Glean.jogIpc.jogTimingDist.start();
+ let t2 = Glean.jogIpc.jogTimingDist.start();
+
+ await sleep(5);
+
+ let t3 = Glean.jogIpc.jogTimingDist.start();
+ Glean.jogIpc.jogTimingDist.cancel(t1);
+
+ await sleep(5);
+
+ Glean.jogIpc.jogTimingDist.stopAndAccumulate(t2); // 10ms
+ Glean.jogIpc.jogTimingDist.stopAndAccumulate(t3); // 5ms
+
+ Glean.jogIpc.jogCustomDist.accumulateSamples([3, 4]);
+
+ Glean.jogIpc.jogLabeledCounter.label_1.add(COUNTERS_1);
+ Glean.jogIpc.jogLabeledCounter.label_2.add(COUNTERS_2);
+
+ Glean.jogIpc.jogLabeledCounterErr["1".repeat(72)].add(INVALID_COUNTERS);
+
+ Glean.jogIpc.jogLabeledCounterWithLabels.label_1.add(COUNTERS_1);
+ Glean.jogIpc.jogLabeledCounterWithLabels.label_2.add(COUNTERS_2);
+
+ Glean.jogIpc.jogLabeledCounterWithLabelsErr["1".repeat(72)].add(
+ INVALID_COUNTERS
+ );
+
+ Glean.jogIpc.jogRate.addToNumerator(44);
+ Glean.jogIpc.jogRate.addToDenominator(14);
+});
+
+add_task(
+ { skip_if: () => !runningInParent },
+ async function test_child_metrics() {
+ // Ensure any _actual_ runtime metrics are registered first.
+ // Otherwise the jog_ipc.* ones will have incorrect ids.
+ Glean.testOnly.badCode;
+ for (let metric of METRICS) {
+ Services.fog.testRegisterRuntimeMetric(...metric);
+ }
+ await run_test_in_child("test_JOGIPC.js");
+ await Services.fog.testFlushAllChildren();
+
+ Assert.equal(Glean.jogIpc.jogCounter.testGetValue(), COUNT);
+
+ // Note: this will break if string list ever rearranges its items.
+ const strings = Glean.jogIpc.jogStringList.testGetValue();
+ Assert.deepEqual(strings, [STRING, ANOTHER_STRING]);
+
+ const data = Glean.jogIpc.jogMemoryDist.testGetValue();
+ Assert.equal(MEMORIES.reduce((a, b) => a + b, 0) * 1024 * 1024, data.sum);
+ for (let [bucket, count] of Object.entries(data.values)) {
+ // We could assert instead, but let's skip to save the logspam.
+ if (count == 0) {
+ continue;
+ }
+ Assert.ok(count == 1 && MEMORY_BUCKETS.includes(bucket));
+ }
+
+ const customData = Glean.jogIpc.jogCustomDist.testGetValue();
+ Assert.equal(3 + 4, customData.sum, "Sum's correct");
+ for (let [bucket, count] of Object.entries(customData.values)) {
+ Assert.ok(
+ count == 0 || (count == 2 && bucket == 1), // both values in the low bucket
+ `Only two buckets have a sample ${bucket} ${count}`
+ );
+ }
+
+ let events = Glean.jogIpc.jogEventNoExtra.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("jog_ipc", events[0].category);
+ Assert.equal("jog_event_no_extra", events[0].name);
+
+ events = Glean.jogIpc.jogEvent.testGetValue();
+ Assert.equal(1, events.length);
+ Assert.equal("jog_ipc", events[0].category);
+ Assert.equal("jog_event", events[0].name);
+ Assert.deepEqual(EVENT_EXTRA, events[0].extra);
+
+ const NANOS_IN_MILLIS = 1e6;
+ const EPSILON = 40000; // bug 1701949
+ const times = Glean.jogIpc.jogTimingDist.testGetValue();
+ Assert.greater(times.sum, 15 * NANOS_IN_MILLIS - EPSILON);
+ // We can't guarantee any specific time values (thank you clocks),
+ // but we can assert there are only two samples.
+ Assert.equal(
+ 2,
+ Object.entries(times.values).reduce(
+ (acc, [bucket, count]) => acc + count,
+ 0
+ )
+ );
+
+ const labeledCounter = Glean.jogIpc.jogLabeledCounter;
+ Assert.equal(labeledCounter.label_1.testGetValue(), COUNTERS_1);
+ Assert.equal(labeledCounter.label_2.testGetValue(), COUNTERS_2);
+
+ Assert.throws(
+ () => Glean.jogIpc.jogLabeledCounterErr.__other__.testGetValue(),
+ /DataError/,
+ "Invalid labels record errors, which throw"
+ );
+
+ const labeledCounterWLabels = Glean.jogIpc.jogLabeledCounterWithLabels;
+ Assert.equal(labeledCounterWLabels.label_1.testGetValue(), COUNTERS_1);
+ Assert.equal(labeledCounterWLabels.label_2.testGetValue(), COUNTERS_2);
+
+ // TODO:(bug 1766515) - This should throw.
+ /*Assert.throws(
+ () =>
+ Glean.jogIpc.jogLabeledCounterWithLabelsErr.__other__.testGetValue(),
+ /DataError/,
+ "Invalid labels record errors, which throw"
+ );*/
+ Assert.equal(
+ Glean.jogIpc.jogLabeledCounterWithLabelsErr.__other__.testGetValue(),
+ INVALID_COUNTERS
+ );
+
+ Assert.deepEqual(
+ { numerator: 44, denominator: 14 },
+ Glean.jogIpc.jogRate.testGetValue()
+ );
+ }
+);
diff --git a/toolkit/components/glean/tests/xpcshell/test_MillionQ.js b/toolkit/components/glean/tests/xpcshell/test_MillionQ.js
new file mode 100644
index 0000000000..d98e73b451
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/test_MillionQ.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function test_queue_longer_than_1k() {
+ // FOG needs a profile directory to put its data in.
+ do_get_profile();
+
+ // Before init, try and fill the preinit queue with > 1000 tasks.
+ const kIterations = 2000;
+ for (let _i = 0; _i < kIterations; _i++) {
+ Glean.testOnly.badCode.add(1);
+ }
+
+ Services.fog.initializeFOG();
+
+ Assert.equal(kIterations, Glean.testOnly.badCode.testGetValue());
+});
diff --git a/toolkit/components/glean/tests/xpcshell/xpcshell.toml b/toolkit/components/glean/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..40b1a22bf4
--- /dev/null
+++ b/toolkit/components/glean/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,32 @@
+[DEFAULT]
+firefox-appdir = "browser"
+head = "head.js"
+
+["test_FOGIPCLimit.js"]
+
+["test_FOGInit.js"]
+
+["test_FOGPrefs.js"]
+skip-if = ["os == 'android'"] # FOG isn't responsible for monitoring prefs and controlling upload on Android
+
+["test_GIFFT.js"]
+run-sequentially = "very high failure rate in parallel"
+
+["test_GIFFTIPC.js"]
+
+["test_Glean.js"]
+
+["test_GleanExperiments.js"]
+skip-if = ["os == 'android'"] # FOG isn't responsible for experiment annotations on Android
+
+["test_GleanIPC.js"]
+
+["test_GleanServerKnobs.js"]
+skip-if = ["os == 'android'"] # Server Knobs on mobile will be handled by the specific app
+
+["test_JOG.js"]
+
+["test_JOGIPC.js"]
+
+["test_MillionQ.js"]
+skip-if = ["os == 'android'"] # Android inits its own FOG, so the test won't work.