diff options
Diffstat (limited to 'toolkit/components/glean/tests/xpcshell/test_GIFFT.js')
-rw-r--r-- | toolkit/components/glean/tests/xpcshell/test_GIFFT.js | 523 |
1 files changed, 523 insertions, 0 deletions
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") + ); +}); |