From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- toolkit/components/glean/tests/xpcshell/head.js | 6 + .../glean/tests/xpcshell/test_FOGIPCLimit.js | 51 ++ .../glean/tests/xpcshell/test_FOGInit.js | 49 ++ .../glean/tests/xpcshell/test_FOGPrefs.js | 47 ++ .../components/glean/tests/xpcshell/test_GIFFT.js | 523 +++++++++++++++ .../glean/tests/xpcshell/test_GIFFTIPC.js | 315 +++++++++ .../components/glean/tests/xpcshell/test_Glean.js | 458 +++++++++++++ .../glean/tests/xpcshell/test_GleanExperiments.js | 28 + .../glean/tests/xpcshell/test_GleanIPC.js | 157 +++++ .../glean/tests/xpcshell/test_GleanServerKnobs.js | 164 +++++ .../components/glean/tests/xpcshell/test_JOG.js | 728 +++++++++++++++++++++ .../components/glean/tests/xpcshell/test_JOGIPC.js | 266 ++++++++ .../glean/tests/xpcshell/test_MillionQ.js | 19 + .../components/glean/tests/xpcshell/xpcshell.toml | 32 + 14 files changed, 2843 insertions(+) create mode 100644 toolkit/components/glean/tests/xpcshell/head.js create mode 100644 toolkit/components/glean/tests/xpcshell/test_FOGIPCLimit.js create mode 100644 toolkit/components/glean/tests/xpcshell/test_FOGInit.js create mode 100644 toolkit/components/glean/tests/xpcshell/test_FOGPrefs.js create mode 100644 toolkit/components/glean/tests/xpcshell/test_GIFFT.js create mode 100644 toolkit/components/glean/tests/xpcshell/test_GIFFTIPC.js create mode 100644 toolkit/components/glean/tests/xpcshell/test_Glean.js create mode 100644 toolkit/components/glean/tests/xpcshell/test_GleanExperiments.js create mode 100644 toolkit/components/glean/tests/xpcshell/test_GleanIPC.js create mode 100644 toolkit/components/glean/tests/xpcshell/test_GleanServerKnobs.js create mode 100644 toolkit/components/glean/tests/xpcshell/test_JOG.js create mode 100644 toolkit/components/glean/tests/xpcshell/test_JOGIPC.js create mode 100644 toolkit/components/glean/tests/xpcshell/test_MillionQ.js create mode 100644 toolkit/components/glean/tests/xpcshell/xpcshell.toml (limited to 'toolkit/components/glean/tests/xpcshell') 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. -- cgit v1.2.3