diff options
Diffstat (limited to 'toolkit/components/glean/tests/xpcshell')
9 files changed, 325 insertions, 34 deletions
diff --git a/toolkit/components/glean/tests/xpcshell/head.js b/toolkit/components/glean/tests/xpcshell/head.js index f42bd02822..eaf8fa9c61 100644 --- a/toolkit/components/glean/tests/xpcshell/head.js +++ b/toolkit/components/glean/tests/xpcshell/head.js @@ -4,3 +4,155 @@ const { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); + +ChromeUtils.defineESModuleGetters(this, { + HttpServer: "resource://testing-common/httpd.sys.mjs", + NetUtil: "resource://gre/modules/NetUtil.sys.mjs", +}); + +const PingServer = { + _httpServer: null, + _started: false, + _defers: [Promise.withResolvers()], + _currentDeferred: 0, + + get port() { + return this._httpServer.identity.primaryPort; + }, + + get host() { + return this._httpServer.identity.primaryHost; + }, + + get started() { + return this._started; + }, + + registerPingHandler(handler) { + this._httpServer.registerPrefixHandler("/submit/", handler); + }, + + resetPingHandler() { + this.registerPingHandler(request => { + let r = request; + console.trace( + `defaultPingHandler() - ${r.method} ${r.scheme}://${r.host}:${r.port}${r.path}` + ); + let deferred = this._defers[this._defers.length - 1]; + this._defers.push(Promise.withResolvers()); + deferred.resolve(request); + }); + }, + + start() { + this._httpServer = new HttpServer(); + this._httpServer.start(-1); + this._started = true; + this.clearRequests(); + this.resetPingHandler(); + }, + + stop() { + return new Promise(resolve => { + this._httpServer.stop(resolve); + this._started = false; + }); + }, + + clearRequests() { + this._defers = [Promise.withResolvers()]; + this._currentDeferred = 0; + }, + + promiseNextRequest() { + const deferred = this._defers[this._currentDeferred++]; + // Send the ping to the consumer on the next tick, so that the completion gets + // signaled to Telemetry. + return new Promise(r => + Services.tm.dispatchToMainThread(() => r(deferred.promise)) + ); + }, + + promiseNextPing() { + return this.promiseNextRequest().then(request => + decodeRequestPayload(request) + ); + }, + + async promiseNextRequests(count) { + let results = []; + for (let i = 0; i < count; ++i) { + results.push(await this.promiseNextRequest()); + } + + return results; + }, + + promiseNextPings(count) { + return this.promiseNextRequests(count).then(requests => { + return Array.from(requests, decodeRequestPayload); + }); + }, +}; + +/** + * Decode the payload of an HTTP request into a ping. + * + * @param {object} request The data representing an HTTP request (nsIHttpRequest). + * @returns {object} The decoded ping payload. + */ +function decodeRequestPayload(request) { + let s = request.bodyInputStream; + let payload = null; + + if ( + request.hasHeader("content-encoding") && + request.getHeader("content-encoding") == "gzip" + ) { + let observer = { + buffer: "", + onStreamComplete(loader, context, status, length, result) { + // String.fromCharCode can only deal with 500,000 characters + // at a time, so chunk the result into parts of that size. + const chunkSize = 500000; + for (let offset = 0; offset < result.length; offset += chunkSize) { + this.buffer += String.fromCharCode.apply( + String, + result.slice(offset, offset + chunkSize) + ); + } + }, + }; + + let scs = Cc["@mozilla.org/streamConverters;1"].getService( + Ci.nsIStreamConverterService + ); + let listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance( + Ci.nsIStreamLoader + ); + listener.init(observer); + let converter = scs.asyncConvertData( + "gzip", + "uncompressed", + listener, + null + ); + converter.onStartRequest(null, null); + converter.onDataAvailable(null, s, 0, s.available()); + converter.onStopRequest(null, null, null); + // TODO: nsIScriptableUnicodeConverter is deprecated + // But I can't figure out how else to ungzip bodyInputStream. + let unicodeConverter = Cc[ + "@mozilla.org/intl/scriptableunicodeconverter" + ].createInstance(Ci.nsIScriptableUnicodeConverter); + unicodeConverter.charset = "UTF-8"; + let utf8string = unicodeConverter.ConvertToUnicode(observer.buffer); + utf8string += unicodeConverter.Finish(); + payload = JSON.parse(utf8string); + } else { + let bytes = NetUtil.readInputStream(s, s.available()); + payload = JSON.parse(new TextDecoder().decode(bytes)); + } + + return payload; +} diff --git a/toolkit/components/glean/tests/xpcshell/test_GIFFT.js b/toolkit/components/glean/tests/xpcshell/test_GIFFT.js index 015b4d4e38..8fcf755d3a 100644 --- a/toolkit/components/glean/tests/xpcshell/test_GIFFT.js +++ b/toolkit/components/glean/tests/xpcshell/test_GIFFT.js @@ -176,10 +176,7 @@ add_task(async function test_gifft_timing_dist() { // But we can guarantee it's only two samples. Assert.equal( 2, - Object.entries(data.values).reduce( - (acc, [bucket, count]) => acc + count, - 0 - ), + Object.entries(data.values).reduce((acc, [, count]) => acc + count, 0), "Only two buckets with samples" ); @@ -188,10 +185,7 @@ add_task(async function test_gifft_timing_dist() { Assert.greaterOrEqual(data.sum, 13, "Histogram's in milliseconds"); Assert.equal( 2, - Object.entries(data.values).reduce( - (acc, [bucket, count]) => acc + count, - 0 - ), + Object.entries(data.values).reduce((acc, [, count]) => acc + count, 0), "Only two samples" ); }); diff --git a/toolkit/components/glean/tests/xpcshell/test_GIFFTIPC.js b/toolkit/components/glean/tests/xpcshell/test_GIFFTIPC.js index a318d57b9c..ae4c48b6e8 100644 --- a/toolkit/components/glean/tests/xpcshell/test_GIFFTIPC.js +++ b/toolkit/components/glean/tests/xpcshell/test_GIFFTIPC.js @@ -291,10 +291,7 @@ add_task( // but we can assert there are only two samples. Assert.equal( 2, - Object.entries(times.values).reduce( - (acc, [bucket, count]) => acc + count, - 0 - ) + Object.entries(times.values).reduce((acc, [, count]) => acc + count, 0) ); const timingHist = histSnapshot.content.TELEMETRY_TEST_EXPONENTIAL; Assert.greaterOrEqual(timingHist.sum, 13, "Histogram's in milliseconds."); @@ -303,7 +300,7 @@ add_task( Assert.equal( 2, Object.entries(timingHist.values).reduce( - (acc, [bucket, count]) => acc + count, + (acc, [, count]) => acc + count, 0 ), "Only two samples" diff --git a/toolkit/components/glean/tests/xpcshell/test_Glean.js b/toolkit/components/glean/tests/xpcshell/test_Glean.js index 8375c22a3e..18b450a69b 100644 --- a/toolkit/components/glean/tests/xpcshell/test_Glean.js +++ b/toolkit/components/glean/tests/xpcshell/test_Glean.js @@ -182,6 +182,7 @@ add_task(async function test_fog_memory_distribution_works() { Glean.testOnly.doYouRemember.accumulate(17); let data = Glean.testOnly.doYouRemember.testGetValue("test-ping"); + Assert.equal(2, data.count, "Count of entries is correct"); // `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)) { @@ -196,6 +197,7 @@ add_task(async function test_fog_custom_distribution_works() { Glean.testOnlyIpc.aCustomDist.accumulateSamples([7, 268435458]); let data = Glean.testOnlyIpc.aCustomDist.testGetValue("store1"); + Assert.equal(2, data.count, "Count of entries is correct"); Assert.equal(7 + 268435458, data.sum, "Sum's correct"); for (let [bucket, count] of Object.entries(data.values)) { Assert.ok( @@ -216,7 +218,7 @@ add_task(function test_fog_custom_pings() { Assert.ok("onePingOnly" in GleanPings); let submitted = false; Glean.testOnly.onePingOneBool.set(false); - GleanPings.onePingOnly.testBeforeNextSubmit(reason => { + GleanPings.onePingOnly.testBeforeNextSubmit(() => { submitted = true; Assert.equal(false, Glean.testOnly.onePingOneBool.testGetValue()); }); @@ -227,7 +229,7 @@ add_task(function test_fog_custom_pings() { add_task(function test_recursive_testBeforeNextSubmit() { Assert.ok("onePingOnly" in GleanPings); let submitted = 0; - let rec = reason => { + let rec = () => { submitted++; GleanPings.onePingOnly.testBeforeNextSubmit(rec); }; @@ -255,6 +257,10 @@ add_task(async function test_fog_timing_distribution_works() { Glean.testOnly.whatTimeIsIt.stopAndAccumulate(t3); // 5ms let data = Glean.testOnly.whatTimeIsIt.testGetValue(); + + // Cancelled timers should not be counted. + Assert.equal(2, data.count, "Count of entries is correct"); + const NANOS_IN_MILLIS = 1e6; // bug 1701949 - Sleep gets close, but sometimes doesn't wait long enough. const EPSILON = 40000; @@ -266,10 +272,7 @@ add_task(async function test_fog_timing_distribution_works() { // But we can guarantee it's only two samples. Assert.equal( 2, - Object.entries(data.values).reduce( - (acc, [bucket, count]) => acc + count, - 0 - ), + Object.entries(data.values).reduce((acc, [, count]) => acc + count, 0), "Only two buckets with samples" ); }); @@ -456,3 +459,106 @@ add_task(async function test_fog_text_works_unusual_character() { Assert.greater(rslt.length, 100); }); + +add_task(async function test_fog_object_works() { + if (!Glean.testOnly.balloons) { + // FIXME(bug 1883857): object metric type not available, e.g. in artifact builds. + // Skipping this test. + return; + } + + Assert.equal( + undefined, + Glean.testOnly.balloons.testGetValue(), + "No object stored" + ); + + // Can't store not-objects. + let invalidValues = [1, "str", false, undefined, null, NaN, Infinity]; + for (let value of invalidValues) { + Assert.throws( + () => Glean.testOnly.balloons.set(value), + /is not an object/, + "Should throw a type error" + ); + } + + // No invalid value will be stored. + Assert.equal( + undefined, + Glean.testOnly.balloons.testGetValue(), + "No object stored" + ); + + // `JS_Stringify` internally throws + // an `TypeError: cyclic object value` exception. + // That's cleared and `set` should not throw on it. + // This eventually should log a proper error in Glean. + let selfref = {}; + selfref.a = selfref; + Glean.testOnly.balloons.set(selfref); + Assert.equal( + undefined, + Glean.testOnly.balloons.testGetValue(), + "No object stored" + ); + + let balloons = [ + { colour: "red", diameter: 5 }, + { colour: "blue", diameter: 7 }, + { colour: "orange" }, + ]; + Glean.testOnly.balloons.set(balloons); + + let result = Glean.testOnly.balloons.testGetValue(); + let expected = [ + { colour: "red", diameter: 5 }, + { colour: "blue", diameter: 7 }, + { colour: "orange", diameter: null }, + ]; + Assert.deepEqual(expected, result); + + // These values are coerced to null or removed. + balloons = [ + { colour: "inf", diameter: Infinity }, + { colour: "negative-inf", diameter: -1 / 0 }, + { colour: "nan", diameter: NaN }, + { colour: "undef", diameter: undefined }, + ]; + Glean.testOnly.balloons.set(balloons); + result = Glean.testOnly.balloons.testGetValue(); + expected = [ + { colour: "inf", diameter: null }, + { colour: "negative-inf", diameter: null }, + { colour: "nan", diameter: null }, + { colour: "undef", diameter: null }, + ]; + Assert.deepEqual(expected, result); + + // colour != color. + let invalid = [{ color: "orange" }, { color: "red", diameter: "small" }]; + Glean.testOnly.balloons.set(invalid); + Assert.throws( + () => Glean.testOnly.balloons.testGetValue(), + /invalid_value/, + "Should throw because last object was invalid." + ); + + Services.fog.testResetFOG(); + // set again to ensure it's stored + balloons = [ + { colour: "red", diameter: 5 }, + { colour: "blue", diameter: 7 }, + ]; + Glean.testOnly.balloons.set(balloons); + result = Glean.testOnly.balloons.testGetValue(); + Assert.deepEqual(balloons, result); + + invalid = [{ colour: "red", diameter: 5, extra: "field" }]; + Glean.testOnly.balloons.set(invalid); + Assert.throws( + () => Glean.testOnly.balloons.testGetValue(), + /invalid_value/, + "Should throw because last object was invalid." + ); +}); diff --git a/toolkit/components/glean/tests/xpcshell/test_GleanIPC.js b/toolkit/components/glean/tests/xpcshell/test_GleanIPC.js index 3d665c23b9..2db1c53218 100644 --- a/toolkit/components/glean/tests/xpcshell/test_GleanIPC.js +++ b/toolkit/components/glean/tests/xpcshell/test_GleanIPC.js @@ -127,10 +127,7 @@ add_task( // but we can assert there are only two samples. Assert.equal( 2, - Object.entries(times.values).reduce( - (acc, [bucket, count]) => acc + count, - 0 - ) + Object.entries(times.values).reduce((acc, [, count]) => acc + count, 0) ); const mabelsCounters = Glean.testOnly.mabelsKitchenCounters; diff --git a/toolkit/components/glean/tests/xpcshell/test_JOG.js b/toolkit/components/glean/tests/xpcshell/test_JOG.js index 53b1d25962..00f3ded135 100644 --- a/toolkit/components/glean/tests/xpcshell/test_JOG.js +++ b/toolkit/components/glean/tests/xpcshell/test_JOG.js @@ -289,11 +289,11 @@ add_task(async function test_jog_custom_pings() { `"ping"`, false ); - Services.fog.testRegisterRuntimePing("jog-ping", true, true, true, []); + Services.fog.testRegisterRuntimePing("jog-ping", true, true, true, true, []); Assert.ok("jogPing" in GleanPings); let submitted = false; Glean.jogCat.jogPingBool.set(false); - GleanPings.jogPing.testBeforeNextSubmit(reason => { + GleanPings.jogPing.testBeforeNextSubmit(() => { submitted = true; Assert.equal(false, Glean.jogCat.jogPingBool.testGetValue()); }); @@ -338,10 +338,7 @@ add_task(async function test_jog_timing_distribution_works() { // But we can guarantee it's only two samples. Assert.equal( 2, - Object.entries(data.values).reduce( - (acc, [bucket, count]) => acc + count, - 0 - ), + Object.entries(data.values).reduce((acc, [, count]) => acc + count, 0), "Only two buckets with samples" ); }); @@ -642,7 +639,9 @@ add_task(function test_jog_dotted_categories_work() { add_task(async function test_jog_ping_works() { const kReason = "reason-1"; - Services.fog.testRegisterRuntimePing("my-ping", true, true, true, [kReason]); + Services.fog.testRegisterRuntimePing("my-ping", true, true, true, true, [ + kReason, + ]); let submitted = false; GleanPings.myPing.testBeforeNextSubmit(reason => { submitted = true; @@ -652,6 +651,20 @@ add_task(async function test_jog_ping_works() { Assert.ok(submitted, "Ping must have been submitted"); }); +add_task(async function test_jog_noinfo_ping_works() { + const kReason = "reason-1"; + Services.fog.testRegisterRuntimePing("noinfo-ping", true, true, true, false, [ + kReason, + ]); + let submitted = false; + GleanPings.noinfoPing.testBeforeNextSubmit(reason => { + submitted = true; + Assert.equal(kReason, reason); + }); + GleanPings.noinfoPing.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()); diff --git a/toolkit/components/glean/tests/xpcshell/test_JOGIPC.js b/toolkit/components/glean/tests/xpcshell/test_JOGIPC.js index 505dba6825..b43a448c53 100644 --- a/toolkit/components/glean/tests/xpcshell/test_JOGIPC.js +++ b/toolkit/components/glean/tests/xpcshell/test_JOGIPC.js @@ -226,10 +226,7 @@ add_task( // but we can assert there are only two samples. Assert.equal( 2, - Object.entries(times.values).reduce( - (acc, [bucket, count]) => acc + count, - 0 - ) + Object.entries(times.values).reduce((acc, [, count]) => acc + count, 0) ); const labeledCounter = Glean.jogIpc.jogLabeledCounter; diff --git a/toolkit/components/glean/tests/xpcshell/test_OHTTP.js b/toolkit/components/glean/tests/xpcshell/test_OHTTP.js new file mode 100644 index 0000000000..76d1d2a67b --- /dev/null +++ b/toolkit/components/glean/tests/xpcshell/test_OHTTP.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async () => { + // FOG needs a profile dir to put its data in. + do_get_profile(); + + PingServer.start(); + + registerCleanupFunction(async () => { + await PingServer.stop(); + }); + + Services.prefs.setIntPref( + "telemetry.fog.test.localhost_port", + PingServer.port + ); + // Port pref needs to be set before init, so let's reset to reinit. + Services.fog.testResetFOG(); +}); + +add_task(async () => { + PingServer.clearRequests(); + GleanPings.testOhttpPing.submit(); + + let ping = await PingServer.promiseNextPing(); + + ok(!("client_info" in ping), "No client_info allowed."); + ok(!("ping_info" in ping), "No ping_info allowed."); +}); diff --git a/toolkit/components/glean/tests/xpcshell/xpcshell.toml b/toolkit/components/glean/tests/xpcshell/xpcshell.toml index 40b1a22bf4..22806a32e6 100644 --- a/toolkit/components/glean/tests/xpcshell/xpcshell.toml +++ b/toolkit/components/glean/tests/xpcshell/xpcshell.toml @@ -30,3 +30,6 @@ skip-if = ["os == 'android'"] # Server Knobs on mobile will be handled by the sp ["test_MillionQ.js"] skip-if = ["os == 'android'"] # Android inits its own FOG, so the test won't work. + +["test_OHTTP.js"] +skip-if = ["os == 'android'"] # FOG isn't responsible for monitoring prefs and controlling upload on Android |