diff options
Diffstat (limited to 'toolkit/components/telemetry/tests/unit/test_HealthPing.js')
-rw-r--r-- | toolkit/components/telemetry/tests/unit/test_HealthPing.js | 403 |
1 files changed, 403 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/tests/unit/test_HealthPing.js b/toolkit/components/telemetry/tests/unit/test_HealthPing.js new file mode 100644 index 0000000000..f29016a2af --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_HealthPing.js @@ -0,0 +1,403 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This tests the public Telemetry API for submitting Health pings. + +"use strict"; + +ChromeUtils.import("resource://gre/modules/TelemetryController.jsm", this); +ChromeUtils.import("resource://gre/modules/TelemetryStorage.jsm", this); +ChromeUtils.import("resource://gre/modules/TelemetryUtils.jsm", this); +ChromeUtils.import("resource://gre/modules/Preferences.jsm", this); +ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this); +ChromeUtils.import( + "resource://testing-common/TelemetryArchiveTesting.jsm", + this +); + +ChromeUtils.defineModuleGetter( + this, + "TelemetryHealthPing", + "resource://gre/modules/HealthPing.jsm" +); + +function checkHealthPingStructure(ping, expectedFailuresDict) { + let payload = ping.payload; + Assert.equal( + ping.type, + TelemetryHealthPing.HEALTH_PING_TYPE, + "Should have recorded a health ping." + ); + + for (let [key, value] of Object.entries(expectedFailuresDict)) { + Assert.deepEqual( + payload[key], + value, + "Should have recorded correct entry with key: " + key + ); + } +} + +function fakeHealthSchedulerTimer(set, clear) { + let telemetryHealthPing = ChromeUtils.import( + "resource://gre/modules/HealthPing.jsm", + null + ); + telemetryHealthPing.Policy.setSchedulerTickTimeout = set; + telemetryHealthPing.Policy.clearSchedulerTickTimeout = clear; +} + +async function waitForConditionWithPromise( + promiseFn, + timeoutMsg, + tryCount = 30 +) { + const SINGLE_TRY_TIMEOUT = 100; + let tries = 0; + do { + try { + return await promiseFn(); + } catch (ex) {} + await new Promise(resolve => do_timeout(SINGLE_TRY_TIMEOUT, resolve)); + } while (++tries <= tryCount); + throw new Error(timeoutMsg); +} + +function fakeSendSubmissionTimeout(timeOut) { + let telemetryHealthPing = ChromeUtils.import( + "resource://gre/modules/TelemetrySend.jsm", + null + ); + telemetryHealthPing.Policy.pingSubmissionTimeout = () => timeOut; +} + +add_task(async function setup() { + // Trigger a proper telemetry init. + do_get_profile(true); + // Make sure we don't generate unexpected pings due to pref changes. + await setEmptyPrefWatchlist(); + Preferences.set(TelemetryUtils.Preferences.HealthPingEnabled, true); + + await TelemetryController.testSetup(); + PingServer.start(); + TelemetrySend.setServer("http://localhost:" + PingServer.port); + Preferences.set( + TelemetryUtils.Preferences.Server, + "http://localhost:" + PingServer.port + ); +}); + +add_task(async function test_sendImmediately() { + PingServer.clearRequests(); + TelemetryHealthPing.testReset(); + + await TelemetryHealthPing.recordSendFailure("testProblem"); + let ping = await PingServer.promiseNextPing(); + checkHealthPingStructure(ping, { + [TelemetryHealthPing.FailureType.SEND_FAILURE]: { + testProblem: 1, + }, + os: TelemetryHealthPing.OsInfo, + reason: TelemetryHealthPing.Reason.IMMEDIATE, + }); +}); + +add_task(async function test_sendOnDelay() { + PingServer.clearRequests(); + TelemetryHealthPing.testReset(); + + // This first failure should immediately trigger a ping. After this, subsequent failures should be throttled. + await TelemetryHealthPing.recordSendFailure("testFailure"); + let testPing = await PingServer.promiseNextPing(); + Assert.equal( + testPing.type, + TelemetryHealthPing.HEALTH_PING_TYPE, + "Should have recorded a health ping." + ); + + // Retrieve delayed call back. + let pingSubmissionCallBack = null; + fakeHealthSchedulerTimer( + callBack => (pingSubmissionCallBack = callBack), + () => {} + ); + + // Record two failures, health ping must not be send now. + await TelemetryHealthPing.recordSendFailure("testFailure"); + await TelemetryHealthPing.recordSendFailure("testFailure"); + + // Wait for sending delayed health ping. + await pingSubmissionCallBack(); + + let ping = await PingServer.promiseNextPing(); + checkHealthPingStructure(ping, { + [TelemetryHealthPing.FailureType.SEND_FAILURE]: { + testFailure: 2, + }, + os: TelemetryHealthPing.OsInfo, + reason: TelemetryHealthPing.Reason.DELAYED, + }); +}); + +add_task(async function test_sendOverSizedPing() { + TelemetryHealthPing.testReset(); + PingServer.clearRequests(); + let OVER_SIZED_PING_TYPE = "over-sized-ping"; + let overSizedData = generateRandomString(2 * 1024 * 1024); + + await TelemetryController.submitExternalPing(OVER_SIZED_PING_TYPE, { + data: overSizedData, + }); + let ping = await PingServer.promiseNextPing(); + + checkHealthPingStructure(ping, { + [TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE]: { + [OVER_SIZED_PING_TYPE]: 1, + }, + os: TelemetryHealthPing.OsInfo, + reason: TelemetryHealthPing.Reason.IMMEDIATE, + }); +}); + +add_task(async function test_healthPingOnTop() { + PingServer.clearRequests(); + TelemetryHealthPing.testReset(); + + let PING_TYPE = "priority-ping"; + + // Fake now to be in throttled state. + let now = fakeNow(2050, 1, 2, 0, 0, 0); + fakeMidnightPingFuzzingDelay(60 * 1000); + + for (let value of [PING_TYPE, PING_TYPE, "health", PING_TYPE]) { + TelemetryController.submitExternalPing(value, {}); + } + + // Now trigger sending pings again. + fakeNow(futureDate(now, 5 * 60 * 1000)); + await TelemetrySend.notifyCanUpload(); + let scheduler = ChromeUtils.import( + "resource://gre/modules/TelemetrySend.jsm", + null + ); + scheduler.SendScheduler.triggerSendingPings(true); + + let pings = await PingServer.promiseNextPings(4); + Assert.equal( + pings[0].type, + "health", + "Should have received the health ping first." + ); +}); + +add_task(async function test_sendOnTimeout() { + TelemetryHealthPing.testReset(); + await TelemetrySend.reset(); + PingServer.clearRequests(); + let PING_TYPE = "ping-on-timeout"; + + // Disable send retry to make this test more deterministic. + fakePingSendTimer( + () => {}, + () => {} + ); + + // Set up small ping submission timeout to always have timeout error. + fakeSendSubmissionTimeout(2); + + await TelemetryController.submitExternalPing(PING_TYPE, {}); + + let response; + PingServer.registerPingHandler((req, res) => { + PingServer.resetPingHandler(); + // We don't finish the response yet to make sure to trigger a timeout. + res.processAsync(); + response = res; + }); + + // Wait for health ping. + let ac = new TelemetryArchiveTesting.Checker(); + await ac.promiseInit(); + await waitForConditionWithPromise(() => { + ac.promiseFindPing("health", []); + }, "Failed to find health ping"); + + if (response) { + response.finish(); + } + + let telemetryHealthPing = ChromeUtils.import( + "resource://gre/modules/TelemetrySend.jsm", + null + ); + fakeSendSubmissionTimeout(telemetryHealthPing.PING_SUBMIT_TIMEOUT_MS); + PingServer.resetPingHandler(); + TelemetrySend.notifyCanUpload(); + + let pings = await PingServer.promiseNextPings(2); + let healthPing = pings.find(ping => ping.type === "health"); + checkHealthPingStructure(healthPing, { + [TelemetryHealthPing.FailureType.SEND_FAILURE]: { + timeout: 1, + }, + os: TelemetryHealthPing.OsInfo, + reason: TelemetryHealthPing.Reason.IMMEDIATE, + }); + await TelemetryStorage.testClearPendingPings(); +}); + +add_task(async function test_sendOnlyTopTenDiscardedPings() { + TelemetryHealthPing.testReset(); + await TelemetrySend.reset(); + PingServer.clearRequests(); + let PING_TYPE = "sort-discarded"; + + // This first failure should immediately trigger a ping. After this, subsequent failures should be throttled. + await TelemetryHealthPing.recordSendFailure("testFailure"); + let testPing = await PingServer.promiseNextPing(); + Assert.equal( + testPing.type, + TelemetryHealthPing.HEALTH_PING_TYPE, + "Should have recorded a health ping." + ); + + // Retrieve delayed call back. + let pingSubmissionCallBack = null; + fakeHealthSchedulerTimer( + callBack => (pingSubmissionCallBack = callBack), + () => {} + ); + + // Add failures + for (let i = 1; i < 12; i++) { + for (let j = 1; j < i; j++) { + TelemetryHealthPing.recordDiscardedPing(PING_TYPE + i); + } + } + + await TelemetrySend.reset(); + await pingSubmissionCallBack(); + let ping = await PingServer.promiseNextPing(); + + checkHealthPingStructure(ping, { + os: TelemetryHealthPing.OsInfo, + reason: TelemetryHealthPing.Reason.DELAYED, + [TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE]: { + [PING_TYPE + 11]: 10, + [PING_TYPE + 10]: 9, + [PING_TYPE + 9]: 8, + [PING_TYPE + 8]: 7, + [PING_TYPE + 7]: 6, + [PING_TYPE + 6]: 5, + [PING_TYPE + 5]: 4, + [PING_TYPE + 4]: 3, + [PING_TYPE + 3]: 2, + [PING_TYPE + 2]: 1, + }, + }); +}); + +add_task(async function test_discardedForSizePending() { + TelemetryHealthPing.testReset(); + PingServer.clearRequests(); + + const PING_TYPE = "discarded-for-size-pending"; + + const OVERSIZED_PING_ID = "9b21ec8f-f762-4d28-a2c1-44e1c4694f24"; + // Create a pending oversized ping. + let overSizedPayload = generateRandomString(2 * 1024 * 1024); + const OVERSIZED_PING = { + id: OVERSIZED_PING_ID, + type: PING_TYPE, + creationDate: new Date().toISOString(), + // Generate a 2MB string to use as the ping payload. + payload: overSizedPayload, + }; + + // Test loadPendingPing. + await TelemetryStorage.savePendingPing(OVERSIZED_PING); + // Try to manually load the oversized ping. + await Assert.rejects( + TelemetryStorage.loadPendingPing(OVERSIZED_PING_ID), + /loadPendingPing - exceeded the maximum ping size/, + "The oversized ping should have been pruned." + ); + + let ping = await PingServer.promiseNextPing(); + checkHealthPingStructure(ping, { + [TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE]: { + "<unknown>": 1, + }, + os: TelemetryHealthPing.OsInfo, + reason: TelemetryHealthPing.Reason.IMMEDIATE, + }); + + // Test _scanPendingPings. + TelemetryHealthPing.testReset(); + await TelemetryStorage.savePendingPing(OVERSIZED_PING); + await TelemetryStorage.loadPendingPingList(); + + ping = await PingServer.promiseNextPing(); + checkHealthPingStructure(ping, { + [TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE]: { + "<unknown>": 1, + }, + os: TelemetryHealthPing.OsInfo, + reason: TelemetryHealthPing.Reason.IMMEDIATE, + }); +}); + +add_task(async function test_usePingSenderOnShutdown() { + if ( + gIsAndroid || + (AppConstants.platform == "linux" && OS.Constants.Sys.bits == 32) + ) { + // We don't support the pingsender on Android, yet, see bug 1335917. + // We also don't support the pingsender testing on Treeherder for + // Linux 32 bit (due to missing libraries). So skip it there too. + // See bug 1310703 comment 78. + return; + } + + TelemetryHealthPing.testReset(); + await TelemetrySend.reset(); + PingServer.clearRequests(); + + // This first failure should immediately trigger a ping. + // After this, subsequent failures should be throttled. + await TelemetryHealthPing.recordSendFailure("testFailure"); + await PingServer.promiseNextPing(); + + TelemetryHealthPing.recordSendFailure("testFailure"); + let nextRequest = PingServer.promiseNextRequest(); + + await TelemetryController.testReset(); + await TelemetryController.testShutdown(); + let request = await nextRequest; + let ping = decodeRequestPayload(request); + + checkHealthPingStructure(ping, { + [TelemetryHealthPing.FailureType.SEND_FAILURE]: { + testFailure: 1, + }, + os: TelemetryHealthPing.OsInfo, + reason: TelemetryHealthPing.Reason.SHUT_DOWN, + }); + + // Check that the health ping is sent at shutdown using the pingsender. + Assert.equal( + request.getHeader("User-Agent"), + "pingsender/1.0", + "Should have received the correct user agent string." + ); + Assert.equal( + request.getHeader("X-PingSender-Version"), + "1.0", + "Should have received the correct PingSender version string." + ); +}); + +add_task(async function cleanup() { + await PingServer.stop(); +}); |