diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /toolkit/components/telemetry/tests/unit | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'toolkit/components/telemetry/tests/unit')
27 files changed, 9132 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/tests/unit/.eslintrc.js b/toolkit/components/telemetry/tests/unit/.eslintrc.js new file mode 100644 index 000000000..d35787cd2 --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/components/telemetry/tests/unit/TelemetryArchiveTesting.jsm b/toolkit/components/telemetry/tests/unit/TelemetryArchiveTesting.jsm new file mode 100644 index 000000000..9be82c883 --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/TelemetryArchiveTesting.jsm @@ -0,0 +1,86 @@ +const {utils: Cu} = Components; +Cu.import("resource://gre/modules/TelemetryArchive.jsm"); +Cu.import("resource://testing-common/Assert.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/TelemetryController.jsm"); + +this.EXPORTED_SYMBOLS = [ + "TelemetryArchiveTesting", +]; + +function checkForProperties(ping, expected) { + for (let [props, val] of expected) { + let test = ping; + for (let prop of props) { + test = test[prop]; + if (test === undefined) { + return false; + } + } + if (test !== val) { + return false; + } + } + return true; +} + +/** + * A helper object that allows test code to check whether a telemetry ping + * was properly saved. To use, first initialize to collect the starting pings + * and then check for new ping data. + */ +function Checker() { +} +Checker.prototype = { + promiseInit: function() { + this._pingMap = new Map(); + return TelemetryArchive.promiseArchivedPingList().then((plist) => { + for (let ping of plist) { + this._pingMap.set(ping.id, ping); + } + }); + }, + + /** + * Find and return a new ping with certain properties. + * + * @param expected: an array of [['prop'...], 'value'] to check + * For example: + * [ + * [['environment', 'build', 'applicationId'], '20150101010101'], + * [['version'], 1], + * [['metadata', 'OOMAllocationSize'], 123456789], + * ] + * @returns a matching ping if found, or null + */ + promiseFindPing: Task.async(function*(type, expected) { + let candidates = []; + let plist = yield TelemetryArchive.promiseArchivedPingList(); + for (let ping of plist) { + if (this._pingMap.has(ping.id)) { + continue; + } + if (ping.type == type) { + candidates.push(ping); + } + } + + for (let candidate of candidates) { + let ping = yield TelemetryArchive.promiseArchivedPingById(candidate.id); + if (checkForProperties(ping, expected)) { + return ping; + } + } + return null; + }), +}; + +const TelemetryArchiveTesting = { + setup: function() { + Services.prefs.setCharPref("toolkit.telemetry.log.level", "Trace"); + Services.prefs.setBoolPref("toolkit.telemetry.archive.enabled", true); + }, + + Checker: Checker, +}; diff --git a/toolkit/components/telemetry/tests/unit/engine.xml b/toolkit/components/telemetry/tests/unit/engine.xml new file mode 100644 index 000000000..2304fcdd7 --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/engine.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>engine-telemetry</ShortName> +<Url type="text/html" method="GET" template="http://www.example.com/search"> + <Param name="q" value="{searchTerms}"/> +</Url> +</SearchPlugin> diff --git a/toolkit/components/telemetry/tests/unit/head.js b/toolkit/components/telemetry/tests/unit/head.js new file mode 100644 index 000000000..51be25766 --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/head.js @@ -0,0 +1,319 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var { classes: Cc, utils: Cu, interfaces: Ci, results: Cr } = Components; + +Cu.import("resource://gre/modules/TelemetryController.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/PromiseUtils.jsm", this); +Cu.import("resource://gre/modules/Task.jsm", this); +Cu.import("resource://gre/modules/FileUtils.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://testing-common/httpd.js", this); +Cu.import("resource://gre/modules/AppConstants.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "AddonTestUtils", + "resource://testing-common/AddonTestUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); + +const gIsWindows = AppConstants.platform == "win"; +const gIsMac = AppConstants.platform == "macosx"; +const gIsAndroid = AppConstants.platform == "android"; +const gIsGonk = AppConstants.platform == "gonk"; +const gIsLinux = AppConstants.platform == "linux"; + +const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry); + +const MILLISECONDS_PER_MINUTE = 60 * 1000; +const MILLISECONDS_PER_HOUR = 60 * MILLISECONDS_PER_MINUTE; +const MILLISECONDS_PER_DAY = 24 * MILLISECONDS_PER_HOUR; + +const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled"; + +const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +var gGlobalScope = this; + +const PingServer = { + _httpServer: null, + _started: false, + _defers: [ PromiseUtils.defer() ], + _currentDeferred: 0, + + get port() { + return this._httpServer.identity.primaryPort; + }, + + get started() { + return this._started; + }, + + registerPingHandler: function(handler) { + const wrapped = wrapWithExceptionHandler(handler); + this._httpServer.registerPrefixHandler("/submit/telemetry/", wrapped); + }, + + resetPingHandler: function() { + this.registerPingHandler((request, response) => { + let deferred = this._defers[this._defers.length - 1]; + this._defers.push(PromiseUtils.defer()); + deferred.resolve(request); + }); + }, + + start: function() { + this._httpServer = new HttpServer(); + this._httpServer.start(-1); + this._started = true; + this.clearRequests(); + this.resetPingHandler(); + }, + + stop: function() { + return new Promise(resolve => { + this._httpServer.stop(resolve); + this._started = false; + }); + }, + + clearRequests: function() { + this._defers = [ PromiseUtils.defer() ]; + this._currentDeferred = 0; + }, + + promiseNextRequest: function() { + 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.currentThread.dispatch(() => r(deferred.promise), + Ci.nsIThread.DISPATCH_NORMAL)); + }, + + promiseNextPing: function() { + return this.promiseNextRequest().then(request => decodeRequestPayload(request)); + }, + + promiseNextRequests: Task.async(function*(count) { + let results = []; + for (let i=0; i<count; ++i) { + results.push(yield this.promiseNextRequest()); + } + + return results; + }), + + promiseNextPings: function(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). + * @return {Object} The decoded ping payload. + */ +function decodeRequestPayload(request) { + let s = request.bodyInputStream; + let payload = null; + let decoder = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON) + + if (request.getHeader("content-encoding") == "gzip") { + let observer = { + buffer: "", + onStreamComplete: function(loader, context, status, length, result) { + this.buffer = String.fromCharCode.apply(this, result); + } + }; + + 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, null, s, 0, s.available()); + converter.onStopRequest(null, null, null); + 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 { + payload = decoder.decodeFromStream(s, s.available()); + } + + return payload; +} + +function wrapWithExceptionHandler(f) { + function wrapper(...args) { + try { + f(...args); + } catch (ex) { + if (typeof(ex) != 'object') { + throw ex; + } + dump("Caught exception: " + ex.message + "\n"); + dump(ex.stack); + do_test_finished(); + } + } + return wrapper; +} + +function loadAddonManager(...args) { + AddonTestUtils.init(gGlobalScope); + AddonTestUtils.overrideCertDB(); + createAppInfo(...args); + + // As we're not running in application, we need to setup the features directory + // used by system add-ons. + const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "app0"], true); + AddonTestUtils.registerDirectory("XREAppFeat", distroDir); + return AddonTestUtils.promiseStartupManager(); +} + +var gAppInfo = null; + +function createAppInfo(ID="xpcshell@tests.mozilla.org", name="XPCShell", + version="1.0", platformVersion="1.0") { + AddonTestUtils.createAppInfo(ID, name, version, platformVersion); + gAppInfo = AddonTestUtils.appInfo; +} + +// Fake the timeout functions for the TelemetryScheduler. +function fakeSchedulerTimer(set, clear) { + let session = Cu.import("resource://gre/modules/TelemetrySession.jsm"); + session.Policy.setSchedulerTickTimeout = set; + session.Policy.clearSchedulerTickTimeout = clear; +} + +/** + * Fake the current date. + * This passes all received arguments to a new Date constructor and + * uses the resulting date to fake the time in Telemetry modules. + * + * @return Date The new faked date. + */ +function fakeNow(...args) { + const date = new Date(...args); + const modules = [ + Cu.import("resource://gre/modules/TelemetrySession.jsm"), + Cu.import("resource://gre/modules/TelemetryEnvironment.jsm"), + Cu.import("resource://gre/modules/TelemetryController.jsm"), + Cu.import("resource://gre/modules/TelemetryStorage.jsm"), + Cu.import("resource://gre/modules/TelemetrySend.jsm"), + Cu.import("resource://gre/modules/TelemetryReportingPolicy.jsm"), + ]; + + for (let m of modules) { + m.Policy.now = () => date; + } + + return new Date(date); +} + +function fakeMonotonicNow(ms) { + const m = Cu.import("resource://gre/modules/TelemetrySession.jsm"); + m.Policy.monotonicNow = () => ms; + return ms; +} + +// Fake the timeout functions for TelemetryController sending. +function fakePingSendTimer(set, clear) { + let module = Cu.import("resource://gre/modules/TelemetrySend.jsm"); + let obj = Cu.cloneInto({set, clear}, module, {cloneFunctions:true}); + module.Policy.setSchedulerTickTimeout = obj.set; + module.Policy.clearSchedulerTickTimeout = obj.clear; +} + +function fakeMidnightPingFuzzingDelay(delayMs) { + let module = Cu.import("resource://gre/modules/TelemetrySend.jsm"); + module.Policy.midnightPingFuzzingDelay = () => delayMs; +} + +function fakeGeneratePingId(func) { + let module = Cu.import("resource://gre/modules/TelemetryController.jsm"); + module.Policy.generatePingId = func; +} + +function fakeCachedClientId(uuid) { + let module = Cu.import("resource://gre/modules/TelemetryController.jsm"); + module.Policy.getCachedClientID = () => uuid; +} + +// Return a date that is |offset| ms in the future from |date|. +function futureDate(date, offset) { + return new Date(date.getTime() + offset); +} + +function truncateToDays(aMsec) { + return Math.floor(aMsec / MILLISECONDS_PER_DAY); +} + +// Returns a promise that resolves to true when the passed promise rejects, +// false otherwise. +function promiseRejects(promise) { + return promise.then(() => false, () => true); +} + +// Generates a random string of at least a specific length. +function generateRandomString(length) { + let string = ""; + + while (string.length < length) { + string += Math.random().toString(36); + } + + return string.substring(0, length); +} + +// Short-hand for retrieving the histogram with that id. +function getHistogram(histogramId) { + return Telemetry.getHistogramById(histogramId); +} + +// Short-hand for retrieving the snapshot of the Histogram with that id. +function getSnapshot(histogramId) { + return Telemetry.getHistogramById(histogramId).snapshot(); +} + +// Helper for setting an empty list of Environment preferences to watch. +function setEmptyPrefWatchlist() { + let TelemetryEnvironment = + Cu.import("resource://gre/modules/TelemetryEnvironment.jsm").TelemetryEnvironment; + return TelemetryEnvironment.onInitialized().then(() => { + TelemetryEnvironment.testWatchPreferences(new Map()); + }); +} + +if (runningInParent) { + // Set logging preferences for all the tests. + Services.prefs.setCharPref("toolkit.telemetry.log.level", "Trace"); + // Telemetry archiving should be on. + Services.prefs.setBoolPref("toolkit.telemetry.archive.enabled", true); + // Telemetry xpcshell tests cannot show the infobar. + Services.prefs.setBoolPref("datareporting.policy.dataSubmissionPolicyBypassNotification", true); + // FHR uploads should be enabled. + Services.prefs.setBoolPref("datareporting.healthreport.uploadEnabled", true); + + fakePingSendTimer((callback, timeout) => { + Services.tm.mainThread.dispatch(() => callback(), Ci.nsIThread.DISPATCH_NORMAL); + }, + () => {}); + + do_register_cleanup(() => TelemetrySend.shutdown()); +} + +TelemetryController.testInitLogging(); + +// Avoid timers interrupting test behavior. +fakeSchedulerTimer(() => {}, () => {}); +// Make pind sending predictable. +fakeMidnightPingFuzzingDelay(0); diff --git a/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js b/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js new file mode 100644 index 000000000..11d730499 --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js @@ -0,0 +1,107 @@ + +Cu.import("resource://gre/modules/TelemetryController.jsm", this); +Cu.import("resource://gre/modules/TelemetrySession.jsm", this); +Cu.import("resource://gre/modules/PromiseUtils.jsm", this); +Cu.import("resource://testing-common/ContentTaskUtils.jsm", this); + +const MESSAGE_TELEMETRY_PAYLOAD = "Telemetry:Payload"; +const MESSAGE_TELEMETRY_GET_CHILD_PAYLOAD = "Telemetry:GetChildPayload"; +const MESSAGE_CHILD_TEST_DONE = "ChildTest:Done"; + +const PLATFORM_VERSION = "1.9.2"; +const APP_VERSION = "1"; +const APP_ID = "xpcshell@tests.mozilla.org"; +const APP_NAME = "XPCShell"; + +function run_child_test() { + // Setup histograms with some fixed values. + let flagHist = Telemetry.getHistogramById("TELEMETRY_TEST_FLAG"); + flagHist.add(1); + let countHist = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT"); + Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT", false); + countHist.add(); + Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT", true); + countHist.add(); + countHist.add(); + let categHist = Telemetry.getHistogramById("TELEMETRY_TEST_CATEGORICAL"); + categHist.add("Label2"); + categHist.add("Label3"); + + let flagKeyed = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_FLAG"); + flagKeyed.add("a", 1); + flagKeyed.add("b", 1); + let countKeyed = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT"); + Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_KEYED_COUNT", false); + countKeyed.add("a"); + countKeyed.add("b"); + Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_KEYED_COUNT", true); + countKeyed.add("a"); + countKeyed.add("b"); + countKeyed.add("b"); +} + +function check_histogram_values(payload) { + const hs = payload.histograms; + Assert.ok("TELEMETRY_TEST_COUNT" in hs, "Should have count test histogram."); + Assert.ok("TELEMETRY_TEST_FLAG" in hs, "Should have flag test histogram."); + Assert.ok("TELEMETRY_TEST_CATEGORICAL" in hs, "Should have categorical test histogram."); + Assert.equal(hs["TELEMETRY_TEST_COUNT"].sum, 2, + "Count test histogram should have the right value."); + Assert.equal(hs["TELEMETRY_TEST_FLAG"].sum, 1, + "Flag test histogram should have the right value."); + Assert.equal(hs["TELEMETRY_TEST_CATEGORICAL"].sum, 3, + "Categorical test histogram should have the right sum."); + + const kh = payload.keyedHistograms; + Assert.ok("TELEMETRY_TEST_KEYED_COUNT" in kh, "Should have keyed count test histogram."); + Assert.ok("TELEMETRY_TEST_KEYED_FLAG" in kh, "Should have keyed flag test histogram."); + Assert.equal(kh["TELEMETRY_TEST_KEYED_COUNT"]["a"].sum, 1, + "Keyed count test histogram should have the right value."); + Assert.equal(kh["TELEMETRY_TEST_KEYED_COUNT"]["b"].sum, 2, + "Keyed count test histogram should have the right value."); + Assert.equal(kh["TELEMETRY_TEST_KEYED_FLAG"]["a"].sum, 1, + "Keyed flag test histogram should have the right value."); + Assert.equal(kh["TELEMETRY_TEST_KEYED_FLAG"]["b"].sum, 1, + "Keyed flag test histogram should have the right value."); +} + +add_task(function*() { + if (!runningInParent) { + TelemetryController.testSetupContent(); + run_child_test(); + dump("... done with child test\n"); + do_send_remote_message(MESSAGE_CHILD_TEST_DONE); + return; + } + + // Setup. + do_get_profile(true); + loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION); + Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true); + yield TelemetryController.testSetup(); + if (runningInParent) { + // Make sure we don't generate unexpected pings due to pref changes. + yield setEmptyPrefWatchlist(); + } + + // Run test in child, don't wait for it to finish. + run_test_in_child("test_ChildHistograms.js"); + yield do_await_remote_message(MESSAGE_CHILD_TEST_DONE); + + yield ContentTaskUtils.waitForCondition(() => { + let payload = TelemetrySession.getPayload("test-ping"); + return payload && + "processes" in payload && + "content" in payload.processes && + "histograms" in payload.processes.content && + "TELEMETRY_TEST_COUNT" in payload.processes.content.histograms; + }); + const payload = TelemetrySession.getPayload("test-ping"); + Assert.ok("processes" in payload, "Should have processes section"); + Assert.ok("content" in payload.processes, "Should have child process section"); + Assert.ok("histograms" in payload.processes.content, "Child process section should have histograms."); + Assert.ok("keyedHistograms" in payload.processes.content, "Child process section should have keyed histograms."); + check_histogram_values(payload.processes.content); + + do_test_finished(); +}); diff --git a/toolkit/components/telemetry/tests/unit/test_PingAPI.js b/toolkit/components/telemetry/tests/unit/test_PingAPI.js new file mode 100644 index 000000000..d4d79aad4 --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_PingAPI.js @@ -0,0 +1,502 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +*/ + +// This tests the public Telemetry API for submitting pings. + +"use strict"; + +Cu.import("resource://gre/modules/ClientID.jsm", this); +Cu.import("resource://gre/modules/TelemetryController.jsm", this); +Cu.import("resource://gre/modules/TelemetryArchive.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://gre/modules/osfile.jsm", this); +Cu.import("resource://gre/modules/Task.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); + +XPCOMUtils.defineLazyGetter(this, "gPingsArchivePath", function() { + return OS.Path.join(OS.Constants.Path.profileDir, "datareporting", "archived"); +}); + +/** + * Fakes the archive storage quota. + * @param {Integer} aArchiveQuota The new quota, in bytes. + */ +function fakeStorageQuota(aArchiveQuota) { + let storage = Cu.import("resource://gre/modules/TelemetryStorage.jsm"); + storage.Policy.getArchiveQuota = () => aArchiveQuota; +} + +/** + * Lists all the valid archived pings and their metadata, sorted by creation date. + * + * @param aFileName {String} The filename. + * @return {Object[]} A list of objects with the extracted data in the form: + * { timestamp: <number>, + * id: <string>, + * type: <string>, + * size: <integer> } + */ +var getArchivedPingsInfo = Task.async(function*() { + let dirIterator = new OS.File.DirectoryIterator(gPingsArchivePath); + let subdirs = (yield dirIterator.nextBatch()).filter(e => e.isDir); + let archivedPings = []; + + // Iterate through the subdirs of |gPingsArchivePath|. + for (let dir of subdirs) { + let fileIterator = new OS.File.DirectoryIterator(dir.path); + let files = (yield fileIterator.nextBatch()).filter(e => !e.isDir); + + // Then get a list of the files for the current subdir. + for (let f of files) { + let pingInfo = TelemetryStorage._testGetArchivedPingDataFromFileName(f.name); + if (!pingInfo) { + // This is not a valid archived ping, skip it. + continue; + } + // Find the size of the ping and then add the info to the array. + pingInfo.size = (yield OS.File.stat(f.path)).size; + archivedPings.push(pingInfo); + } + } + + // Sort the list by creation date and then return it. + archivedPings.sort((a, b) => b.timestamp - a.timestamp); + return archivedPings; +}); + +add_task(function* test_setup() { + do_get_profile(true); + // Make sure we don't generate unexpected pings due to pref changes. + yield setEmptyPrefWatchlist(); + Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true); +}); + +add_task(function* test_archivedPings() { + // TelemetryController should not be fully initialized at this point. + // Submitting pings should still work fine. + + const PINGS = [ + { + type: "test-ping-api-1", + payload: { foo: "bar"}, + dateCreated: new Date(2010, 1, 1, 10, 0, 0), + }, + { + type: "test-ping-api-2", + payload: { moo: "meh"}, + dateCreated: new Date(2010, 2, 1, 10, 0, 0), + }, + ]; + + // Submit pings and check the ping list. + let expectedPingList = []; + + for (let data of PINGS) { + fakeNow(data.dateCreated); + data.id = yield TelemetryController.submitExternalPing(data.type, data.payload); + let list = yield TelemetryArchive.promiseArchivedPingList(); + + expectedPingList.push({ + id: data.id, + type: data.type, + timestampCreated: data.dateCreated.getTime(), + }); + Assert.deepEqual(list, expectedPingList, "Archived ping list should contain submitted pings"); + } + + // Check loading the archived pings. + let checkLoadingPings = Task.async(function*() { + for (let data of PINGS) { + let ping = yield TelemetryArchive.promiseArchivedPingById(data.id); + Assert.equal(ping.id, data.id, "Archived ping should have matching id"); + Assert.equal(ping.type, data.type, "Archived ping should have matching type"); + Assert.equal(ping.creationDate, data.dateCreated.toISOString(), + "Archived ping should have matching creation date"); + } + }); + + yield checkLoadingPings(); + + // Check that we find the archived pings again by scanning after a restart. + yield TelemetryController.testReset(); + + let pingList = yield TelemetryArchive.promiseArchivedPingList(); + Assert.deepEqual(expectedPingList, pingList, + "Should have submitted pings in archive list after restart"); + yield checkLoadingPings(); + + // Write invalid pings into the archive with both valid and invalid names. + let writeToArchivedDir = Task.async(function*(dirname, filename, content, compressed) { + const dirPath = OS.Path.join(gPingsArchivePath, dirname); + yield OS.File.makeDir(dirPath, { ignoreExisting: true }); + const filePath = OS.Path.join(dirPath, filename); + const options = { tmpPath: filePath + ".tmp", noOverwrite: false }; + if (compressed) { + options.compression = "lz4"; + } + yield OS.File.writeAtomic(filePath, content, options); + }); + + const FAKE_ID1 = "10000000-0123-0123-0123-0123456789a1"; + const FAKE_ID2 = "20000000-0123-0123-0123-0123456789a2"; + const FAKE_ID3 = "20000000-0123-0123-0123-0123456789a3"; + const FAKE_TYPE = "foo"; + + // These should get rejected. + yield writeToArchivedDir("xx", "foo.json", "{}"); + yield writeToArchivedDir("2010-02", "xx.xx.xx.json", "{}"); + // This one should get picked up... + yield writeToArchivedDir("2010-02", "1." + FAKE_ID1 + "." + FAKE_TYPE + ".json", "{}"); + // ... but get overwritten by this one. + yield writeToArchivedDir("2010-02", "2." + FAKE_ID1 + "." + FAKE_TYPE + ".json", ""); + // This should get picked up fine. + yield writeToArchivedDir("2010-02", "3." + FAKE_ID2 + "." + FAKE_TYPE + ".json", ""); + // This compressed ping should get picked up fine as well. + yield writeToArchivedDir("2010-02", "4." + FAKE_ID3 + "." + FAKE_TYPE + ".jsonlz4", ""); + + expectedPingList.push({ + id: FAKE_ID1, + type: "foo", + timestampCreated: 2, + }); + expectedPingList.push({ + id: FAKE_ID2, + type: "foo", + timestampCreated: 3, + }); + expectedPingList.push({ + id: FAKE_ID3, + type: "foo", + timestampCreated: 4, + }); + expectedPingList.sort((a, b) => a.timestampCreated - b.timestampCreated); + + // Reset the TelemetryArchive so we scan the archived dir again. + yield TelemetryController.testReset(); + + // Check that we are still picking up the valid archived pings on disk, + // plus the valid ones above. + pingList = yield TelemetryArchive.promiseArchivedPingList(); + Assert.deepEqual(expectedPingList, pingList, "Should have picked up valid archived pings"); + yield checkLoadingPings(); + + // Now check that we fail to load the two invalid pings from above. + Assert.ok((yield promiseRejects(TelemetryArchive.promiseArchivedPingById(FAKE_ID1))), + "Should have rejected invalid ping"); + Assert.ok((yield promiseRejects(TelemetryArchive.promiseArchivedPingById(FAKE_ID2))), + "Should have rejected invalid ping"); +}); + +add_task(function* test_archiveCleanup() { + const PING_TYPE = "foo"; + + // Empty the archive. + yield OS.File.removeDir(gPingsArchivePath); + + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SCAN_PING_COUNT").clear(); + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_DIRECTORIES_COUNT").clear(); + // Also reset these histograms to make sure normal sized pings don't get counted. + Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED").clear(); + Telemetry.getHistogramById("TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB").clear(); + + // Build the cache. Nothing should be evicted as there's no ping directory. + yield TelemetryController.testReset(); + yield TelemetryStorage.testCleanupTaskPromise(); + yield TelemetryArchive.promiseArchivedPingList(); + + let h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SCAN_PING_COUNT").snapshot(); + Assert.equal(h.sum, 0, "Telemetry must report 0 pings scanned if no archive dir exists."); + // One directory out of four was removed as well. + h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS").snapshot(); + Assert.equal(h.sum, 0, "Telemetry must report 0 evicted dirs if no archive dir exists."); + + let expectedPrunedInfo = []; + let expectedNotPrunedInfo = []; + + let checkArchive = Task.async(function*() { + // Check that the pruned pings are not on disk anymore. + for (let prunedInfo of expectedPrunedInfo) { + yield Assert.rejects(TelemetryArchive.promiseArchivedPingById(prunedInfo.id), + "Ping " + prunedInfo.id + " should have been pruned."); + const pingPath = + TelemetryStorage._testGetArchivedPingPath(prunedInfo.id, prunedInfo.creationDate, PING_TYPE); + Assert.ok(!(yield OS.File.exists(pingPath)), "The ping should not be on the disk anymore."); + } + + // Check that the expected pings are there. + for (let expectedInfo of expectedNotPrunedInfo) { + Assert.ok((yield TelemetryArchive.promiseArchivedPingById(expectedInfo.id)), + "Ping" + expectedInfo.id + " should be in the archive."); + } + }); + + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SESSION_PING_COUNT").clear(); + + // Create a ping which should be pruned because it is past the retention period. + let date = fakeNow(2010, 1, 1, 1, 0, 0); + let firstDate = date; + let pingId = yield TelemetryController.submitExternalPing(PING_TYPE, {}, {}); + expectedPrunedInfo.push({ id: pingId, creationDate: date }); + + // Create a ping which should be kept because it is within the retention period. + const oldestDirectoryDate = fakeNow(2010, 2, 1, 1, 0, 0); + pingId = yield TelemetryController.submitExternalPing(PING_TYPE, {}, {}); + expectedNotPrunedInfo.push({ id: pingId, creationDate: oldestDirectoryDate }); + + // Create 20 other pings which are within the retention period, but would be affected + // by the disk quota. + for (let month of [3, 4]) { + for (let minute = 0; minute < 10; minute++) { + date = fakeNow(2010, month, 1, 1, minute, 0); + pingId = yield TelemetryController.submitExternalPing(PING_TYPE, {}, {}); + expectedNotPrunedInfo.push({ id: pingId, creationDate: date }); + } + } + + // We expect all the pings we archived to be in this histogram. + h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SESSION_PING_COUNT"); + Assert.equal(h.snapshot().sum, 22, "All the pings must be live-accumulated in the histogram."); + // Reset the histogram that will be populated by the archive scan. + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS").clear(); + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_OLDEST_DIRECTORY_AGE").clear(); + + // Move the current date 60 days ahead of the first ping. + fakeNow(futureDate(firstDate, 60 * MILLISECONDS_PER_DAY)); + // Reset TelemetryArchive and TelemetryController to start the startup cleanup. + yield TelemetryController.testReset(); + // Wait for the cleanup to finish. + yield TelemetryStorage.testCleanupTaskPromise(); + // Then scan the archived dir. + yield TelemetryArchive.promiseArchivedPingList(); + + // Check that the archive is in the correct state. + yield checkArchive(); + + // Make sure the ping count is correct after the scan (one ping was removed). + h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SCAN_PING_COUNT").snapshot(); + Assert.equal(h.sum, 21, "The histogram must count all the pings in the archive."); + // One directory out of four was removed as well. + h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS").snapshot(); + Assert.equal(h.sum, 1, "Telemetry must correctly report removed archive directories."); + // Check that the remaining directories are correctly counted. + h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_DIRECTORIES_COUNT").snapshot(); + Assert.equal(h.sum, 3, "Telemetry must correctly report the remaining archive directories."); + // Check that the remaining directories are correctly counted. + const oldestAgeInMonths = 1; + h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_OLDEST_DIRECTORY_AGE").snapshot(); + Assert.equal(h.sum, oldestAgeInMonths, + "Telemetry must correctly report age of the oldest directory in the archive."); + + // We need to test the archive size before we hit the quota, otherwise a special + // value is recorded. + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").clear(); + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA").clear(); + Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTING_OVER_QUOTA_MS").clear(); + + // Move the current date 60 days ahead of the second ping. + fakeNow(futureDate(oldestDirectoryDate, 60 * MILLISECONDS_PER_DAY)); + // Reset TelemetryController and TelemetryArchive. + yield TelemetryController.testReset(); + // Wait for the cleanup to finish. + yield TelemetryStorage.testCleanupTaskPromise(); + // Then scan the archived dir again. + yield TelemetryArchive.promiseArchivedPingList(); + + // Move the oldest ping to the unexpected pings list. + expectedPrunedInfo.push(expectedNotPrunedInfo.shift()); + // Check that the archive is in the correct state. + yield checkArchive(); + + // Find how much disk space the archive takes. + const archivedPingsInfo = yield getArchivedPingsInfo(); + let archiveSizeInBytes = + archivedPingsInfo.reduce((lastResult, element) => lastResult + element.size, 0); + + // Check that the correct values for quota probes are reported when no quota is hit. + h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").snapshot(); + Assert.equal(h.sum, Math.round(archiveSizeInBytes / 1024 / 1024), + "Telemetry must report the correct archive size."); + h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA").snapshot(); + Assert.equal(h.sum, 0, "Telemetry must report 0 evictions if quota is not hit."); + h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTING_OVER_QUOTA_MS").snapshot(); + Assert.equal(h.sum, 0, "Telemetry must report a null elapsed time if quota is not hit."); + + // Set the quota to 80% of the space. + const testQuotaInBytes = archiveSizeInBytes * 0.8; + fakeStorageQuota(testQuotaInBytes); + + // The storage prunes archived pings until we reach 90% of the requested storage quota. + // Based on that, find how many pings should be kept. + const safeQuotaSize = testQuotaInBytes * 0.9; + let sizeInBytes = 0; + let pingsWithinQuota = []; + let pingsOutsideQuota = []; + + for (let pingInfo of archivedPingsInfo) { + sizeInBytes += pingInfo.size; + if (sizeInBytes >= safeQuotaSize) { + pingsOutsideQuota.push({ id: pingInfo.id, creationDate: new Date(pingInfo.timestamp) }); + continue; + } + pingsWithinQuota.push({ id: pingInfo.id, creationDate: new Date(pingInfo.timestamp) }); + } + + expectedNotPrunedInfo = pingsWithinQuota; + expectedPrunedInfo = expectedPrunedInfo.concat(pingsOutsideQuota); + + // Reset TelemetryArchive and TelemetryController to start the startup cleanup. + yield TelemetryController.testReset(); + yield TelemetryStorage.testCleanupTaskPromise(); + yield TelemetryArchive.promiseArchivedPingList(); + // Check that the archive is in the correct state. + yield checkArchive(); + + h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA").snapshot(); + Assert.equal(h.sum, pingsOutsideQuota.length, + "Telemetry must correctly report the over quota pings evicted from the archive."); + h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").snapshot(); + Assert.equal(h.sum, 300, "Archive quota was hit, a special size must be reported."); + + // Trigger a cleanup again and make sure we're not removing anything. + yield TelemetryController.testReset(); + yield TelemetryStorage.testCleanupTaskPromise(); + yield TelemetryArchive.promiseArchivedPingList(); + yield checkArchive(); + + const OVERSIZED_PING_ID = "9b21ec8f-f762-4d28-a2c1-44e1c4694f24"; + // Create and archive an oversized, uncompressed, ping. + const OVERSIZED_PING = { + id: OVERSIZED_PING_ID, + type: PING_TYPE, + creationDate: (new Date()).toISOString(), + // Generate a ~2MB string to use as the payload. + payload: generateRandomString(2 * 1024 * 1024) + }; + yield TelemetryArchive.promiseArchivePing(OVERSIZED_PING); + + // Get the size of the archived ping. + const oversizedPingPath = + TelemetryStorage._testGetArchivedPingPath(OVERSIZED_PING.id, new Date(OVERSIZED_PING.creationDate), PING_TYPE) + "lz4"; + const archivedPingSizeMB = Math.floor((yield OS.File.stat(oversizedPingPath)).size / 1024 / 1024); + + // We expect the oversized ping to be pruned when scanning the archive. + expectedPrunedInfo.push({ id: OVERSIZED_PING_ID, creationDate: new Date(OVERSIZED_PING.creationDate) }); + + // Scan the archive. + yield TelemetryController.testReset(); + yield TelemetryStorage.testCleanupTaskPromise(); + yield TelemetryArchive.promiseArchivedPingList(); + // The following also checks that non oversized pings are not removed. + yield checkArchive(); + + // Make sure we're correctly updating the related histograms. + h = Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED").snapshot(); + Assert.equal(h.sum, 1, "Telemetry must report 1 oversized ping in the archive."); + h = Telemetry.getHistogramById("TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB").snapshot(); + Assert.equal(h.counts[archivedPingSizeMB], 1, + "Telemetry must report the correct size for the oversized ping."); +}); + +add_task(function* test_clientId() { + // Check that a ping submitted after the delayed telemetry initialization completed + // should get a valid client id. + yield TelemetryController.testReset(); + const clientId = yield ClientID.getClientID(); + + let id = yield TelemetryController.submitExternalPing("test-type", {}, {addClientId: true}); + let ping = yield TelemetryArchive.promiseArchivedPingById(id); + + Assert.ok(!!ping, "Should have loaded the ping."); + Assert.ok("clientId" in ping, "Ping should have a client id."); + Assert.ok(UUID_REGEX.test(ping.clientId), "Client id is in UUID format."); + Assert.equal(ping.clientId, clientId, "Ping client id should match the global client id."); + + // We should have cached the client id now. Lets confirm that by + // checking the client id on a ping submitted before the async + // controller setup is finished. + let promiseSetup = TelemetryController.testReset(); + id = yield TelemetryController.submitExternalPing("test-type", {}, {addClientId: true}); + ping = yield TelemetryArchive.promiseArchivedPingById(id); + Assert.equal(ping.clientId, clientId); + + // Finish setup. + yield promiseSetup; +}); + +add_task(function* test_InvalidPingType() { + const TYPES = [ + "a", + "-", + "¿€€€?", + "-foo-", + "-moo", + "zoo-", + ".bar", + "asfd.asdf", + ]; + + for (let type of TYPES) { + let histogram = Telemetry.getKeyedHistogramById("TELEMETRY_INVALID_PING_TYPE_SUBMITTED"); + Assert.equal(histogram.snapshot(type).sum, 0, + "Should not have counted this invalid ping yet: " + type); + Assert.ok(promiseRejects(TelemetryController.submitExternalPing(type, {})), + "Ping type should have been rejected."); + Assert.equal(histogram.snapshot(type).sum, 1, + "Should have counted this as an invalid ping type."); + } +}); + +add_task(function* test_InvalidPayloadType() { + const PAYLOAD_TYPES = [ + 19, + "string", + [1, 2, 3, 4], + null, + undefined, + ]; + + let histogram = Telemetry.getHistogramById("TELEMETRY_INVALID_PAYLOAD_SUBMITTED"); + for (let i = 0; i < PAYLOAD_TYPES.length; i++) { + histogram.clear(); + Assert.equal(histogram.snapshot().sum, 0, + "Should not have counted this invalid payload yet: " + JSON.stringify(PAYLOAD_TYPES[i])); + Assert.ok(yield promiseRejects(TelemetryController.submitExternalPing("payload-test", PAYLOAD_TYPES[i])), + "Payload type should have been rejected."); + Assert.equal(histogram.snapshot().sum, 1, + "Should have counted this as an invalid payload type."); + } +}); + +add_task(function* test_currentPingData() { + yield TelemetryController.testSetup(); + + // Setup test data. + let h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT"); + h.clear(); + h.add(1); + let k = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"); + k.clear(); + k.add("a", 1); + + // Get current ping data objects and check that their data is sane. + for (let subsession of [true, false]) { + let ping = TelemetryController.getCurrentPingData(subsession); + + Assert.ok(!!ping, "Should have gotten a ping."); + Assert.equal(ping.type, "main", "Ping should have correct type."); + const expectedReason = subsession ? "gather-subsession-payload" : "gather-payload"; + Assert.equal(ping.payload.info.reason, expectedReason, "Ping should have the correct reason."); + + let id = "TELEMETRY_TEST_RELEASE_OPTOUT"; + Assert.ok(id in ping.payload.histograms, "Payload should have test count histogram."); + Assert.equal(ping.payload.histograms[id].sum, 1, "Test count value should match."); + id = "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"; + Assert.ok(id in ping.payload.keyedHistograms, "Payload should have keyed test histogram."); + Assert.equal(ping.payload.keyedHistograms[id]["a"].sum, 1, "Keyed test value should match."); + } +}); + +add_task(function* test_shutdown() { + yield TelemetryController.testShutdown(); +}); diff --git a/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js b/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js new file mode 100644 index 000000000..c86fb0499 --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js @@ -0,0 +1,236 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +*/ + +Cu.import("resource://gre/modules/Preferences.jsm", this); +Cu.import("resource://gre/modules/Promise.jsm", this); +Cu.import("resource://gre/modules/Task.jsm", this); +Cu.import("resource://gre/modules/TelemetryArchive.jsm", this); +Cu.import("resource://gre/modules/TelemetryController.jsm", this); +Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", this); +Cu.import("resource://gre/modules/osfile.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); + +const MS_IN_ONE_HOUR = 60 * 60 * 1000; +const MS_IN_ONE_DAY = 24 * MS_IN_ONE_HOUR; + +const PREF_BRANCH = "toolkit.telemetry."; +const PREF_ARCHIVE_ENABLED = PREF_BRANCH + "archive.enabled"; + +const REASON_ABORTED_SESSION = "aborted-session"; +const REASON_DAILY = "daily"; +const REASON_ENVIRONMENT_CHANGE = "environment-change"; +const REASON_SHUTDOWN = "shutdown"; + +XPCOMUtils.defineLazyGetter(this, "DATAREPORTING_PATH", function() { + return OS.Path.join(OS.Constants.Path.profileDir, "datareporting"); +}); + +var promiseValidateArchivedPings = Task.async(function*(aExpectedReasons) { + // The list of ping reasons which mark the session end (and must reset the subsession + // count). + const SESSION_END_PING_REASONS = new Set([ REASON_ABORTED_SESSION, REASON_SHUTDOWN ]); + + let list = yield TelemetryArchive.promiseArchivedPingList(); + + // We're just interested in the "main" pings. + list = list.filter(p => p.type == "main"); + + Assert.equal(aExpectedReasons.length, list.length, "All the expected pings must be received."); + + let previousPing = yield TelemetryArchive.promiseArchivedPingById(list[0].id); + Assert.equal(aExpectedReasons.shift(), previousPing.payload.info.reason, + "Telemetry should only get pings with expected reasons."); + Assert.equal(previousPing.payload.info.previousSessionId, null, + "The first session must report a null previous session id."); + Assert.equal(previousPing.payload.info.previousSubsessionId, null, + "The first subsession must report a null previous subsession id."); + Assert.equal(previousPing.payload.info.profileSubsessionCounter, 1, + "profileSubsessionCounter must be 1 the first time."); + Assert.equal(previousPing.payload.info.subsessionCounter, 1, + "subsessionCounter must be 1 the first time."); + + let expectedSubsessionCounter = 1; + let expectedPreviousSessionId = previousPing.payload.info.sessionId; + + for (let i = 1; i < list.length; i++) { + let currentPing = yield TelemetryArchive.promiseArchivedPingById(list[i].id); + let currentInfo = currentPing.payload.info; + let previousInfo = previousPing.payload.info; + do_print("Archive entry " + i + " - id: " + currentPing.id + ", reason: " + currentInfo.reason); + + Assert.equal(aExpectedReasons.shift(), currentInfo.reason, + "Telemetry should only get pings with expected reasons."); + Assert.equal(currentInfo.previousSessionId, expectedPreviousSessionId, + "Telemetry must correctly chain session identifiers."); + Assert.equal(currentInfo.previousSubsessionId, previousInfo.subsessionId, + "Telemetry must correctly chain subsession identifiers."); + Assert.equal(currentInfo.profileSubsessionCounter, previousInfo.profileSubsessionCounter + 1, + "Telemetry must correctly track the profile subsessions count."); + Assert.equal(currentInfo.subsessionCounter, expectedSubsessionCounter, + "The subsession counter should be monotonically increasing."); + + // Store the current ping as previous. + previousPing = currentPing; + // Reset the expected subsession counter, if required. Otherwise increment the expected + // subsession counter. + // If this is the final subsession of a session we need to update expected values accordingly. + if (SESSION_END_PING_REASONS.has(currentInfo.reason)) { + expectedSubsessionCounter = 1; + expectedPreviousSessionId = currentInfo.sessionId; + } else { + expectedSubsessionCounter++; + } + } +}); + +add_task(function* test_setup() { + do_test_pending(); + + // Addon manager needs a profile directory + do_get_profile(); + loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + // Make sure we don't generate unexpected pings due to pref changes. + yield setEmptyPrefWatchlist(); + + Preferences.set(PREF_TELEMETRY_ENABLED, true); +}); + +add_task(function* test_subsessionsChaining() { + if (gIsAndroid) { + // We don't support subsessions yet on Android, so skip the next checks. + return; + } + + const PREF_TEST = PREF_BRANCH + "test.pref1"; + const PREFS_TO_WATCH = new Map([ + [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_VALUE}], + ]); + Preferences.reset(PREF_TEST); + + // Fake the clock data to manually trigger an aborted-session ping and a daily ping. + // This is also helpful to make sure we get the archived pings in an expected order. + let now = fakeNow(2009, 9, 18, 0, 0, 0); + let monotonicNow = fakeMonotonicNow(1000); + + let moveClockForward = (minutes) => { + let ms = minutes * MILLISECONDS_PER_MINUTE; + now = fakeNow(futureDate(now, ms)); + monotonicNow = fakeMonotonicNow(monotonicNow + ms); + } + + // Keep track of the ping reasons we're expecting in this test. + let expectedReasons = []; + + // Start and shut down Telemetry. We expect a shutdown ping with profileSubsessionCounter: 1, + // subsessionCounter: 1, subsessionId: A, and previousSubsessionId: null to be archived. + yield TelemetryController.testSetup(); + yield TelemetryController.testShutdown(); + expectedReasons.push(REASON_SHUTDOWN); + + // Start Telemetry but don't wait for it to initialise before shutting down. We expect a + // shutdown ping with profileSubsessionCounter: 2, subsessionCounter: 1, subsessionId: B + // and previousSubsessionId: A to be archived. + moveClockForward(30); + TelemetryController.testReset(); + yield TelemetryController.testShutdown(); + expectedReasons.push(REASON_SHUTDOWN); + + // Start Telemetry and simulate an aborted-session ping. We expect an aborted-session ping + // with profileSubsessionCounter: 3, subsessionCounter: 1, subsessionId: C and + // previousSubsessionId: B to be archived. + let schedulerTickCallback = null; + fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {}); + yield TelemetryController.testReset(); + moveClockForward(6); + // Trigger the an aborted session ping save. When testing,we are not saving the aborted-session + // ping as soon as Telemetry starts, otherwise we would end up with unexpected pings being + // sent when calling |TelemetryController.testReset()|, thus breaking some tests. + Assert.ok(!!schedulerTickCallback); + yield schedulerTickCallback(); + expectedReasons.push(REASON_ABORTED_SESSION); + + // Start Telemetry and trigger an environment change through a pref modification. We expect + // an environment-change ping with profileSubsessionCounter: 4, subsessionCounter: 1, + // subsessionId: D and previousSubsessionId: C to be archived. + moveClockForward(30); + yield TelemetryController.testReset(); + TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); + moveClockForward(30); + Preferences.set(PREF_TEST, 1); + expectedReasons.push(REASON_ENVIRONMENT_CHANGE); + + // Shut down Telemetry. We expect a shutdown ping with profileSubsessionCounter: 5, + // subsessionCounter: 2, subsessionId: E and previousSubsessionId: D to be archived. + moveClockForward(30); + yield TelemetryController.testShutdown(); + expectedReasons.push(REASON_SHUTDOWN); + + // Start Telemetry and trigger a daily ping. We expect a daily ping with + // profileSubsessionCounter: 6, subsessionCounter: 1, subsessionId: F and + // previousSubsessionId: E to be archived. + moveClockForward(30); + yield TelemetryController.testReset(); + + // Delay the callback around midnight. + now = fakeNow(futureDate(now, MS_IN_ONE_DAY)); + // Trigger the daily ping. + yield schedulerTickCallback(); + expectedReasons.push(REASON_DAILY); + + // Trigger an environment change ping. We expect an environment-changed ping with + // profileSubsessionCounter: 7, subsessionCounter: 2, subsessionId: G and + // previousSubsessionId: F to be archived. + moveClockForward(30); + Preferences.set(PREF_TEST, 0); + expectedReasons.push(REASON_ENVIRONMENT_CHANGE); + + // Shut down Telemetry and trigger a shutdown ping. + moveClockForward(30); + yield TelemetryController.testShutdown(); + expectedReasons.push(REASON_SHUTDOWN); + + // Start Telemetry and trigger an environment change. + yield TelemetryController.testReset(); + TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); + moveClockForward(30); + Preferences.set(PREF_TEST, 1); + expectedReasons.push(REASON_ENVIRONMENT_CHANGE); + + // Don't shut down, instead trigger an aborted-session ping. + moveClockForward(6); + // Trigger the an aborted session ping save. + yield schedulerTickCallback(); + expectedReasons.push(REASON_ABORTED_SESSION); + + // Start Telemetry and trigger a daily ping. + moveClockForward(30); + yield TelemetryController.testReset(); + // Delay the callback around midnight. + now = futureDate(now, MS_IN_ONE_DAY); + fakeNow(now); + // Trigger the daily ping. + yield schedulerTickCallback(); + expectedReasons.push(REASON_DAILY); + + // Trigger an environment change. + moveClockForward(30); + Preferences.set(PREF_TEST, 0); + expectedReasons.push(REASON_ENVIRONMENT_CHANGE); + + // And an aborted-session ping again. + moveClockForward(6); + // Trigger the an aborted session ping save. + yield schedulerTickCallback(); + expectedReasons.push(REASON_ABORTED_SESSION); + + // Make sure the aborted-session ping gets archived. + yield TelemetryController.testReset(); + + yield promiseValidateArchivedPings(expectedReasons); +}); + +add_task(function* () { + yield TelemetryController.testShutdown(); + do_test_finished(); +}); diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryController.js b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js new file mode 100644 index 000000000..b383de6bf --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js @@ -0,0 +1,507 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +*/ +/* This testcase triggers two telemetry pings. + * + * Telemetry code keeps histograms of past telemetry pings. The first + * ping populates these histograms. One of those histograms is then + * checked in the second request. + */ + +Cu.import("resource://gre/modules/ClientID.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://gre/modules/TelemetryController.jsm", this); +Cu.import("resource://gre/modules/TelemetryStorage.jsm", this); +Cu.import("resource://gre/modules/TelemetrySend.jsm", this); +Cu.import("resource://gre/modules/TelemetryArchive.jsm", this); +Cu.import("resource://gre/modules/Task.jsm", this); +Cu.import("resource://gre/modules/Promise.jsm", this); +Cu.import("resource://gre/modules/Preferences.jsm"); + +const PING_FORMAT_VERSION = 4; +const DELETION_PING_TYPE = "deletion"; +const TEST_PING_TYPE = "test-ping-type"; + +const PLATFORM_VERSION = "1.9.2"; +const APP_VERSION = "1"; +const APP_NAME = "XPCShell"; + +const PREF_BRANCH = "toolkit.telemetry."; +const PREF_ENABLED = PREF_BRANCH + "enabled"; +const PREF_ARCHIVE_ENABLED = PREF_BRANCH + "archive.enabled"; +const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled"; +const PREF_UNIFIED = PREF_BRANCH + "unified"; + +var gClientID = null; + +function sendPing(aSendClientId, aSendEnvironment) { + if (PingServer.started) { + TelemetrySend.setServer("http://localhost:" + PingServer.port); + } else { + TelemetrySend.setServer("http://doesnotexist"); + } + + let options = { + addClientId: aSendClientId, + addEnvironment: aSendEnvironment, + }; + return TelemetryController.submitExternalPing(TEST_PING_TYPE, {}, options); +} + +function checkPingFormat(aPing, aType, aHasClientId, aHasEnvironment) { + const MANDATORY_PING_FIELDS = [ + "type", "id", "creationDate", "version", "application", "payload" + ]; + + const APPLICATION_TEST_DATA = { + buildId: gAppInfo.appBuildID, + name: APP_NAME, + version: APP_VERSION, + displayVersion: AppConstants.MOZ_APP_VERSION_DISPLAY, + vendor: "Mozilla", + platformVersion: PLATFORM_VERSION, + xpcomAbi: "noarch-spidermonkey", + }; + + // Check that the ping contains all the mandatory fields. + for (let f of MANDATORY_PING_FIELDS) { + Assert.ok(f in aPing, f + " must be available."); + } + + Assert.equal(aPing.type, aType, "The ping must have the correct type."); + Assert.equal(aPing.version, PING_FORMAT_VERSION, "The ping must have the correct version."); + + // Test the application section. + for (let f in APPLICATION_TEST_DATA) { + Assert.equal(aPing.application[f], APPLICATION_TEST_DATA[f], + f + " must have the correct value."); + } + + // We can't check the values for channel and architecture. Just make + // sure they are in. + Assert.ok("architecture" in aPing.application, + "The application section must have an architecture field."); + Assert.ok("channel" in aPing.application, + "The application section must have a channel field."); + + // Check the clientId and environment fields, as needed. + Assert.equal("clientId" in aPing, aHasClientId); + Assert.equal("environment" in aPing, aHasEnvironment); +} + +add_task(function* test_setup() { + // Addon manager needs a profile directory + do_get_profile(); + loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + // Make sure we don't generate unexpected pings due to pref changes. + yield setEmptyPrefWatchlist(); + + Services.prefs.setBoolPref(PREF_ENABLED, true); + Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true); + + yield new Promise(resolve => + Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(resolve))); +}); + +add_task(function* asyncSetup() { + yield TelemetryController.testSetup(); +}); + +// Ensure that not overwriting an existing file fails silently +add_task(function* test_overwritePing() { + let ping = {id: "foo"}; + yield TelemetryStorage.savePing(ping, true); + yield TelemetryStorage.savePing(ping, false); + yield TelemetryStorage.cleanupPingFile(ping); +}); + +// Checks that a sent ping is correctly received by a dummy http server. +add_task(function* test_simplePing() { + PingServer.start(); + // Update the Telemetry Server preference with the address of the local server. + // Otherwise we might end up sending stuff to a non-existing server after + // |TelemetryController.testReset| is called. + Preferences.set(TelemetryController.Constants.PREF_SERVER, "http://localhost:" + PingServer.port); + + yield sendPing(false, false); + let request = yield PingServer.promiseNextRequest(); + + // Check that we have a version query parameter in the URL. + Assert.notEqual(request.queryString, ""); + + // Make sure the version in the query string matches the new ping format version. + let params = request.queryString.split("&"); + Assert.ok(params.find(p => p == ("v=" + PING_FORMAT_VERSION))); + + let ping = decodeRequestPayload(request); + checkPingFormat(ping, TEST_PING_TYPE, false, false); +}); + +add_task(function* test_disableDataUpload() { + const isUnified = Preferences.get(PREF_UNIFIED, false); + if (!isUnified) { + // Skipping the test if unified telemetry is off, as no deletion ping will + // be generated. + return; + } + + // Disable FHR upload: this should trigger a deletion ping. + Preferences.set(PREF_FHR_UPLOAD_ENABLED, false); + + let ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, DELETION_PING_TYPE, true, false); + // Wait on ping activity to settle. + yield TelemetrySend.testWaitOnOutgoingPings(); + + // Restore FHR Upload. + Preferences.set(PREF_FHR_UPLOAD_ENABLED, true); + + // Simulate a failure in sending the deletion ping by disabling the HTTP server. + yield PingServer.stop(); + + // Try to send a ping. It will be saved as pending and get deleted when disabling upload. + TelemetryController.submitExternalPing(TEST_PING_TYPE, {}); + + // Disable FHR upload to send a deletion ping again. + Preferences.set(PREF_FHR_UPLOAD_ENABLED, false); + + // Wait on sending activity to settle, as |TelemetryController.testReset()| doesn't do that. + yield TelemetrySend.testWaitOnOutgoingPings(); + // Wait for the pending pings to be deleted. Resetting TelemetryController doesn't + // trigger the shutdown, so we need to call it ourselves. + yield TelemetryStorage.shutdown(); + // Simulate a restart, and spin the send task. + yield TelemetryController.testReset(); + + // Disabling Telemetry upload must clear out all the pending pings. + let pendingPings = yield TelemetryStorage.loadPendingPingList(); + Assert.equal(pendingPings.length, 1, + "All the pending pings but the deletion ping should have been deleted"); + + // Enable the ping server again. + PingServer.start(); + // We set the new server using the pref, otherwise it would get reset with + // |TelemetryController.testReset|. + Preferences.set(TelemetryController.Constants.PREF_SERVER, "http://localhost:" + PingServer.port); + + // Stop the sending task and then start it again. + yield TelemetrySend.shutdown(); + // Reset the controller to spin the ping sending task. + yield TelemetryController.testReset(); + ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, DELETION_PING_TYPE, true, false); + + // Wait on ping activity to settle before moving on to the next test. If we were + // to shut down telemetry, even though the PingServer caught the expected pings, + // TelemetrySend could still be processing them (clearing pings would happen in + // a couple of ticks). Shutting down would cancel the request and save them as + // pending pings. + yield TelemetrySend.testWaitOnOutgoingPings(); + // Restore FHR Upload. + Preferences.set(PREF_FHR_UPLOAD_ENABLED, true); +}); + +add_task(function* test_pingHasClientId() { + const PREF_CACHED_CLIENTID = "toolkit.telemetry.cachedClientID"; + + // Make sure we have no cached client ID for this test: we'll try to send + // a ping with it while Telemetry is being initialized. + Preferences.reset(PREF_CACHED_CLIENTID); + yield TelemetryController.testShutdown(); + yield ClientID._reset(); + yield TelemetryStorage.testClearPendingPings(); + // And also clear the counter histogram since we're here. + let h = Telemetry.getHistogramById("TELEMETRY_PING_SUBMISSION_WAITING_CLIENTID"); + h.clear(); + + // Init telemetry and try to send a ping with a client ID. + let promisePingSetup = TelemetryController.testReset(); + yield sendPing(true, false); + Assert.equal(h.snapshot().sum, 1, + "We must have a ping waiting for the clientId early during startup."); + // Wait until we are fully initialized. Pings will be assembled but won't get + // sent before then. + yield promisePingSetup; + + let ping = yield PingServer.promiseNextPing(); + // Fetch the client ID after initializing and fetching the the ping, so we + // don't unintentionally trigger its loading. We'll still need the client ID + // to see if the ping looks sane. + gClientID = yield ClientID.getClientID(); + + checkPingFormat(ping, TEST_PING_TYPE, true, false); + Assert.equal(ping.clientId, gClientID, "The correct clientId must be reported."); + + // Shutdown Telemetry so we can safely restart it. + yield TelemetryController.testShutdown(); + yield TelemetryStorage.testClearPendingPings(); + + // We should have cached the client ID now. Lets confirm that by checking it before + // the async ping setup is finished. + h.clear(); + promisePingSetup = TelemetryController.testReset(); + yield sendPing(true, false); + yield promisePingSetup; + + // Check that we received the cached client id. + Assert.equal(h.snapshot().sum, 0, "We must have used the cached clientId."); + ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, TEST_PING_TYPE, true, false); + Assert.equal(ping.clientId, gClientID, + "Telemetry should report the correct cached clientId."); + + // Check that sending a ping without relying on the cache, after the + // initialization, still works. + Preferences.reset(PREF_CACHED_CLIENTID); + yield TelemetryController.testShutdown(); + yield TelemetryStorage.testClearPendingPings(); + yield TelemetryController.testReset(); + yield sendPing(true, false); + ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, TEST_PING_TYPE, true, false); + Assert.equal(ping.clientId, gClientID, "The correct clientId must be reported."); + Assert.equal(h.snapshot().sum, 0, "No ping should have been waiting for a clientId."); +}); + +add_task(function* test_pingHasEnvironment() { + // Send a ping with the environment data. + yield sendPing(false, true); + let ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, TEST_PING_TYPE, false, true); + + // Test a field in the environment build section. + Assert.equal(ping.application.buildId, ping.environment.build.buildId); +}); + +add_task(function* test_pingHasEnvironmentAndClientId() { + // Send a ping with the environment data and client id. + yield sendPing(true, true); + let ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, TEST_PING_TYPE, true, true); + + // Test a field in the environment build section. + Assert.equal(ping.application.buildId, ping.environment.build.buildId); + // Test that we have the correct clientId. + Assert.equal(ping.clientId, gClientID, "The correct clientId must be reported."); +}); + +add_task(function* test_archivePings() { + let now = new Date(2009, 10, 18, 12, 0, 0); + fakeNow(now); + + // Disable ping upload so that pings don't get sent. + // With unified telemetry the FHR upload pref controls this, + // with non-unified telemetry the Telemetry enabled pref. + const isUnified = Preferences.get(PREF_UNIFIED, false); + const uploadPref = isUnified ? PREF_FHR_UPLOAD_ENABLED : PREF_ENABLED; + Preferences.set(uploadPref, false); + + // If we're using unified telemetry, disabling ping upload will generate a "deletion" + // ping. Catch it. + if (isUnified) { + let ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, DELETION_PING_TYPE, true, false); + } + + // Register a new Ping Handler that asserts if a ping is received, then send a ping. + PingServer.registerPingHandler(() => Assert.ok(false, "Telemetry must not send pings if not allowed to.")); + let pingId = yield sendPing(true, true); + + // Check that the ping was archived, even with upload disabled. + let ping = yield TelemetryArchive.promiseArchivedPingById(pingId); + Assert.equal(ping.id, pingId, "TelemetryController should still archive pings."); + + // Check that pings don't get archived if not allowed to. + now = new Date(2010, 10, 18, 12, 0, 0); + fakeNow(now); + Preferences.set(PREF_ARCHIVE_ENABLED, false); + pingId = yield sendPing(true, true); + let promise = TelemetryArchive.promiseArchivedPingById(pingId); + Assert.ok((yield promiseRejects(promise)), + "TelemetryController should not archive pings if the archive pref is disabled."); + + // Enable archiving and the upload so that pings get sent and archived again. + Preferences.set(uploadPref, true); + Preferences.set(PREF_ARCHIVE_ENABLED, true); + + now = new Date(2014, 6, 18, 22, 0, 0); + fakeNow(now); + // Restore the non asserting ping handler. + PingServer.resetPingHandler(); + pingId = yield sendPing(true, true); + + // Check that we archive pings when successfully sending them. + yield PingServer.promiseNextPing(); + ping = yield TelemetryArchive.promiseArchivedPingById(pingId); + Assert.equal(ping.id, pingId, + "TelemetryController should still archive pings if ping upload is enabled."); +}); + +// Test that we fuzz the submission time around midnight properly +// to avoid overloading the telemetry servers. +add_task(function* test_midnightPingSendFuzzing() { + const fuzzingDelay = 60 * 60 * 1000; + fakeMidnightPingFuzzingDelay(fuzzingDelay); + let now = new Date(2030, 5, 1, 11, 0, 0); + fakeNow(now); + + let waitForTimer = () => new Promise(resolve => { + fakePingSendTimer((callback, timeout) => { + resolve([callback, timeout]); + }, () => {}); + }); + + PingServer.clearRequests(); + yield TelemetryController.testReset(); + + // A ping after midnight within the fuzzing delay should not get sent. + now = new Date(2030, 5, 2, 0, 40, 0); + fakeNow(now); + PingServer.registerPingHandler((req, res) => { + Assert.ok(false, "No ping should be received yet."); + }); + let timerPromise = waitForTimer(); + yield sendPing(true, true); + let [timerCallback, timerTimeout] = yield timerPromise; + Assert.ok(!!timerCallback); + Assert.deepEqual(futureDate(now, timerTimeout), new Date(2030, 5, 2, 1, 0, 0)); + + // A ping just before the end of the fuzzing delay should not get sent. + now = new Date(2030, 5, 2, 0, 59, 59); + fakeNow(now); + timerPromise = waitForTimer(); + yield sendPing(true, true); + [timerCallback, timerTimeout] = yield timerPromise; + Assert.deepEqual(timerTimeout, 1 * 1000); + + // Restore the previous ping handler. + PingServer.resetPingHandler(); + + // Setting the clock to after the fuzzing delay, we should trigger the two ping sends + // with the timer callback. + now = futureDate(now, timerTimeout); + fakeNow(now); + yield timerCallback(); + const pings = yield PingServer.promiseNextPings(2); + for (let ping of pings) { + checkPingFormat(ping, TEST_PING_TYPE, true, true); + } + yield TelemetrySend.testWaitOnOutgoingPings(); + + // Moving the clock further we should still send pings immediately. + now = futureDate(now, 5 * 60 * 1000); + yield sendPing(true, true); + let ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, TEST_PING_TYPE, true, true); + yield TelemetrySend.testWaitOnOutgoingPings(); + + // Check that pings shortly before midnight are immediately sent. + now = fakeNow(2030, 5, 3, 23, 59, 0); + yield sendPing(true, true); + ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, TEST_PING_TYPE, true, true); + yield TelemetrySend.testWaitOnOutgoingPings(); + + // Clean-up. + fakeMidnightPingFuzzingDelay(0); + fakePingSendTimer(() => {}, () => {}); +}); + +add_task(function* test_changePingAfterSubmission() { + // Submit a ping with a custom payload. + let payload = { canary: "test" }; + let pingPromise = TelemetryController.submitExternalPing(TEST_PING_TYPE, payload, options); + + // Change the payload with a predefined value. + payload.canary = "changed"; + + // Wait for the ping to be archived. + const pingId = yield pingPromise; + + // Make sure our changes didn't affect the submitted payload. + let archivedCopy = yield TelemetryArchive.promiseArchivedPingById(pingId); + Assert.equal(archivedCopy.payload.canary, "test", + "The payload must not be changed after being submitted."); +}); + +add_task(function* test_telemetryEnabledUnexpectedValue() { + // Remove the default value for toolkit.telemetry.enabled from the default prefs. + // Otherwise, we wouldn't be able to set the pref to a string. + let defaultPrefBranch = Services.prefs.getDefaultBranch(null); + defaultPrefBranch.deleteBranch(PREF_ENABLED); + + // Set the preferences controlling the Telemetry status to a string. + Preferences.set(PREF_ENABLED, "false"); + // Check that Telemetry is not enabled. + yield TelemetryController.testReset(); + Assert.equal(Telemetry.canRecordExtended, false, + "Invalid values must not enable Telemetry recording."); + + // Delete the pref again. + defaultPrefBranch.deleteBranch(PREF_ENABLED); + + // Make sure that flipping it to true works. + Preferences.set(PREF_ENABLED, true); + yield TelemetryController.testReset(); + Assert.equal(Telemetry.canRecordExtended, true, + "True must enable Telemetry recording."); + + // Also check that the false works as well. + Preferences.set(PREF_ENABLED, false); + yield TelemetryController.testReset(); + Assert.equal(Telemetry.canRecordExtended, false, + "False must disable Telemetry recording."); +}); + +add_task(function* test_telemetryCleanFHRDatabase() { + const FHR_DBNAME_PREF = "datareporting.healthreport.dbName"; + const CUSTOM_DB_NAME = "unlikely.to.be.used.sqlite"; + const DEFAULT_DB_NAME = "healthreport.sqlite"; + + // Check that we're able to remove a FHR DB with a custom name. + const CUSTOM_DB_PATHS = [ + OS.Path.join(OS.Constants.Path.profileDir, CUSTOM_DB_NAME), + OS.Path.join(OS.Constants.Path.profileDir, CUSTOM_DB_NAME + "-wal"), + OS.Path.join(OS.Constants.Path.profileDir, CUSTOM_DB_NAME + "-shm"), + ]; + Preferences.set(FHR_DBNAME_PREF, CUSTOM_DB_NAME); + + // Write fake DB files to the profile directory. + for (let dbFilePath of CUSTOM_DB_PATHS) { + yield OS.File.writeAtomic(dbFilePath, "some data"); + } + + // Trigger the cleanup and check that the files were removed. + yield TelemetryStorage.removeFHRDatabase(); + for (let dbFilePath of CUSTOM_DB_PATHS) { + Assert.ok(!(yield OS.File.exists(dbFilePath)), "The DB must not be on the disk anymore: " + dbFilePath); + } + + // We should not break anything if there's no DB file. + yield TelemetryStorage.removeFHRDatabase(); + + // Check that we're able to remove a FHR DB with the default name. + Preferences.reset(FHR_DBNAME_PREF); + + const DEFAULT_DB_PATHS = [ + OS.Path.join(OS.Constants.Path.profileDir, DEFAULT_DB_NAME), + OS.Path.join(OS.Constants.Path.profileDir, DEFAULT_DB_NAME + "-wal"), + OS.Path.join(OS.Constants.Path.profileDir, DEFAULT_DB_NAME + "-shm"), + ]; + + // Write fake DB files to the profile directory. + for (let dbFilePath of DEFAULT_DB_PATHS) { + yield OS.File.writeAtomic(dbFilePath, "some data"); + } + + // Trigger the cleanup and check that the files were removed. + yield TelemetryStorage.removeFHRDatabase(); + for (let dbFilePath of DEFAULT_DB_PATHS) { + Assert.ok(!(yield OS.File.exists(dbFilePath)), "The DB must not be on the disk anymore: " + dbFilePath); + } +}); + +add_task(function* stopServer() { + yield PingServer.stop(); +}); diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryControllerBuildID.js b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerBuildID.js new file mode 100644 index 000000000..b8a88afa2 --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerBuildID.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +*/ +/* Test inclusion of previous build ID in telemetry pings when build ID changes. + * bug 841028 + * + * Cases to cover: + * 1) Run with no "previousBuildID" stored in prefs: + * -> no previousBuildID in telemetry system info, new value set in prefs. + * 2) previousBuildID in prefs, equal to current build ID: + * -> no previousBuildID in telemetry, prefs not updated. + * 3) previousBuildID in prefs, not equal to current build ID: + * -> previousBuildID in telemetry, new value set in prefs. + */ + +"use strict"; + +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/TelemetryController.jsm", this); +Cu.import("resource://gre/modules/TelemetrySession.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +// Force the Telemetry enabled preference so that TelemetrySession.testReset() doesn't exit early. +Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true); + +// Set up our dummy AppInfo object so we can control the appBuildID. +Cu.import("resource://testing-common/AppInfo.jsm", this); +updateAppInfo(); + +// Check that when run with no previous build ID stored, we update the pref but do not +// put anything into the metadata. +add_task(function* test_firstRun() { + yield TelemetryController.testReset(); + let metadata = TelemetrySession.getMetadata(); + do_check_false("previousBuildID" in metadata); + let appBuildID = getAppInfo().appBuildID; + let buildIDPref = Services.prefs.getCharPref(TelemetrySession.Constants.PREF_PREVIOUS_BUILDID); + do_check_eq(appBuildID, buildIDPref); +}); + +// Check that a subsequent run with the same build ID does not put prev build ID in +// metadata. Assumes testFirstRun() has already been called to set the previousBuildID pref. +add_task(function* test_secondRun() { + yield TelemetryController.testReset(); + let metadata = TelemetrySession.getMetadata(); + do_check_false("previousBuildID" in metadata); +}); + +// Set up telemetry with a different app build ID and check that the old build ID +// is returned in the metadata and the pref is updated to the new build ID. +// Assumes testFirstRun() has been called to set the previousBuildID pref. +const NEW_BUILD_ID = "20130314"; +add_task(function* test_newBuild() { + let info = getAppInfo(); + let oldBuildID = info.appBuildID; + info.appBuildID = NEW_BUILD_ID; + yield TelemetryController.testReset(); + let metadata = TelemetrySession.getMetadata(); + do_check_eq(metadata.previousBuildId, oldBuildID); + let buildIDPref = Services.prefs.getCharPref(TelemetrySession.Constants.PREF_PREVIOUS_BUILDID); + do_check_eq(NEW_BUILD_ID, buildIDPref); +}); + + +function run_test() { + // Make sure we have a profile directory. + do_get_profile(); + + run_next_test(); +} diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js new file mode 100644 index 000000000..391db0d9d --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that TelemetryController sends close to shutdown don't lead +// to AsyncShutdown timeouts. + +"use strict"; + +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/TelemetryController.jsm", this); +Cu.import("resource://gre/modules/TelemetrySend.jsm", this); +Cu.import("resource://gre/modules/Timer.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://gre/modules/AsyncShutdown.jsm", this); +Cu.import("resource://testing-common/httpd.js", this); + +const PREF_BRANCH = "toolkit.telemetry."; +const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled"; + +function contentHandler(metadata, response) +{ + dump("contentHandler called for path: " + metadata._path + "\n"); + // We intentionally don't finish writing the response here to let the + // client time out. + response.processAsync(); + response.setHeader("Content-Type", "text/plain"); +} + +add_task(function* test_setup() { + // Addon manager needs a profile directory + do_get_profile(); + loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + // Make sure we don't generate unexpected pings due to pref changes. + yield setEmptyPrefWatchlist(); + + Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true); + Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true); +}); + +/** + * Ensures that TelemetryController does not hang processing shutdown + * phases. Assumes that Telemetry shutdown routines do not take longer than + * CRASH_TIMEOUT_MS to complete. + */ +add_task(function* test_sendTelemetryShutsDownWithinReasonableTimeout() { + const CRASH_TIMEOUT_MS = 5 * 1000; + // Enable testing mode for AsyncShutdown, otherwise some testing-only functionality + // is not available. + Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true); + // Reducing the max delay for waitiing on phases to complete from 1 minute + // (standard) to 10 seconds to avoid blocking the tests in case of misbehavior. + Services.prefs.setIntPref("toolkit.asyncshutdown.crash_timeout", CRASH_TIMEOUT_MS); + + let httpServer = new HttpServer(); + httpServer.registerPrefixHandler("/", contentHandler); + httpServer.start(-1); + + yield TelemetryController.testSetup(); + TelemetrySend.setServer("http://localhost:" + httpServer.identity.primaryPort); + let submissionPromise = TelemetryController.submitExternalPing("test-ping-type", {}); + + // Trigger the AsyncShutdown phase TelemetryController hangs off. + AsyncShutdown.profileBeforeChange._trigger(); + AsyncShutdown.sendTelemetry._trigger(); + // Now wait for the ping submission. + yield submissionPromise; + + // If we get here, we didn't time out in the shutdown routines. + Assert.ok(true, "Didn't time out on shutdown."); +}); diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js b/toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js new file mode 100644 index 000000000..ca5d1820b --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that TelemetrySession notifies correctly on idle-daily. + +Cu.import("resource://testing-common/httpd.js", this); +Cu.import("resource://gre/modules/PromiseUtils.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/Task.jsm", this); +Cu.import("resource://gre/modules/TelemetryStorage.jsm", this); +Cu.import("resource://gre/modules/TelemetryController.jsm", this); +Cu.import("resource://gre/modules/TelemetrySession.jsm", this); +Cu.import("resource://gre/modules/TelemetrySend.jsm", this); + +const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled"; + +var gHttpServer = null; + +add_task(function* test_setup() { + do_get_profile(); + + // Make sure we don't generate unexpected pings due to pref changes. + yield setEmptyPrefWatchlist(); + + Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true); + Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true); + + // Start the webserver to check if the pending ping correctly arrives. + gHttpServer = new HttpServer(); + gHttpServer.start(-1); +}); + +add_task(function* testSendPendingOnIdleDaily() { + // Create a valid pending ping. + const PENDING_PING = { + id: "2133234d-4ea1-44f4-909e-ce8c6c41e0fc", + type: "test-ping", + version: 4, + application: {}, + payload: {}, + }; + yield TelemetryStorage.savePing(PENDING_PING, true); + + // Telemetry will not send this ping at startup, because it's not overdue. + yield TelemetryController.testSetup(); + TelemetrySend.setServer("http://localhost:" + gHttpServer.identity.primaryPort); + + let pendingPromise = new Promise(resolve => + gHttpServer.registerPrefixHandler("/submit/telemetry/", request => resolve(request))); + + let gatherPromise = PromiseUtils.defer(); + Services.obs.addObserver(gatherPromise.resolve, "gather-telemetry", false); + + // Check that we are correctly receiving the gather-telemetry notification. + TelemetrySession.observe(null, "idle-daily", null); + yield gatherPromise; + Assert.ok(true, "Received gather-telemetry notification."); + + Services.obs.removeObserver(gatherPromise.resolve, "gather-telemetry"); + + // Check that the pending ping is correctly received. + let ns = {}; + let module = Cu.import("resource://gre/modules/TelemetrySend.jsm", ns); + module.TelemetrySendImpl.observe(null, "idle-daily", null); + let request = yield pendingPromise; + let ping = decodeRequestPayload(request); + + // Validate the ping data. + Assert.equal(ping.id, PENDING_PING.id); + Assert.equal(ping.type, PENDING_PING.type); + + yield new Promise(resolve => gHttpServer.stop(resolve)); +}); diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js new file mode 100644 index 000000000..35181272a --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js @@ -0,0 +1,1528 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Cu.import("resource://gre/modules/AddonManager.jsm"); +Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", this); +Cu.import("resource://gre/modules/Preferences.jsm", this); +Cu.import("resource://gre/modules/PromiseUtils.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://testing-common/AddonManagerTesting.jsm"); +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://testing-common/MockRegistrar.jsm", this); +Cu.import("resource://gre/modules/FileUtils.jsm"); + +// AttributionCode is only needed for Firefox +XPCOMUtils.defineLazyModuleGetter(this, "AttributionCode", + "resource:///modules/AttributionCode.jsm"); + +// Lazy load |LightweightThemeManager|, we won't be using it on Gonk. +XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager", + "resource://gre/modules/LightweightThemeManager.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge", + "resource://gre/modules/ProfileAge.jsm"); + +// The webserver hosting the addons. +var gHttpServer = null; +// The URL of the webserver root. +var gHttpRoot = null; +// The URL of the data directory, on the webserver. +var gDataRoot = null; + +const PLATFORM_VERSION = "1.9.2"; +const APP_VERSION = "1"; +const APP_ID = "xpcshell@tests.mozilla.org"; +const APP_NAME = "XPCShell"; +const APP_HOTFIX_VERSION = "2.3.4a"; + +const DISTRIBUTION_ID = "distributor-id"; +const DISTRIBUTION_VERSION = "4.5.6b"; +const DISTRIBUTOR_NAME = "Some Distributor"; +const DISTRIBUTOR_CHANNEL = "A Channel"; +const PARTNER_NAME = "test"; +const PARTNER_ID = "NicePartner-ID-3785"; +const DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC = "distribution-customization-complete"; + +const GFX_VENDOR_ID = "0xabcd"; +const GFX_DEVICE_ID = "0x1234"; + +// The profile reset date, in milliseconds (Today) +const PROFILE_RESET_DATE_MS = Date.now(); +// The profile creation date, in milliseconds (Yesterday). +const PROFILE_CREATION_DATE_MS = PROFILE_RESET_DATE_MS - MILLISECONDS_PER_DAY; + +const FLASH_PLUGIN_NAME = "Shockwave Flash"; +const FLASH_PLUGIN_DESC = "A mock flash plugin"; +const FLASH_PLUGIN_VERSION = "\u201c1.1.1.1\u201d"; +const PLUGIN_MIME_TYPE1 = "application/x-shockwave-flash"; +const PLUGIN_MIME_TYPE2 = "text/plain"; + +const PLUGIN2_NAME = "Quicktime"; +const PLUGIN2_DESC = "A mock Quicktime plugin"; +const PLUGIN2_VERSION = "2.3"; + +const PERSONA_ID = "3785"; +// Defined by LightweightThemeManager, it is appended to the PERSONA_ID. +const PERSONA_ID_SUFFIX = "@personas.mozilla.org"; +const PERSONA_NAME = "Test Theme"; +const PERSONA_DESCRIPTION = "A nice theme/persona description."; + +const PLUGIN_UPDATED_TOPIC = "plugins-list-updated"; + +// system add-ons are enabled at startup, so record date when the test starts +const SYSTEM_ADDON_INSTALL_DATE = Date.now(); + +// Valid attribution code to write so that settings.attribution can be tested. +const ATTRIBUTION_CODE = "source%3Dgoogle.com"; + +/** + * Used to mock plugin tags in our fake plugin host. + */ +function PluginTag(aName, aDescription, aVersion, aEnabled) { + this.name = aName; + this.description = aDescription; + this.version = aVersion; + this.disabled = !aEnabled; +} + +PluginTag.prototype = { + name: null, + description: null, + version: null, + filename: null, + fullpath: null, + disabled: false, + blocklisted: false, + clicktoplay: true, + + mimeTypes: [ PLUGIN_MIME_TYPE1, PLUGIN_MIME_TYPE2 ], + + getMimeTypes: function(count) { + count.value = this.mimeTypes.length; + return this.mimeTypes; + } +}; + +// A container for the plugins handled by the fake plugin host. +var gInstalledPlugins = [ + new PluginTag("Java", "A mock Java plugin", "1.0", false /* Disabled */), + new PluginTag(FLASH_PLUGIN_NAME, FLASH_PLUGIN_DESC, FLASH_PLUGIN_VERSION, true), +]; + +// A fake plugin host for testing plugin telemetry environment. +var PluginHost = { + getPluginTags: function(countRef) { + countRef.value = gInstalledPlugins.length; + return gInstalledPlugins; + }, + + QueryInterface: function(iid) { + if (iid.equals(Ci.nsIPluginHost) + || iid.equals(Ci.nsISupports)) + return this; + + throw Components.results.NS_ERROR_NO_INTERFACE; + } +} + +function registerFakePluginHost() { + MockRegistrar.register("@mozilla.org/plugin/host;1", PluginHost); +} + +var SysInfo = { + overrides: {}, + + getProperty(name) { + // Assert.ok(false, "Mock SysInfo: " + name + ", " + JSON.stringify(this.overrides)); + if (name in this.overrides) { + return this.overrides[name]; + } + try { + return this._genuine.getProperty(name); + } catch (ex) { + throw ex; + } + }, + + get(name) { + return this._genuine.get(name); + }, + + QueryInterface(iid) { + if (iid.equals(Ci.nsIPropertyBag2) + || iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } +}; + +function registerFakeSysInfo() { + MockRegistrar.register("@mozilla.org/system-info;1", SysInfo); +} + +function MockAddonWrapper(aAddon) { + this.addon = aAddon; +} +MockAddonWrapper.prototype = { + get id() { + return this.addon.id; + }, + + get type() { + return "service"; + }, + + get appDisabled() { + return false; + }, + + get isCompatible() { + return true; + }, + + get isPlatformCompatible() { + return true; + }, + + get scope() { + return AddonManager.SCOPE_PROFILE; + }, + + get foreignInstall() { + return false; + }, + + get providesUpdatesSecurely() { + return true; + }, + + get blocklistState() { + return 0; // Not blocked. + }, + + get pendingOperations() { + return AddonManager.PENDING_NONE; + }, + + get permissions() { + return AddonManager.PERM_CAN_UNINSTALL | AddonManager.PERM_CAN_DISABLE; + }, + + get isActive() { + return true; + }, + + get name() { + return this.addon.name; + }, + + get version() { + return this.addon.version; + }, + + get creator() { + return new AddonManagerPrivate.AddonAuthor(this.addon.author); + }, + + get userDisabled() { + return this.appDisabled; + }, +}; + +function createMockAddonProvider(aName) { + let mockProvider = { + _addons: [], + + get name() { + return aName; + }, + + addAddon: function(aAddon) { + this._addons.push(aAddon); + AddonManagerPrivate.callAddonListeners("onInstalled", new MockAddonWrapper(aAddon)); + }, + + getAddonsByTypes: function (aTypes, aCallback) { + aCallback(this._addons.map(a => new MockAddonWrapper(a))); + }, + + shutdown() { + return Promise.resolve(); + }, + }; + + return mockProvider; +} + +/** + * Used to spoof the Persona Id. + */ +function spoofTheme(aId, aName, aDesc) { + return { + id: aId, + name: aName, + description: aDesc, + headerURL: "http://lwttest.invalid/a.png", + footerURL: "http://lwttest.invalid/b.png", + textcolor: Math.random().toString(), + accentcolor: Math.random().toString() + }; +} + +function spoofGfxAdapter() { + try { + let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug); + gfxInfo.spoofVendorID(GFX_VENDOR_ID); + gfxInfo.spoofDeviceID(GFX_DEVICE_ID); + } catch (x) { + // If we can't test gfxInfo, that's fine, we'll note it later. + } +} + +function spoofProfileReset() { + let profileAccessor = new ProfileAge(); + + return profileAccessor.writeTimes({ + created: PROFILE_CREATION_DATE_MS, + reset: PROFILE_RESET_DATE_MS + }); +} + +function spoofPartnerInfo() { + let prefsToSpoof = {}; + prefsToSpoof["distribution.id"] = DISTRIBUTION_ID; + prefsToSpoof["distribution.version"] = DISTRIBUTION_VERSION; + prefsToSpoof["app.distributor"] = DISTRIBUTOR_NAME; + prefsToSpoof["app.distributor.channel"] = DISTRIBUTOR_CHANNEL; + prefsToSpoof["app.partner.test"] = PARTNER_NAME; + prefsToSpoof["mozilla.partner.id"] = PARTNER_ID; + + // Spoof the preferences. + for (let pref in prefsToSpoof) { + Preferences.set(pref, prefsToSpoof[pref]); + } +} + +function getAttributionFile() { + let file = Services.dirsvc.get("LocalAppData", Ci.nsIFile); + file.append("mozilla"); + file.append(AppConstants.MOZ_APP_NAME); + file.append("postSigningData"); + return file; +} + +function spoofAttributionData() { + if (gIsWindows) { + AttributionCode._clearCache(); + let stream = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + stream.init(getAttributionFile(), -1, -1, 0); + stream.write(ATTRIBUTION_CODE, ATTRIBUTION_CODE.length); + } +} + +function cleanupAttributionData() { + if (gIsWindows) { + getAttributionFile().remove(false); + AttributionCode._clearCache(); + } +} + +/** + * Check that a value is a string and not empty. + * + * @param aValue The variable to check. + * @return True if |aValue| has type "string" and is not empty, False otherwise. + */ +function checkString(aValue) { + return (typeof aValue == "string") && (aValue != ""); +} + +/** + * If value is non-null, check if it's a valid string. + * + * @param aValue The variable to check. + * @return True if it's null or a valid string, false if it's non-null and an invalid + * string. + */ +function checkNullOrString(aValue) { + if (aValue) { + return checkString(aValue); + } else if (aValue === null) { + return true; + } + + return false; +} + +/** + * If value is non-null, check if it's a boolean. + * + * @param aValue The variable to check. + * @return True if it's null or a valid boolean, false if it's non-null and an invalid + * boolean. + */ +function checkNullOrBool(aValue) { + return aValue === null || (typeof aValue == "boolean"); +} + +function checkBuildSection(data) { + const expectedInfo = { + applicationId: APP_ID, + applicationName: APP_NAME, + buildId: gAppInfo.appBuildID, + version: APP_VERSION, + vendor: "Mozilla", + platformVersion: PLATFORM_VERSION, + xpcomAbi: "noarch-spidermonkey", + }; + + Assert.ok("build" in data, "There must be a build section in Environment."); + + for (let f in expectedInfo) { + Assert.ok(checkString(data.build[f]), f + " must be a valid string."); + Assert.equal(data.build[f], expectedInfo[f], f + " must have the correct value."); + } + + // Make sure architecture and hotfixVersion are in the environment. + Assert.ok(checkString(data.build.architecture)); + Assert.ok(checkString(data.build.hotfixVersion)); + Assert.equal(data.build.hotfixVersion, APP_HOTFIX_VERSION); + + if (gIsMac) { + let macUtils = Cc["@mozilla.org/xpcom/mac-utils;1"].getService(Ci.nsIMacUtils); + if (macUtils && macUtils.isUniversalBinary) { + Assert.ok(checkString(data.build.architecturesInBinary)); + } + } +} + +function checkSettingsSection(data) { + const EXPECTED_FIELDS_TYPES = { + blocklistEnabled: "boolean", + e10sEnabled: "boolean", + e10sCohort: "string", + telemetryEnabled: "boolean", + locale: "string", + update: "object", + userPrefs: "object", + }; + + Assert.ok("settings" in data, "There must be a settings section in Environment."); + + for (let f in EXPECTED_FIELDS_TYPES) { + Assert.equal(typeof data.settings[f], EXPECTED_FIELDS_TYPES[f], + f + " must have the correct type."); + } + + // Check "addonCompatibilityCheckEnabled" separately, as it is not available + // on Gonk. + if (gIsGonk) { + Assert.ok(!("addonCompatibilityCheckEnabled" in data.settings), "Must not be available on Gonk."); + } else { + Assert.equal(data.settings.addonCompatibilityCheckEnabled, AddonManager.checkCompatibility); + } + + // Check "isDefaultBrowser" separately, as it is not available on Android an can either be + // null or boolean on other platforms. + if (gIsAndroid) { + Assert.ok(!("isDefaultBrowser" in data.settings), "Must not be available on Android."); + } else { + Assert.ok(checkNullOrBool(data.settings.isDefaultBrowser)); + } + + // Check "channel" separately, as it can either be null or string. + let update = data.settings.update; + Assert.ok(checkNullOrString(update.channel)); + Assert.equal(typeof update.enabled, "boolean"); + Assert.equal(typeof update.autoDownload, "boolean"); + + // Check "defaultSearchEngine" separately, as it can either be undefined or string. + if ("defaultSearchEngine" in data.settings) { + checkString(data.settings.defaultSearchEngine); + Assert.equal(typeof data.settings.defaultSearchEngineData, "object"); + } + + if ("attribution" in data.settings) { + Assert.equal(typeof data.settings.attribution, "object"); + Assert.equal(data.settings.attribution.source, "google.com"); + } +} + +function checkProfileSection(data) { + Assert.ok("profile" in data, "There must be a profile section in Environment."); + Assert.equal(data.profile.creationDate, truncateToDays(PROFILE_CREATION_DATE_MS)); + Assert.equal(data.profile.resetDate, truncateToDays(PROFILE_RESET_DATE_MS)); +} + +function checkPartnerSection(data, isInitial) { + const EXPECTED_FIELDS = { + distributionId: DISTRIBUTION_ID, + distributionVersion: DISTRIBUTION_VERSION, + partnerId: PARTNER_ID, + distributor: DISTRIBUTOR_NAME, + distributorChannel: DISTRIBUTOR_CHANNEL, + }; + + Assert.ok("partner" in data, "There must be a partner section in Environment."); + + for (let f in EXPECTED_FIELDS) { + let expected = isInitial ? null : EXPECTED_FIELDS[f]; + Assert.strictEqual(data.partner[f], expected, f + " must have the correct value."); + } + + // Check that "partnerNames" exists and contains the correct element. + Assert.ok(Array.isArray(data.partner.partnerNames)); + if (isInitial) { + Assert.equal(data.partner.partnerNames.length, 0); + } else { + Assert.ok(data.partner.partnerNames.includes(PARTNER_NAME)); + } +} + +function checkGfxAdapter(data) { + const EXPECTED_ADAPTER_FIELDS_TYPES = { + description: "string", + vendorID: "string", + deviceID: "string", + subsysID: "string", + RAM: "number", + driver: "string", + driverVersion: "string", + driverDate: "string", + GPUActive: "boolean", + }; + + for (let f in EXPECTED_ADAPTER_FIELDS_TYPES) { + Assert.ok(f in data, f + " must be available."); + + if (data[f]) { + // Since we have a non-null value, check if it has the correct type. + Assert.equal(typeof data[f], EXPECTED_ADAPTER_FIELDS_TYPES[f], + f + " must have the correct type."); + } + } +} + +function checkSystemSection(data) { + const EXPECTED_FIELDS = [ "memoryMB", "cpu", "os", "hdd", "gfx" ]; + const EXPECTED_HDD_FIELDS = [ "profile", "binary", "system" ]; + + Assert.ok("system" in data, "There must be a system section in Environment."); + + // Make sure we have all the top level sections and fields. + for (let f of EXPECTED_FIELDS) { + Assert.ok(f in data.system, f + " must be available."); + } + + Assert.ok(Number.isFinite(data.system.memoryMB), "MemoryMB must be a number."); + + if (gIsWindows || gIsMac || gIsLinux) { + let EXTRA_CPU_FIELDS = ["cores", "model", "family", "stepping", + "l2cacheKB", "l3cacheKB", "speedMHz", "vendor"]; + + for (let f of EXTRA_CPU_FIELDS) { + // Note this is testing TelemetryEnvironment.js only, not that the + // values are valid - null is the fallback. + Assert.ok(f in data.system.cpu, f + " must be available under cpu."); + } + + if (gIsWindows) { + Assert.equal(typeof data.system.isWow64, "boolean", + "isWow64 must be available on Windows and have the correct type."); + Assert.ok("virtualMaxMB" in data.system, "virtualMaxMB must be available."); + Assert.ok(Number.isFinite(data.system.virtualMaxMB), + "virtualMaxMB must be a number."); + } + + // We insist these are available + for (let f of ["cores"]) { + Assert.ok(!(f in data.system.cpu) || + Number.isFinite(data.system.cpu[f]), + f + " must be a number if non null."); + } + + // These should be numbers if they are not null + for (let f of ["model", "family", "stepping", "l2cacheKB", + "l3cacheKB", "speedMHz"]) { + Assert.ok(!(f in data.system.cpu) || + data.system.cpu[f] === null || + Number.isFinite(data.system.cpu[f]), + f + " must be a number if non null."); + } + } + + let cpuData = data.system.cpu; + Assert.ok(Number.isFinite(cpuData.count), "CPU count must be a number."); + Assert.ok(Array.isArray(cpuData.extensions), "CPU extensions must be available."); + + // Device data is only available on Android or Gonk. + if (gIsAndroid || gIsGonk) { + let deviceData = data.system.device; + Assert.ok(checkNullOrString(deviceData.model)); + Assert.ok(checkNullOrString(deviceData.manufacturer)); + Assert.ok(checkNullOrString(deviceData.hardware)); + Assert.ok(checkNullOrBool(deviceData.isTablet)); + } + + let osData = data.system.os; + Assert.ok(checkNullOrString(osData.name)); + Assert.ok(checkNullOrString(osData.version)); + Assert.ok(checkNullOrString(osData.locale)); + + // Service pack is only available on Windows. + if (gIsWindows) { + Assert.ok(Number.isFinite(osData["servicePackMajor"]), + "ServicePackMajor must be a number."); + Assert.ok(Number.isFinite(osData["servicePackMinor"]), + "ServicePackMinor must be a number."); + if ("windowsBuildNumber" in osData) { + // This might not be available on all Windows platforms. + Assert.ok(Number.isFinite(osData["windowsBuildNumber"]), + "windowsBuildNumber must be a number."); + } + if ("windowsUBR" in osData) { + // This might not be available on all Windows platforms. + Assert.ok((osData["windowsUBR"] === null) || Number.isFinite(osData["windowsUBR"]), + "windowsUBR must be null or a number."); + } + } else if (gIsAndroid || gIsGonk) { + Assert.ok(checkNullOrString(osData.kernelVersion)); + } + + let check = gIsWindows ? checkString : checkNullOrString; + for (let disk of EXPECTED_HDD_FIELDS) { + Assert.ok(check(data.system.hdd[disk].model)); + Assert.ok(check(data.system.hdd[disk].revision)); + } + + let gfxData = data.system.gfx; + Assert.ok("D2DEnabled" in gfxData); + Assert.ok("DWriteEnabled" in gfxData); + // DWriteVersion is disabled due to main thread jank and will be enabled + // again as part of bug 1154500. + // Assert.ok("DWriteVersion" in gfxData); + if (gIsWindows) { + Assert.equal(typeof gfxData.D2DEnabled, "boolean"); + Assert.equal(typeof gfxData.DWriteEnabled, "boolean"); + // As above, will be enabled again as part of bug 1154500. + // Assert.ok(checkString(gfxData.DWriteVersion)); + } + + Assert.ok("adapters" in gfxData); + Assert.ok(gfxData.adapters.length > 0, "There must be at least one GFX adapter."); + for (let adapter of gfxData.adapters) { + checkGfxAdapter(adapter); + } + Assert.equal(typeof gfxData.adapters[0].GPUActive, "boolean"); + Assert.ok(gfxData.adapters[0].GPUActive, "The first GFX adapter must be active."); + + Assert.ok(Array.isArray(gfxData.monitors)); + if (gIsWindows || gIsMac) { + Assert.ok(gfxData.monitors.length >= 1, "There is at least one monitor."); + Assert.equal(typeof gfxData.monitors[0].screenWidth, "number"); + Assert.equal(typeof gfxData.monitors[0].screenHeight, "number"); + if (gIsWindows) { + Assert.equal(typeof gfxData.monitors[0].refreshRate, "number"); + Assert.equal(typeof gfxData.monitors[0].pseudoDisplay, "boolean"); + } + if (gIsMac) { + Assert.equal(typeof gfxData.monitors[0].scale, "number"); + } + } + + Assert.equal(typeof gfxData.features, "object"); + Assert.equal(typeof gfxData.features.compositor, "string"); + + try { + // If we've not got nsIGfxInfoDebug, then this will throw and stop us doing + // this test. + let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug); + + if (gIsWindows || gIsMac) { + Assert.equal(GFX_VENDOR_ID, gfxData.adapters[0].vendorID); + Assert.equal(GFX_DEVICE_ID, gfxData.adapters[0].deviceID); + } + + let features = gfxInfo.getFeatures(); + Assert.equal(features.compositor, gfxData.features.compositor); + Assert.equal(features.opengl, gfxData.features.opengl); + Assert.equal(features.webgl, gfxData.features.webgl); + } + catch (e) {} +} + +function checkActiveAddon(data) { + let signedState = mozinfo.addon_signing ? "number" : "undefined"; + // system add-ons have an undefined signState + if (data.isSystem) + signedState = "undefined"; + + const EXPECTED_ADDON_FIELDS_TYPES = { + blocklisted: "boolean", + name: "string", + userDisabled: "boolean", + appDisabled: "boolean", + version: "string", + scope: "number", + type: "string", + foreignInstall: "boolean", + hasBinaryComponents: "boolean", + installDay: "number", + updateDay: "number", + signedState: signedState, + isSystem: "boolean", + }; + + for (let f in EXPECTED_ADDON_FIELDS_TYPES) { + Assert.ok(f in data, f + " must be available."); + Assert.equal(typeof data[f], EXPECTED_ADDON_FIELDS_TYPES[f], + f + " must have the correct type."); + } + + // We check "description" separately, as it can be null. + Assert.ok(checkNullOrString(data.description)); +} + +function checkPlugin(data) { + const EXPECTED_PLUGIN_FIELDS_TYPES = { + name: "string", + version: "string", + description: "string", + blocklisted: "boolean", + disabled: "boolean", + clicktoplay: "boolean", + updateDay: "number", + }; + + for (let f in EXPECTED_PLUGIN_FIELDS_TYPES) { + Assert.ok(f in data, f + " must be available."); + Assert.equal(typeof data[f], EXPECTED_PLUGIN_FIELDS_TYPES[f], + f + " must have the correct type."); + } + + Assert.ok(Array.isArray(data.mimeTypes)); + for (let type of data.mimeTypes) { + Assert.ok(checkString(type)); + } +} + +function checkTheme(data) { + // "hasBinaryComponents" is not available when testing. + const EXPECTED_THEME_FIELDS_TYPES = { + id: "string", + blocklisted: "boolean", + name: "string", + userDisabled: "boolean", + appDisabled: "boolean", + version: "string", + scope: "number", + foreignInstall: "boolean", + installDay: "number", + updateDay: "number", + }; + + for (let f in EXPECTED_THEME_FIELDS_TYPES) { + Assert.ok(f in data, f + " must be available."); + Assert.equal(typeof data[f], EXPECTED_THEME_FIELDS_TYPES[f], + f + " must have the correct type."); + } + + // We check "description" separately, as it can be null. + Assert.ok(checkNullOrString(data.description)); +} + +function checkActiveGMPlugin(data) { + // GMP plugin version defaults to null until GMPDownloader runs to update it. + if (data.version) { + Assert.equal(typeof data.version, "string"); + } + Assert.equal(typeof data.userDisabled, "boolean"); + Assert.equal(typeof data.applyBackgroundUpdates, "number"); +} + +function checkAddonsSection(data, expectBrokenAddons) { + const EXPECTED_FIELDS = [ + "activeAddons", "theme", "activePlugins", "activeGMPlugins", "activeExperiment", + "persona", + ]; + + Assert.ok("addons" in data, "There must be an addons section in Environment."); + for (let f of EXPECTED_FIELDS) { + Assert.ok(f in data.addons, f + " must be available."); + } + + // Check the active addons, if available. + if (!expectBrokenAddons) { + let activeAddons = data.addons.activeAddons; + for (let addon in activeAddons) { + checkActiveAddon(activeAddons[addon]); + } + } + + // Check "theme" structure. + if (Object.keys(data.addons.theme).length !== 0) { + checkTheme(data.addons.theme); + } + + // Check the active plugins. + Assert.ok(Array.isArray(data.addons.activePlugins)); + for (let plugin of data.addons.activePlugins) { + checkPlugin(plugin); + } + + // Check active GMPlugins + let activeGMPlugins = data.addons.activeGMPlugins; + for (let gmPlugin in activeGMPlugins) { + checkActiveGMPlugin(activeGMPlugins[gmPlugin]); + } + + // Check the active Experiment + let experiment = data.addons.activeExperiment; + if (Object.keys(experiment).length !== 0) { + Assert.ok(checkString(experiment.id)); + Assert.ok(checkString(experiment.branch)); + } + + // Check persona + Assert.ok(checkNullOrString(data.addons.persona)); +} + +function checkEnvironmentData(data, isInitial = false, expectBrokenAddons = false) { + checkBuildSection(data); + checkSettingsSection(data); + checkProfileSection(data); + checkPartnerSection(data, isInitial); + checkSystemSection(data); + checkAddonsSection(data, expectBrokenAddons); +} + +add_task(function* setup() { + // Load a custom manifest to provide search engine loading from JAR files. + do_load_manifest("chrome.manifest"); + registerFakeSysInfo(); + spoofGfxAdapter(); + do_get_profile(); + + // The system add-on must be installed before AddonManager is started. + const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "app0"], true); + do_get_file("system.xpi").copyTo(distroDir, "tel-system-xpi@tests.mozilla.org.xpi"); + let system_addon = FileUtils.File(distroDir.path); + system_addon.append("tel-system-xpi@tests.mozilla.org.xpi"); + system_addon.lastModifiedTime = SYSTEM_ADDON_INSTALL_DATE; + loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION); + + // Spoof the persona ID, but not on Gonk. + if (!gIsGonk) { + LightweightThemeManager.currentTheme = + spoofTheme(PERSONA_ID, PERSONA_NAME, PERSONA_DESCRIPTION); + } + // Register a fake plugin host for consistent flash version data. + registerFakePluginHost(); + + // Setup a webserver to serve Addons, Plugins, etc. + gHttpServer = new HttpServer(); + gHttpServer.start(-1); + let port = gHttpServer.identity.primaryPort; + gHttpRoot = "http://localhost:" + port + "/"; + gDataRoot = gHttpRoot + "data/"; + gHttpServer.registerDirectory("/data/", do_get_cwd()); + do_register_cleanup(() => gHttpServer.stop(() => {})); + + // Spoof the the hotfixVersion + Preferences.set("extensions.hotfix.lastVersion", APP_HOTFIX_VERSION); + + // Create the attribution data file, so that settings.attribution will exist. + // The attribution functionality only exists in Firefox. + if (AppConstants.MOZ_BUILD_APP == "browser") { + spoofAttributionData(); + do_register_cleanup(cleanupAttributionData); + } + + yield spoofProfileReset(); + TelemetryEnvironment.delayedInit(); +}); + +add_task(function* test_checkEnvironment() { + let environmentData = yield TelemetryEnvironment.onInitialized(); + checkEnvironmentData(environmentData, true); + + spoofPartnerInfo(); + Services.obs.notifyObservers(null, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC, null); + + environmentData = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(environmentData); +}); + +add_task(function* test_prefWatchPolicies() { + const PREF_TEST_1 = "toolkit.telemetry.test.pref_new"; + const PREF_TEST_2 = "toolkit.telemetry.test.pref1"; + const PREF_TEST_3 = "toolkit.telemetry.test.pref2"; + const PREF_TEST_4 = "toolkit.telemetry.test.pref_old"; + const PREF_TEST_5 = "toolkit.telemetry.test.requiresRestart"; + + const expectedValue = "some-test-value"; + const unexpectedValue = "unexpected-test-value"; + + const PREFS_TO_WATCH = new Map([ + [PREF_TEST_1, {what: TelemetryEnvironment.RECORD_PREF_VALUE}], + [PREF_TEST_2, {what: TelemetryEnvironment.RECORD_PREF_STATE}], + [PREF_TEST_3, {what: TelemetryEnvironment.RECORD_PREF_STATE}], + [PREF_TEST_4, {what: TelemetryEnvironment.RECORD_PREF_VALUE}], + [PREF_TEST_5, {what: TelemetryEnvironment.RECORD_PREF_VALUE, requiresRestart: true}], + ]); + + Preferences.set(PREF_TEST_4, expectedValue); + Preferences.set(PREF_TEST_5, expectedValue); + + // Set the Environment preferences to watch. + TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); + let deferred = PromiseUtils.defer(); + + // Check that the pref values are missing or present as expected + Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_1], undefined); + Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_4], expectedValue); + Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_5], expectedValue); + + TelemetryEnvironment.registerChangeListener("testWatchPrefs", + (reason, data) => deferred.resolve(data)); + let oldEnvironmentData = TelemetryEnvironment.currentEnvironment; + + // Trigger a change in the watched preferences. + Preferences.set(PREF_TEST_1, expectedValue); + Preferences.set(PREF_TEST_2, false); + Preferences.set(PREF_TEST_5, unexpectedValue); + let eventEnvironmentData = yield deferred.promise; + + // Unregister the listener. + TelemetryEnvironment.unregisterChangeListener("testWatchPrefs"); + + // Check environment contains the correct data. + Assert.deepEqual(oldEnvironmentData, eventEnvironmentData); + let userPrefs = TelemetryEnvironment.currentEnvironment.settings.userPrefs; + + Assert.equal(userPrefs[PREF_TEST_1], expectedValue, + "Environment contains the correct preference value."); + Assert.equal(userPrefs[PREF_TEST_2], "<user-set>", + "Report that the pref was user set but the value is not shown."); + Assert.ok(!(PREF_TEST_3 in userPrefs), + "Do not report if preference not user set."); + Assert.equal(userPrefs[PREF_TEST_5], expectedValue, + "The pref value in the environment data should still be the same"); +}); + +add_task(function* test_prefWatch_prefReset() { + const PREF_TEST = "toolkit.telemetry.test.pref1"; + const PREFS_TO_WATCH = new Map([ + [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_STATE}], + ]); + + // Set the preference to a non-default value. + Preferences.set(PREF_TEST, false); + + // Set the Environment preferences to watch. + TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); + let deferred = PromiseUtils.defer(); + TelemetryEnvironment.registerChangeListener("testWatchPrefs_reset", deferred.resolve); + + Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST], "<user-set>"); + + // Trigger a change in the watched preferences. + Preferences.reset(PREF_TEST); + yield deferred.promise; + + Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST], undefined); + + // Unregister the listener. + TelemetryEnvironment.unregisterChangeListener("testWatchPrefs_reset"); +}); + +add_task(function* test_addonsWatch_InterestingChange() { + const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi"; + const ADDON_ID = "tel-restartless-xpi@tests.mozilla.org"; + // We only expect a single notification for each install, uninstall, enable, disable. + const EXPECTED_NOTIFICATIONS = 4; + + let receivedNotifications = 0; + + let registerCheckpointPromise = (aExpected) => { + return new Promise(resolve => TelemetryEnvironment.registerChangeListener( + "testWatchAddons_Changes" + aExpected, (reason, data) => { + Assert.equal(reason, "addons-changed"); + receivedNotifications++; + resolve(); + })); + }; + + let assertCheckpoint = (aExpected) => { + Assert.equal(receivedNotifications, aExpected); + TelemetryEnvironment.unregisterChangeListener("testWatchAddons_Changes" + aExpected); + }; + + // Test for receiving one notification after each change. + let checkpointPromise = registerCheckpointPromise(1); + yield AddonManagerTesting.installXPIFromURL(ADDON_INSTALL_URL); + yield checkpointPromise; + assertCheckpoint(1); + Assert.ok(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons); + + checkpointPromise = registerCheckpointPromise(2); + let addon = yield AddonManagerTesting.getAddonById(ADDON_ID); + addon.userDisabled = true; + yield checkpointPromise; + assertCheckpoint(2); + Assert.ok(!(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons)); + + checkpointPromise = registerCheckpointPromise(3); + addon.userDisabled = false; + yield checkpointPromise; + assertCheckpoint(3); + Assert.ok(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons); + + checkpointPromise = registerCheckpointPromise(4); + yield AddonManagerTesting.uninstallAddonByID(ADDON_ID); + yield checkpointPromise; + assertCheckpoint(4); + Assert.ok(!(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons)); + + Assert.equal(receivedNotifications, EXPECTED_NOTIFICATIONS, + "We must only receive the notifications we expect."); +}); + +add_task(function* test_pluginsWatch_Add() { + if (gIsAndroid) { + Assert.ok(true, "Skipping: there is no Plugin Manager on Android."); + return; + } + + Assert.equal(TelemetryEnvironment.currentEnvironment.addons.activePlugins.length, 1); + + let newPlugin = new PluginTag(PLUGIN2_NAME, PLUGIN2_DESC, PLUGIN2_VERSION, true); + gInstalledPlugins.push(newPlugin); + + let deferred = PromiseUtils.defer(); + let receivedNotifications = 0; + let callback = (reason, data) => { + receivedNotifications++; + Assert.equal(reason, "addons-changed"); + deferred.resolve(); + }; + TelemetryEnvironment.registerChangeListener("testWatchPlugins_Add", callback); + + Services.obs.notifyObservers(null, PLUGIN_UPDATED_TOPIC, null); + yield deferred.promise; + + Assert.equal(TelemetryEnvironment.currentEnvironment.addons.activePlugins.length, 2); + + TelemetryEnvironment.unregisterChangeListener("testWatchPlugins_Add"); + + Assert.equal(receivedNotifications, 1, "We must only receive one notification."); +}); + +add_task(function* test_pluginsWatch_Remove() { + if (gIsAndroid) { + Assert.ok(true, "Skipping: there is no Plugin Manager on Android."); + return; + } + + // Find the test plugin. + let plugin = gInstalledPlugins.find(p => (p.name == PLUGIN2_NAME)); + Assert.ok(plugin, "The test plugin must exist."); + + // Remove it from the PluginHost. + gInstalledPlugins = gInstalledPlugins.filter(p => p != plugin); + + let deferred = PromiseUtils.defer(); + let receivedNotifications = 0; + let callback = () => { + receivedNotifications++; + deferred.resolve(); + }; + TelemetryEnvironment.registerChangeListener("testWatchPlugins_Remove", callback); + + Services.obs.notifyObservers(null, PLUGIN_UPDATED_TOPIC, null); + yield deferred.promise; + + TelemetryEnvironment.unregisterChangeListener("testWatchPlugins_Remove"); + + Assert.equal(receivedNotifications, 1, "We must only receive one notification."); +}); + +add_task(function* test_addonsWatch_NotInterestingChange() { + // We are not interested to dictionary addons changes. + const DICTIONARY_ADDON_INSTALL_URL = gDataRoot + "dictionary.xpi"; + const INTERESTING_ADDON_INSTALL_URL = gDataRoot + "restartless.xpi"; + + let receivedNotification = false; + let deferred = PromiseUtils.defer(); + TelemetryEnvironment.registerChangeListener("testNotInteresting", + () => { + Assert.ok(!receivedNotification, "Should not receive multiple notifications"); + receivedNotification = true; + deferred.resolve(); + }); + + yield AddonManagerTesting.installXPIFromURL(DICTIONARY_ADDON_INSTALL_URL); + yield AddonManagerTesting.installXPIFromURL(INTERESTING_ADDON_INSTALL_URL); + + yield deferred.promise; + Assert.ok(!("telemetry-dictionary@tests.mozilla.org" in + TelemetryEnvironment.currentEnvironment.addons.activeAddons), + "Dictionaries should not appear in active addons."); + + TelemetryEnvironment.unregisterChangeListener("testNotInteresting"); +}); + +add_task(function* test_addonsAndPlugins() { + const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi"; + const ADDON_ID = "tel-restartless-xpi@tests.mozilla.org"; + const ADDON_INSTALL_DATE = truncateToDays(Date.now()); + const EXPECTED_ADDON_DATA = { + blocklisted: false, + description: "A restartless addon which gets enabled without a reboot.", + name: "XPI Telemetry Restartless Test", + userDisabled: false, + appDisabled: false, + version: "1.0", + scope: 1, + type: "extension", + foreignInstall: false, + hasBinaryComponents: false, + installDay: ADDON_INSTALL_DATE, + updateDay: ADDON_INSTALL_DATE, + signedState: mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED, + isSystem: false, + }; + const SYSTEM_ADDON_ID = "tel-system-xpi@tests.mozilla.org"; + const EXPECTED_SYSTEM_ADDON_DATA = { + blocklisted: false, + description: "A system addon which is shipped with Firefox.", + name: "XPI Telemetry System Add-on Test", + userDisabled: false, + appDisabled: false, + version: "1.0", + scope: 1, + type: "extension", + foreignInstall: false, + hasBinaryComponents: false, + installDay: truncateToDays(SYSTEM_ADDON_INSTALL_DATE), + updateDay: truncateToDays(SYSTEM_ADDON_INSTALL_DATE), + signedState: undefined, + isSystem: true, + }; + + const EXPECTED_PLUGIN_DATA = { + name: FLASH_PLUGIN_NAME, + version: FLASH_PLUGIN_VERSION, + description: FLASH_PLUGIN_DESC, + blocklisted: false, + disabled: false, + clicktoplay: true, + }; + + // Install an addon so we have some data. + yield AddonManagerTesting.installXPIFromURL(ADDON_INSTALL_URL); + + let data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + + // Check addon data. + Assert.ok(ADDON_ID in data.addons.activeAddons, "We must have one active addon."); + let targetAddon = data.addons.activeAddons[ADDON_ID]; + for (let f in EXPECTED_ADDON_DATA) { + Assert.equal(targetAddon[f], EXPECTED_ADDON_DATA[f], f + " must have the correct value."); + } + + // Check system add-on data. + Assert.ok(SYSTEM_ADDON_ID in data.addons.activeAddons, "We must have one active system addon."); + let targetSystemAddon = data.addons.activeAddons[SYSTEM_ADDON_ID]; + for (let f in EXPECTED_SYSTEM_ADDON_DATA) { + Assert.equal(targetSystemAddon[f], EXPECTED_SYSTEM_ADDON_DATA[f], f + " must have the correct value."); + } + + // Check theme data. + let theme = data.addons.theme; + Assert.equal(theme.id, (PERSONA_ID + PERSONA_ID_SUFFIX)); + Assert.equal(theme.name, PERSONA_NAME); + Assert.equal(theme.description, PERSONA_DESCRIPTION); + + // Check plugin data. + Assert.equal(data.addons.activePlugins.length, 1, "We must have only one active plugin."); + let targetPlugin = data.addons.activePlugins[0]; + for (let f in EXPECTED_PLUGIN_DATA) { + Assert.equal(targetPlugin[f], EXPECTED_PLUGIN_DATA[f], f + " must have the correct value."); + } + + // Check plugin mime types. + Assert.ok(targetPlugin.mimeTypes.find(m => m == PLUGIN_MIME_TYPE1)); + Assert.ok(targetPlugin.mimeTypes.find(m => m == PLUGIN_MIME_TYPE2)); + Assert.ok(!targetPlugin.mimeTypes.find(m => m == "Not There.")); + + let personaId = (gIsGonk) ? null : PERSONA_ID; + Assert.equal(data.addons.persona, personaId, "The correct Persona Id must be reported."); + + // Uninstall the addon. + yield AddonManagerTesting.uninstallAddonByID(ADDON_ID); +}); + +add_task(function* test_signedAddon() { + const ADDON_INSTALL_URL = gDataRoot + "signed.xpi"; + const ADDON_ID = "tel-signed-xpi@tests.mozilla.org"; + const ADDON_INSTALL_DATE = truncateToDays(Date.now()); + const EXPECTED_ADDON_DATA = { + blocklisted: false, + description: "A signed addon which gets enabled without a reboot.", + name: "XPI Telemetry Signed Test", + userDisabled: false, + appDisabled: false, + version: "1.0", + scope: 1, + type: "extension", + foreignInstall: false, + hasBinaryComponents: false, + installDay: ADDON_INSTALL_DATE, + updateDay: ADDON_INSTALL_DATE, + signedState: mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED, + }; + + let deferred = PromiseUtils.defer(); + TelemetryEnvironment.registerChangeListener("test_signedAddon", deferred.resolve); + + // Install the addon. + yield AddonManagerTesting.installXPIFromURL(ADDON_INSTALL_URL); + + yield deferred.promise; + // Unregister the listener. + TelemetryEnvironment.unregisterChangeListener("test_signedAddon"); + + let data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + + // Check addon data. + Assert.ok(ADDON_ID in data.addons.activeAddons, "Add-on should be in the environment."); + let targetAddon = data.addons.activeAddons[ADDON_ID]; + for (let f in EXPECTED_ADDON_DATA) { + Assert.equal(targetAddon[f], EXPECTED_ADDON_DATA[f], f + " must have the correct value."); + } +}); + +add_task(function* test_addonsFieldsLimit() { + const ADDON_INSTALL_URL = gDataRoot + "long-fields.xpi"; + const ADDON_ID = "tel-longfields-xpi@tests.mozilla.org"; + + // Install the addon and wait for the TelemetryEnvironment to pick it up. + let deferred = PromiseUtils.defer(); + TelemetryEnvironment.registerChangeListener("test_longFieldsAddon", deferred.resolve); + yield AddonManagerTesting.installXPIFromURL(ADDON_INSTALL_URL); + yield deferred.promise; + TelemetryEnvironment.unregisterChangeListener("test_longFieldsAddon"); + + let data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + + // Check that the addon is available and that the string fields are limited. + Assert.ok(ADDON_ID in data.addons.activeAddons, "Add-on should be in the environment."); + let targetAddon = data.addons.activeAddons[ADDON_ID]; + + // TelemetryEnvironment limits the length of string fields for activeAddons to 100 chars, + // to mitigate misbehaving addons. + Assert.lessOrEqual(targetAddon.version.length, 100, + "The version string must have been limited"); + Assert.lessOrEqual(targetAddon.name.length, 100, + "The name string must have been limited"); + Assert.lessOrEqual(targetAddon.description.length, 100, + "The description string must have been limited"); +}); + +add_task(function* test_collectionWithbrokenAddonData() { + const BROKEN_ADDON_ID = "telemetry-test2.example.com@services.mozilla.org"; + const BROKEN_MANIFEST = { + id: "telemetry-test2.example.com@services.mozilla.org", + name: "telemetry broken addon", + origin: "https://telemetry-test2.example.com", + version: 1, // This is intentionally not a string. + signedState: AddonManager.SIGNEDSTATE_SIGNED, + }; + + const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi"; + const ADDON_ID = "tel-restartless-xpi@tests.mozilla.org"; + const ADDON_INSTALL_DATE = truncateToDays(Date.now()); + const EXPECTED_ADDON_DATA = { + blocklisted: false, + description: "A restartless addon which gets enabled without a reboot.", + name: "XPI Telemetry Restartless Test", + userDisabled: false, + appDisabled: false, + version: "1.0", + scope: 1, + type: "extension", + foreignInstall: false, + hasBinaryComponents: false, + installDay: ADDON_INSTALL_DATE, + updateDay: ADDON_INSTALL_DATE, + signedState: mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_MISSING : + AddonManager.SIGNEDSTATE_NOT_REQUIRED, + }; + + let receivedNotifications = 0; + + let registerCheckpointPromise = (aExpected) => { + return new Promise(resolve => TelemetryEnvironment.registerChangeListener( + "testBrokenAddon_collection" + aExpected, (reason, data) => { + Assert.equal(reason, "addons-changed"); + receivedNotifications++; + resolve(); + })); + }; + + let assertCheckpoint = (aExpected) => { + Assert.equal(receivedNotifications, aExpected); + TelemetryEnvironment.unregisterChangeListener("testBrokenAddon_collection" + aExpected); + }; + + // Register the broken provider and install the broken addon. + let checkpointPromise = registerCheckpointPromise(1); + let brokenAddonProvider = createMockAddonProvider("Broken Extensions Provider"); + AddonManagerPrivate.registerProvider(brokenAddonProvider); + brokenAddonProvider.addAddon(BROKEN_MANIFEST); + yield checkpointPromise; + assertCheckpoint(1); + + // Now install an addon which returns the correct information. + checkpointPromise = registerCheckpointPromise(2); + yield AddonManagerTesting.installXPIFromURL(ADDON_INSTALL_URL); + yield checkpointPromise; + assertCheckpoint(2); + + // Check that the new environment contains the Social addon installed with the broken + // manifest and the rest of the data. + let data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data, false, true /* expect broken addons*/); + + let activeAddons = data.addons.activeAddons; + Assert.ok(BROKEN_ADDON_ID in activeAddons, + "The addon with the broken manifest must be reported."); + Assert.equal(activeAddons[BROKEN_ADDON_ID].version, null, + "null should be reported for invalid data."); + Assert.ok(ADDON_ID in activeAddons, + "The valid addon must be reported."); + Assert.equal(activeAddons[ADDON_ID].description, EXPECTED_ADDON_DATA.description, + "The description for the valid addon should be correct."); + + // Unregister the broken provider so we don't mess with other tests. + AddonManagerPrivate.unregisterProvider(brokenAddonProvider); + + // Uninstall the valid addon. + yield AddonManagerTesting.uninstallAddonByID(ADDON_ID); +}); + +add_task(function* test_defaultSearchEngine() { + // Check that no default engine is in the environment before the search service is + // initialized. + let data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + Assert.ok(!("defaultSearchEngine" in data.settings)); + Assert.ok(!("defaultSearchEngineData" in data.settings)); + + // Load the engines definitions from a custom JAR file: that's needed so that + // the search provider reports an engine identifier. + let url = "chrome://testsearchplugin/locale/searchplugins/"; + let resProt = Services.io.getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + resProt.setSubstitution("search-plugins", + Services.io.newURI(url, null, null)); + + // Initialize the search service. + yield new Promise(resolve => Services.search.init(resolve)); + + // Our default engine from the JAR file has an identifier. Check if it is correctly + // reported. + data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + Assert.equal(data.settings.defaultSearchEngine, "telemetrySearchIdentifier"); + let expectedSearchEngineData = { + name: "telemetrySearchIdentifier", + loadPath: "jar:[other]/searchTest.jar!testsearchplugin/telemetrySearchIdentifier.xml", + origin: "default", + submissionURL: "http://ar.wikipedia.org/wiki/%D8%AE%D8%A7%D8%B5:%D8%A8%D8%AD%D8%AB?search=&sourceid=Mozilla-search" + }; + Assert.deepEqual(data.settings.defaultSearchEngineData, expectedSearchEngineData); + + // Remove all the search engines. + for (let engine of Services.search.getEngines()) { + Services.search.removeEngine(engine); + } + // The search service does not notify "engine-current" when removing a default engine. + // Manually force the notification. + // TODO: remove this when bug 1165341 is resolved. + Services.obs.notifyObservers(null, "browser-search-engine-modified", "engine-current"); + + // Then check that no default engine is reported if none is available. + data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + Assert.equal(data.settings.defaultSearchEngine, "NONE"); + Assert.deepEqual(data.settings.defaultSearchEngineData, {name:"NONE"}); + + // Add a new search engine (this will have no engine identifier). + const SEARCH_ENGINE_ID = "telemetry_default"; + const SEARCH_ENGINE_URL = "http://www.example.org/?search={searchTerms}"; + Services.search.addEngineWithDetails(SEARCH_ENGINE_ID, "", null, "", "get", SEARCH_ENGINE_URL); + + // Register a new change listener and then wait for the search engine change to be notified. + let deferred = PromiseUtils.defer(); + TelemetryEnvironment.registerChangeListener("testWatch_SearchDefault", deferred.resolve); + Services.search.defaultEngine = Services.search.getEngineByName(SEARCH_ENGINE_ID); + yield deferred.promise; + + data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + + const EXPECTED_SEARCH_ENGINE = "other-" + SEARCH_ENGINE_ID; + Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE); + + const EXPECTED_SEARCH_ENGINE_DATA = { + name: "telemetry_default", + loadPath: "[other]addEngineWithDetails", + origin: "verified" + }; + Assert.deepEqual(data.settings.defaultSearchEngineData, EXPECTED_SEARCH_ENGINE_DATA); + TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault"); + + // Cleanly install an engine from an xml file, and check if origin is + // recorded as "verified". + let promise = new Promise(resolve => { + TelemetryEnvironment.registerChangeListener("testWatch_SearchDefault", resolve); + }); + let engine = yield new Promise((resolve, reject) => { + Services.obs.addObserver(function obs(obsSubject, obsTopic, obsData) { + try { + let searchEngine = obsSubject.QueryInterface(Ci.nsISearchEngine); + do_print("Observed " + obsData + " for " + searchEngine.name); + if (obsData != "engine-added" || searchEngine.name != "engine-telemetry") { + return; + } + + Services.obs.removeObserver(obs, "browser-search-engine-modified"); + resolve(searchEngine); + } catch (ex) { + reject(ex); + } + }, "browser-search-engine-modified", false); + Services.search.addEngine("file://" + do_get_cwd().path + "/engine.xml", + null, null, false); + }); + Services.search.defaultEngine = engine; + yield promise; + TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault"); + data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + Assert.deepEqual(data.settings.defaultSearchEngineData, + {"name":"engine-telemetry", "loadPath":"[other]/engine.xml", "origin":"verified"}); + + // Now break this engine's load path hash. + promise = new Promise(resolve => { + TelemetryEnvironment.registerChangeListener("testWatch_SearchDefault", resolve); + }); + engine.wrappedJSObject.setAttr("loadPathHash", "broken"); + Services.obs.notifyObservers(null, "browser-search-engine-modified", "engine-current"); + yield promise; + TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault"); + data = TelemetryEnvironment.currentEnvironment; + Assert.equal(data.settings.defaultSearchEngineData.origin, "invalid"); + Services.search.removeEngine(engine); + + // Define and reset the test preference. + const PREF_TEST = "toolkit.telemetry.test.pref1"; + const PREFS_TO_WATCH = new Map([ + [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_STATE}], + ]); + Preferences.reset(PREF_TEST); + + // Watch the test preference. + TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); + deferred = PromiseUtils.defer(); + TelemetryEnvironment.registerChangeListener("testSearchEngine_pref", deferred.resolve); + // Trigger an environment change. + Preferences.set(PREF_TEST, 1); + yield deferred.promise; + TelemetryEnvironment.unregisterChangeListener("testSearchEngine_pref"); + + // Check that the search engine information is correctly retained when prefs change. + data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE); + + // Check that by default we are not sending a cohort identifier... + Assert.equal(data.settings.searchCohort, undefined); + + // ... but that if a cohort identifier is set, we send it. + Services.prefs.setCharPref("browser.search.cohort", "testcohort"); + Services.obs.notifyObservers(null, "browser-search-service", "init-complete"); + data = TelemetryEnvironment.currentEnvironment; + Assert.equal(data.settings.searchCohort, "testcohort"); +}); + +add_task(function* test_osstrings() { + // First test that numbers in sysinfo properties are converted to string fields + // in system.os. + SysInfo.overrides = { + version: 1, + name: 2, + kernel_version: 3, + }; + + yield TelemetryEnvironment.testCleanRestart().onInitialized(); + let data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + + Assert.equal(data.system.os.version, "1"); + Assert.equal(data.system.os.name, "2"); + if (AppConstants.platform == "android") { + Assert.equal(data.system.os.kernelVersion, "3"); + } + + // Check that null values are also handled. + SysInfo.overrides = { + version: null, + name: null, + kernel_version: null, + }; + + yield TelemetryEnvironment.testCleanRestart().onInitialized(); + data = TelemetryEnvironment.currentEnvironment; + checkEnvironmentData(data); + + Assert.equal(data.system.os.version, null); + Assert.equal(data.system.os.name, null); + if (AppConstants.platform == "android") { + Assert.equal(data.system.os.kernelVersion, null); + } + + // Clean up. + SysInfo.overrides = {}; + yield TelemetryEnvironment.testCleanRestart().onInitialized(); +}); + +add_task(function* test_environmentShutdown() { + // Define and reset the test preference. + const PREF_TEST = "toolkit.telemetry.test.pref1"; + const PREFS_TO_WATCH = new Map([ + [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_STATE}], + ]); + Preferences.reset(PREF_TEST); + + // Set up the preferences and listener, then the trigger shutdown + TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); + TelemetryEnvironment.registerChangeListener("test_environmentShutdownChange", () => { + // Register a new change listener that asserts if change is propogated + Assert.ok(false, "No change should be propagated after shutdown."); + }); + TelemetryEnvironment.shutdown(); + + // Flipping the test preference after shutdown should not trigger the listener + Preferences.set(PREF_TEST, 1); + + // Unregister the listener. + TelemetryEnvironment.unregisterChangeListener("test_environmentShutdownChange"); +}); diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js b/toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js new file mode 100644 index 000000000..2bfb62c14 --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js @@ -0,0 +1,249 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +*/ + +const OPTIN = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN; +const OPTOUT = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTOUT; + +function checkEventFormat(events) { + Assert.ok(Array.isArray(events), "Events should be serialized to an array."); + for (let e of events) { + Assert.ok(Array.isArray(e), "Event should be an array."); + Assert.greaterOrEqual(e.length, 4, "Event should have at least 4 elements."); + Assert.lessOrEqual(e.length, 6, "Event should have at most 6 elements."); + + Assert.equal(typeof(e[0]), "number", "Element 0 should be a number."); + Assert.equal(typeof(e[1]), "string", "Element 1 should be a string."); + Assert.equal(typeof(e[2]), "string", "Element 2 should be a string."); + Assert.equal(typeof(e[3]), "string", "Element 3 should be a string."); + + if (e.length > 4) { + Assert.ok(e[4] === null || typeof(e[4]) == "string", + "Event element 4 should be null or a string."); + } + if (e.length > 5) { + Assert.ok(e[5] === null || typeof(e[5]) == "object", + "Event element 5 should be null or an object."); + } + + let extra = e[5]; + if (extra) { + Assert.ok(Object.keys(extra).every(k => typeof(k) == "string"), + "All extra keys should be strings."); + Assert.ok(Object.values(extra).every(v => typeof(v) == "string"), + "All extra values should be strings."); + } + } +} + +add_task(function* test_recording() { + Telemetry.clearEvents(); + + // Record some events. + let expected = [ + {optout: false, event: ["telemetry.test", "test1", "object1"]}, + {optout: false, event: ["telemetry.test", "test2", "object2"]}, + + {optout: false, event: ["telemetry.test", "test1", "object1", "value"]}, + {optout: false, event: ["telemetry.test", "test1", "object1", "value", null]}, + {optout: false, event: ["telemetry.test", "test1", "object1", null, {"key1": "value1"}]}, + {optout: false, event: ["telemetry.test", "test1", "object1", "value", {"key1": "value1", "key2": "value2"}]}, + + {optout: true, event: ["telemetry.test", "optout", "object1"]}, + {optout: false, event: ["telemetry.test.second", "test", "object1"]}, + {optout: false, event: ["telemetry.test.second", "test", "object1", null, {"key1": "value1"}]}, + ]; + + for (let entry of expected) { + entry.tsBefore = Math.floor(Telemetry.msSinceProcessStart()); + try { + Telemetry.recordEvent(...entry.event); + } catch (ex) { + Assert.ok(false, `Failed to record event ${JSON.stringify(entry.event)}: ${ex}`); + } + entry.tsAfter = Math.floor(Telemetry.msSinceProcessStart()); + } + + // Strip off trailing null values to match the serialized events. + for (let entry of expected) { + let e = entry.event; + while ((e.length >= 3) && (e[e.length - 1] === null)) { + e.pop(); + } + } + + // The following should not result in any recorded events. + Assert.throws(() => Telemetry.recordEvent("unknown.category", "test1", "object1"), + /Error: Unknown event: \["unknown.category", "test1", "object1"\]/, + "Should throw on unknown category."); + Assert.throws(() => Telemetry.recordEvent("telemetry.test", "unknown", "object1"), + /Error: Unknown event: \["telemetry.test", "unknown", "object1"\]/, + "Should throw on unknown method."); + Assert.throws(() => Telemetry.recordEvent("telemetry.test", "test1", "unknown"), + /Error: Unknown event: \["telemetry.test", "test1", "unknown"\]/, + "Should throw on unknown object."); + + let checkEvents = (events, expectedEvents) => { + checkEventFormat(events); + Assert.equal(events.length, expectedEvents.length, + "Snapshot should have the right number of events."); + + for (let i = 0; i < events.length; ++i) { + let {tsBefore, tsAfter} = expectedEvents[i]; + let ts = events[i][0]; + Assert.greaterOrEqual(ts, tsBefore, "The recorded timestamp should be greater than the one before recording."); + Assert.lessOrEqual(ts, tsAfter, "The recorded timestamp should be less than the one after recording."); + + let recordedData = events[i].slice(1); + let expectedData = expectedEvents[i].event.slice(); + Assert.deepEqual(recordedData, expectedData, "The recorded event data should match."); + } + }; + + // Check that the expected events were recorded. + let events = Telemetry.snapshotBuiltinEvents(OPTIN, false); + checkEvents(events, expected); + + // Check serializing only opt-out events. + events = Telemetry.snapshotBuiltinEvents(OPTOUT, false); + filtered = expected.filter(e => e.optout == true); + checkEvents(events, filtered); +}); + +add_task(function* test_clear() { + Telemetry.clearEvents(); + + const COUNT = 10; + for (let i = 0; i < COUNT; ++i) { + Telemetry.recordEvent("telemetry.test", "test1", "object1"); + Telemetry.recordEvent("telemetry.test.second", "test", "object1"); + } + + // Check that events were recorded. + // The events are cleared by passing the respective flag. + let events = Telemetry.snapshotBuiltinEvents(OPTIN, true); + Assert.equal(events.length, 2 * COUNT, `Should have recorded ${2 * COUNT} events.`); + + // Now the events should be cleared. + events = Telemetry.snapshotBuiltinEvents(OPTIN, false); + Assert.equal(events.length, 0, `Should have cleared the events.`); +}); + +add_task(function* test_expiry() { + Telemetry.clearEvents(); + + // Recording call with event that is expired by version. + Telemetry.recordEvent("telemetry.test", "expired_version", "object1"); + let events = Telemetry.snapshotBuiltinEvents(OPTIN, true); + Assert.equal(events.length, 0, "Should not record event with expired version."); + + // Recording call with event that is expired by date. + Telemetry.recordEvent("telemetry.test", "expired_date", "object1"); + events = Telemetry.snapshotBuiltinEvents(OPTIN, true); + Assert.equal(events.length, 0, "Should not record event with expired date."); + + // Recording call with event that has expiry_version and expiry_date in the future. + Telemetry.recordEvent("telemetry.test", "not_expired_optout", "object1"); + events = Telemetry.snapshotBuiltinEvents(OPTOUT, true); + Assert.equal(events.length, 1, "Should record event when date and version are not expired."); +}); + +add_task(function* test_invalidParams() { + Telemetry.clearEvents(); + + // Recording call with wrong type for value argument. + Telemetry.recordEvent("telemetry.test", "test1", "object1", 1); + let events = Telemetry.snapshotBuiltinEvents(OPTIN, true); + Assert.equal(events.length, 0, "Should not record event when value argument with invalid type is passed."); + + // Recording call with wrong type for extra argument. + Telemetry.recordEvent("telemetry.test", "test1", "object1", null, "invalid"); + events = Telemetry.snapshotBuiltinEvents(OPTIN, true); + Assert.equal(events.length, 0, "Should not record event when extra argument with invalid type is passed."); + + // Recording call with unknown extra key. + Telemetry.recordEvent("telemetry.test", "test1", "object1", null, {"key3": "x"}); + events = Telemetry.snapshotBuiltinEvents(OPTIN, true); + Assert.equal(events.length, 0, "Should not record event when extra argument with invalid key is passed."); + + // Recording call with invalid value type. + Telemetry.recordEvent("telemetry.test", "test1", "object1", null, {"key3": 1}); + events = Telemetry.snapshotBuiltinEvents(OPTIN, true); + Assert.equal(events.length, 0, "Should not record event when extra argument with invalid value type is passed."); +}); + +add_task(function* test_storageLimit() { + Telemetry.clearEvents(); + + // Record more events than the storage limit allows. + let LIMIT = 1000; + let COUNT = LIMIT + 10; + for (let i = 0; i < COUNT; ++i) { + Telemetry.recordEvent("telemetry.test", "test1", "object1", String(i)); + } + + // Check that the right events were recorded. + let events = Telemetry.snapshotBuiltinEvents(OPTIN, true); + Assert.equal(events.length, LIMIT, `Should have only recorded ${LIMIT} events`); + Assert.ok(events.every((e, idx) => e[4] === String(idx)), + "Should have recorded all events from before hitting the limit."); +}); + +add_task(function* test_valueLimits() { + Telemetry.clearEvents(); + + // Record values that are at or over the limits for string lengths. + let LIMIT = 80; + let expected = [ + ["telemetry.test", "test1", "object1", "a".repeat(LIMIT - 10), null], + ["telemetry.test", "test1", "object1", "a".repeat(LIMIT ), null], + ["telemetry.test", "test1", "object1", "a".repeat(LIMIT + 1), null], + ["telemetry.test", "test1", "object1", "a".repeat(LIMIT + 10), null], + + ["telemetry.test", "test1", "object1", null, {key1: "a".repeat(LIMIT - 10)}], + ["telemetry.test", "test1", "object1", null, {key1: "a".repeat(LIMIT )}], + ["telemetry.test", "test1", "object1", null, {key1: "a".repeat(LIMIT + 1)}], + ["telemetry.test", "test1", "object1", null, {key1: "a".repeat(LIMIT + 10)}], + ]; + + for (let event of expected) { + Telemetry.recordEvent(...event); + if (event[3]) { + event[3] = event[3].substr(0, LIMIT); + } + if (event[4]) { + event[4].key1 = event[4].key1.substr(0, LIMIT); + } + } + + // Strip off trailing null values to match the serialized events. + for (let e of expected) { + while ((e.length >= 3) && (e[e.length - 1] === null)) { + e.pop(); + } + } + + // Check that the right events were recorded. + let events = Telemetry.snapshotBuiltinEvents(OPTIN, true); + Assert.equal(events.length, expected.length, + "Should have recorded the expected number of events"); + for (let i = 0; i < expected.length; ++i) { + Assert.deepEqual(events[i].slice(1), expected[i], + "Should have recorded the expected event data."); + } +}); + +add_task(function* test_unicodeValues() { + Telemetry.clearEvents(); + + // Record string values containing unicode characters. + let value = "漢語"; + Telemetry.recordEvent("telemetry.test", "test1", "object1", value); + Telemetry.recordEvent("telemetry.test", "test1", "object1", null, {"key1": value}); + + // Check that the values were correctly recorded. + let events = Telemetry.snapshotBuiltinEvents(OPTIN, true); + Assert.equal(events.length, 2, "Should have recorded 2 events."); + Assert.equal(events[0][4], value, "Should have recorded the right value."); + Assert.equal(events[1][5]["key1"], value, "Should have recorded the right extra value."); +}); diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryFlagClear.js b/toolkit/components/telemetry/tests/unit/test_TelemetryFlagClear.js new file mode 100644 index 000000000..712aceb3b --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryFlagClear.js @@ -0,0 +1,14 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() +{ + let testFlag = Services.telemetry.getHistogramById("TELEMETRY_TEST_FLAG"); + equal(JSON.stringify(testFlag.snapshot().counts), "[1,0,0]", "Original value is correct"); + testFlag.add(1); + equal(JSON.stringify(testFlag.snapshot().counts), "[0,1,0]", "Value is correct after ping."); + testFlag.clear(); + equal(JSON.stringify(testFlag.snapshot().counts), "[1,0,0]", "Value is correct after calling clear()"); + testFlag.add(1); + equal(JSON.stringify(testFlag.snapshot().counts), "[0,1,0]", "Value is correct after ping."); +} diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryLateWrites.js b/toolkit/components/telemetry/tests/unit/test_TelemetryLateWrites.js new file mode 100644 index 000000000..f2b2b3bba --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryLateWrites.js @@ -0,0 +1,127 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +*/ +/* A testcase to make sure reading late writes stacks works. */ + +Cu.import("resource://gre/modules/Services.jsm", this); + +// Constants from prio.h for nsIFileOutputStream.init +const PR_WRONLY = 0x2; +const PR_CREATE_FILE = 0x8; +const PR_TRUNCATE = 0x20; +const RW_OWNER = parseInt("0600", 8); + +const STACK_SUFFIX1 = "stack1.txt"; +const STACK_SUFFIX2 = "stack2.txt"; +const STACK_BOGUS_SUFFIX = "bogus.txt"; +const LATE_WRITE_PREFIX = "Telemetry.LateWriteFinal-"; + +// The names and IDs don't matter, but the format of the IDs does. +const LOADED_MODULES = { + '4759A7E6993548C89CAF716A67EC242D00': 'libtest.so', + 'F77AF15BB8D6419FA875954B4A3506CA00': 'libxul.so', + '1E2F7FB590424E8F93D60BB88D66B8C500': 'libc.so' +}; +const N_MODULES = Object.keys(LOADED_MODULES).length; + +// Format of individual items is [index, offset-in-library]. +const STACK1 = [ + [ 0, 0 ], + [ 1, 1 ], + [ 2, 2 ] +]; +const STACK2 = [ + [ 0, 0 ], + [ 1, 5 ], + [ 2, 10 ], +]; +// XXX The only error checking is for a zero-sized stack. +const STACK_BOGUS = []; + +function write_string_to_file(file, contents) { + let ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + ostream.init(file, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, + RW_OWNER, ostream.DEFER_OPEN); + ostream.write(contents, contents.length); + ostream.QueryInterface(Ci.nsISafeOutputStream).finish(); + ostream.close(); +} + +function construct_file(suffix) { + let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile); + let file = profileDirectory.clone(); + file.append(LATE_WRITE_PREFIX + suffix); + return file; +} + +function write_late_writes_file(stack, suffix) +{ + let file = construct_file(suffix); + let contents = N_MODULES + "\n"; + for (let id in LOADED_MODULES) { + contents += id + " " + LOADED_MODULES[id] + "\n"; + } + + contents += stack.length + "\n"; + for (let element of stack) { + contents += element[0] + " " + element[1].toString(16) + "\n"; + } + + write_string_to_file(file, contents); +} + +function run_test() { + do_get_profile(); + + write_late_writes_file(STACK1, STACK_SUFFIX1); + write_late_writes_file(STACK2, STACK_SUFFIX2); + write_late_writes_file(STACK_BOGUS, STACK_BOGUS_SUFFIX); + + let lateWrites = Telemetry.lateWrites; + do_check_true("memoryMap" in lateWrites); + do_check_eq(lateWrites.memoryMap.length, 0); + do_check_true("stacks" in lateWrites); + do_check_eq(lateWrites.stacks.length, 0); + + do_test_pending(); + Telemetry.asyncFetchTelemetryData(function () { + actual_test(); + }); +} + +function actual_test() { + do_check_false(construct_file(STACK_SUFFIX1).exists()); + do_check_false(construct_file(STACK_SUFFIX2).exists()); + do_check_false(construct_file(STACK_BOGUS_SUFFIX).exists()); + + let lateWrites = Telemetry.lateWrites; + + do_check_true("memoryMap" in lateWrites); + do_check_eq(lateWrites.memoryMap.length, N_MODULES); + for (let id in LOADED_MODULES) { + let matchingLibrary = lateWrites.memoryMap.filter(function(library, idx, array) { + return library[1] == id; + }); + do_check_eq(matchingLibrary.length, 1); + let library = matchingLibrary[0] + let name = library[0]; + do_check_eq(LOADED_MODULES[id], name); + } + + do_check_true("stacks" in lateWrites); + do_check_eq(lateWrites.stacks.length, 2); + let uneval_STACKS = [uneval(STACK1), uneval(STACK2)]; + let first_stack = lateWrites.stacks[0]; + let second_stack = lateWrites.stacks[1]; + function stackChecker(canonicalStack) { + let unevalCanonicalStack = uneval(canonicalStack); + return function(obj, idx, array) { + return unevalCanonicalStack == obj; + } + } + do_check_eq(uneval_STACKS.filter(stackChecker(first_stack)).length, 1); + do_check_eq(uneval_STACKS.filter(stackChecker(second_stack)).length, 1); + + do_test_finished(); +} diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryLockCount.js b/toolkit/components/telemetry/tests/unit/test_TelemetryLockCount.js new file mode 100644 index 000000000..808f2f3ec --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryLockCount.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +*/ +/* A testcase to make sure reading the failed profile lock count works. */ + +Cu.import("resource://gre/modules/Services.jsm", this); + +const LOCK_FILE_NAME = "Telemetry.FailedProfileLocks.txt"; +const N_FAILED_LOCKS = 10; + +// Constants from prio.h for nsIFileOutputStream.init +const PR_WRONLY = 0x2; +const PR_CREATE_FILE = 0x8; +const PR_TRUNCATE = 0x20; +const RW_OWNER = parseInt("0600", 8); + +function write_string_to_file(file, contents) { + let ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + ostream.init(file, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, + RW_OWNER, ostream.DEFER_OPEN); + ostream.write(contents, contents.length); + ostream.QueryInterface(Ci.nsISafeOutputStream).finish(); + ostream.close(); +} + +function construct_file() { + let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile); + let file = profileDirectory.clone(); + file.append(LOCK_FILE_NAME); + return file; +} + +function run_test() { + do_get_profile(); + + do_check_eq(Telemetry.failedProfileLockCount, 0); + + write_string_to_file(construct_file(), N_FAILED_LOCKS.toString()); + + // Make sure that we're not eagerly reading the count now that the + // file exists. + do_check_eq(Telemetry.failedProfileLockCount, 0); + + do_test_pending(); + Telemetry.asyncFetchTelemetryData(actual_test); +} + +function actual_test() { + do_check_eq(Telemetry.failedProfileLockCount, N_FAILED_LOCKS); + do_check_false(construct_file().exists()); + do_test_finished(); +} diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryLog.js b/toolkit/components/telemetry/tests/unit/test_TelemetryLog.js new file mode 100644 index 000000000..ea37a1bc5 --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryLog.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +Cu.import("resource://gre/modules/TelemetryLog.jsm", this); +Cu.import("resource://gre/modules/TelemetrySession.jsm", this); + +const TEST_PREFIX = "TEST-"; +const TEST_REGEX = new RegExp("^" + TEST_PREFIX); + +function check_event(event, id, data) +{ + do_print("Checking message " + id); + do_check_eq(event[0], id); + do_check_true(event[1] > 0); + + if (data === undefined) { + do_check_true(event.length == 2); + } else { + do_check_eq(event.length, data.length + 2); + for (var i = 0; i < data.length; ++i) { + do_check_eq(typeof(event[i + 2]), "string"); + do_check_eq(event[i + 2], data[i]); + } + } +} + +add_task(function* () +{ + do_get_profile(); + // TODO: After Bug 1254550 lands we should not need to set the pref here. + Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true); + yield TelemetryController.testSetup(); + + TelemetryLog.log(TEST_PREFIX + "1", ["val", 123, undefined]); + TelemetryLog.log(TEST_PREFIX + "2", []); + TelemetryLog.log(TEST_PREFIX + "3"); + + var log = TelemetrySession.getPayload().log.filter(function(e) { + // Only want events that were generated by the test. + return TEST_REGEX.test(e[0]); + }); + + do_check_eq(log.length, 3); + check_event(log[0], TEST_PREFIX + "1", ["val", "123", "undefined"]); + check_event(log[1], TEST_PREFIX + "2", []); + check_event(log[2], TEST_PREFIX + "3", undefined); + do_check_true(log[0][1] <= log[1][1]); + do_check_true(log[1][1] <= log[2][1]); + + yield TelemetryController.testShutdown(); +}); diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js b/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js new file mode 100644 index 000000000..68606a98e --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js @@ -0,0 +1,268 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that TelemetryController sends close to shutdown don't lead +// to AsyncShutdown timeouts. + +"use strict"; + +Cu.import("resource://gre/modules/Preferences.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/TelemetryController.jsm", this); +Cu.import("resource://gre/modules/TelemetrySend.jsm", this); +Cu.import("resource://gre/modules/TelemetryReportingPolicy.jsm", this); +Cu.import("resource://gre/modules/TelemetryUtils.jsm", this); +Cu.import("resource://gre/modules/Timer.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://gre/modules/UpdateUtils.jsm", this); + +const PREF_BRANCH = "toolkit.telemetry."; +const PREF_SERVER = PREF_BRANCH + "server"; + +const TEST_CHANNEL = "TestChannelABC"; + +const PREF_POLICY_BRANCH = "datareporting.policy."; +const PREF_BYPASS_NOTIFICATION = PREF_POLICY_BRANCH + "dataSubmissionPolicyBypassNotification"; +const PREF_DATA_SUBMISSION_ENABLED = PREF_POLICY_BRANCH + "dataSubmissionEnabled"; +const PREF_CURRENT_POLICY_VERSION = PREF_POLICY_BRANCH + "currentPolicyVersion"; +const PREF_MINIMUM_POLICY_VERSION = PREF_POLICY_BRANCH + "minimumPolicyVersion"; +const PREF_MINIMUM_CHANNEL_POLICY_VERSION = PREF_MINIMUM_POLICY_VERSION + ".channel-" + TEST_CHANNEL; +const PREF_ACCEPTED_POLICY_VERSION = PREF_POLICY_BRANCH + "dataSubmissionPolicyAcceptedVersion"; +const PREF_ACCEPTED_POLICY_DATE = PREF_POLICY_BRANCH + "dataSubmissionPolicyNotifiedTime"; + +function fakeShowPolicyTimeout(set, clear) { + let reportingPolicy = Cu.import("resource://gre/modules/TelemetryReportingPolicy.jsm"); + reportingPolicy.Policy.setShowInfobarTimeout = set; + reportingPolicy.Policy.clearShowInfobarTimeout = clear; +} + +function fakeResetAcceptedPolicy() { + Preferences.reset(PREF_ACCEPTED_POLICY_DATE); + Preferences.reset(PREF_ACCEPTED_POLICY_VERSION); +} + +function setMinimumPolicyVersion(aNewPolicyVersion) { + const CHANNEL_NAME = UpdateUtils.getUpdateChannel(false); + // We might have channel-dependent minimum policy versions. + const CHANNEL_DEPENDENT_PREF = PREF_MINIMUM_POLICY_VERSION + ".channel-" + CHANNEL_NAME; + + // Does the channel-dependent pref exist? If so, set its value. + if (Preferences.get(CHANNEL_DEPENDENT_PREF, undefined)) { + Preferences.set(CHANNEL_DEPENDENT_PREF, aNewPolicyVersion); + return; + } + + // We don't have a channel specific minimu, so set the common one. + Preferences.set(PREF_MINIMUM_POLICY_VERSION, aNewPolicyVersion); +} + +add_task(function* test_setup() { + // Addon manager needs a profile directory + do_get_profile(true); + loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + + // Make sure we don't generate unexpected pings due to pref changes. + yield setEmptyPrefWatchlist(); + + Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true); + // Don't bypass the notifications in this test, we'll fake it. + Services.prefs.setBoolPref(PREF_BYPASS_NOTIFICATION, false); + + TelemetryReportingPolicy.setup(); +}); + +add_task(function* test_firstRun() { + const PREF_FIRST_RUN = "toolkit.telemetry.reportingpolicy.firstRun"; + const FIRST_RUN_TIMEOUT_MSEC = 60 * 1000; // 60s + const OTHER_RUNS_TIMEOUT_MSEC = 10 * 1000; // 10s + + Preferences.reset(PREF_FIRST_RUN); + + let startupTimeout = 0; + fakeShowPolicyTimeout((callback, timeout) => startupTimeout = timeout, () => {}); + TelemetryReportingPolicy.reset(); + + Services.obs.notifyObservers(null, "sessionstore-windows-restored", null); + Assert.equal(startupTimeout, FIRST_RUN_TIMEOUT_MSEC, + "The infobar display timeout should be 60s on the first run."); + + // Run again, and check that we actually wait only 10 seconds. + TelemetryReportingPolicy.reset(); + Services.obs.notifyObservers(null, "sessionstore-windows-restored", null); + Assert.equal(startupTimeout, OTHER_RUNS_TIMEOUT_MSEC, + "The infobar display timeout should be 10s on other runs."); +}); + +add_task(function* test_prefs() { + TelemetryReportingPolicy.reset(); + + let now = fakeNow(2009, 11, 18); + + // If the date is not valid (earlier than 2012), we don't regard the policy as accepted. + TelemetryReportingPolicy.testInfobarShown(); + Assert.ok(!TelemetryReportingPolicy.testIsUserNotified()); + Assert.equal(Preferences.get(PREF_ACCEPTED_POLICY_DATE, null), 0, + "Invalid dates should not make the policy accepted."); + + // Check that the notification date and version are correctly saved to the prefs. + now = fakeNow(2012, 11, 18); + TelemetryReportingPolicy.testInfobarShown(); + Assert.equal(Preferences.get(PREF_ACCEPTED_POLICY_DATE, null), now.getTime(), + "A valid date must correctly be saved."); + + // Now that user is notified, check if we are allowed to upload. + Assert.ok(TelemetryReportingPolicy.canUpload(), + "We must be able to upload after the policy is accepted."); + + // Disable submission and check that we're no longer allowed to upload. + Preferences.set(PREF_DATA_SUBMISSION_ENABLED, false); + Assert.ok(!TelemetryReportingPolicy.canUpload(), + "We must not be able to upload if data submission is disabled."); + + // Turn the submission back on. + Preferences.set(PREF_DATA_SUBMISSION_ENABLED, true); + Assert.ok(TelemetryReportingPolicy.canUpload(), + "We must be able to upload if data submission is enabled and the policy was accepted."); + + // Set a new minimum policy version and check that user is no longer notified. + let newMinimum = Preferences.get(PREF_CURRENT_POLICY_VERSION, 1) + 1; + setMinimumPolicyVersion(newMinimum); + Assert.ok(!TelemetryReportingPolicy.testIsUserNotified(), + "A greater minimum policy version must invalidate the policy and disable upload."); + + // Eventually accept the policy and make sure user is notified. + Preferences.set(PREF_CURRENT_POLICY_VERSION, newMinimum); + TelemetryReportingPolicy.testInfobarShown(); + Assert.ok(TelemetryReportingPolicy.testIsUserNotified(), + "Accepting the policy again should show the user as notified."); + Assert.ok(TelemetryReportingPolicy.canUpload(), + "Accepting the policy again should let us upload data."); + + // Set a new, per channel, minimum policy version. Start by setting a test current channel. + let defaultPrefs = new Preferences({ defaultBranch: true }); + defaultPrefs.set("app.update.channel", TEST_CHANNEL); + + // Increase and set the new minimum version, then check that we're not notified anymore. + newMinimum++; + Preferences.set(PREF_MINIMUM_CHANNEL_POLICY_VERSION, newMinimum); + Assert.ok(!TelemetryReportingPolicy.testIsUserNotified(), + "Increasing the minimum policy version should invalidate the policy."); + + // Eventually accept the policy and make sure user is notified. + Preferences.set(PREF_CURRENT_POLICY_VERSION, newMinimum); + TelemetryReportingPolicy.testInfobarShown(); + Assert.ok(TelemetryReportingPolicy.testIsUserNotified(), + "Accepting the policy again should show the user as notified."); + Assert.ok(TelemetryReportingPolicy.canUpload(), + "Accepting the policy again should let us upload data."); +}); + +add_task(function* test_migratePrefs() { + const DEPRECATED_FHR_PREFS = { + "datareporting.policy.dataSubmissionPolicyAccepted": true, + "datareporting.policy.dataSubmissionPolicyBypassAcceptance": true, + "datareporting.policy.dataSubmissionPolicyResponseType": "foxyeah", + "datareporting.policy.dataSubmissionPolicyResponseTime": Date.now().toString(), + }; + + // Make sure the preferences are set before setting up the policy. + for (let name in DEPRECATED_FHR_PREFS) { + Preferences.set(name, DEPRECATED_FHR_PREFS[name]); + } + // Set up the policy. + TelemetryReportingPolicy.reset(); + // They should have been removed by now. + for (let name in DEPRECATED_FHR_PREFS) { + Assert.ok(!Preferences.has(name), name + " should have been removed."); + } +}); + +add_task(function* test_userNotifiedOfCurrentPolicy() { + fakeResetAcceptedPolicy(); + TelemetryReportingPolicy.reset(); + + // User should be reported as not notified by default. + Assert.ok(!TelemetryReportingPolicy.testIsUserNotified(), + "The initial state should be unnotified."); + + // Forcing a policy version should not automatically make the user notified. + Preferences.set(PREF_ACCEPTED_POLICY_VERSION, + TelemetryReportingPolicy.DEFAULT_DATAREPORTING_POLICY_VERSION); + Assert.ok(!TelemetryReportingPolicy.testIsUserNotified(), + "The default state of the date should have a time of 0 and it should therefore fail"); + + // Showing the notification bar should make the user notified. + fakeNow(2012, 11, 11); + TelemetryReportingPolicy.testInfobarShown(); + Assert.ok(TelemetryReportingPolicy.testIsUserNotified(), + "Using the proper API causes user notification to report as true."); + + // It is assumed that later versions of the policy will incorporate previous + // ones, therefore this should also return true. + let newVersion = + Preferences.get(PREF_CURRENT_POLICY_VERSION, 1) + 1; + Preferences.set(PREF_ACCEPTED_POLICY_VERSION, newVersion); + Assert.ok(TelemetryReportingPolicy.testIsUserNotified(), + "A future version of the policy should pass."); + + newVersion = + Preferences.get(PREF_CURRENT_POLICY_VERSION, 1) - 1; + Preferences.set(PREF_ACCEPTED_POLICY_VERSION, newVersion); + Assert.ok(!TelemetryReportingPolicy.testIsUserNotified(), + "A previous version of the policy should fail."); +}); + +add_task(function* test_canSend() { + const TEST_PING_TYPE = "test-ping"; + + PingServer.start(); + Preferences.set(PREF_SERVER, "http://localhost:" + PingServer.port); + + yield TelemetryController.testReset(); + TelemetryReportingPolicy.reset(); + + // User should be reported as not notified by default. + Assert.ok(!TelemetryReportingPolicy.testIsUserNotified(), + "The initial state should be unnotified."); + + // Assert if we receive any ping before the policy is accepted. + PingServer.registerPingHandler(() => Assert.ok(false, "Should not have received any pings now")); + yield TelemetryController.submitExternalPing(TEST_PING_TYPE, {}); + + // Reset the ping handler. + PingServer.resetPingHandler(); + + // Fake the infobar: this should also trigger the ping send task. + TelemetryReportingPolicy.testInfobarShown(); + let ping = yield PingServer.promiseNextPings(1); + Assert.equal(ping.length, 1, "We should have received one ping."); + Assert.equal(ping[0].type, TEST_PING_TYPE, + "We should have received the previous ping."); + + // Submit another ping, to make sure it gets sent. + yield TelemetryController.submitExternalPing(TEST_PING_TYPE, {}); + + // Get the ping and check its type. + ping = yield PingServer.promiseNextPings(1); + Assert.equal(ping.length, 1, "We should have received one ping."); + Assert.equal(ping[0].type, TEST_PING_TYPE, "We should have received the new ping."); + + // Fake a restart with a pending ping. + yield TelemetryController.addPendingPing(TEST_PING_TYPE, {}); + yield TelemetryController.testReset(); + + // We should be immediately sending the ping out. + ping = yield PingServer.promiseNextPings(1); + Assert.equal(ping.length, 1, "We should have received one ping."); + Assert.equal(ping[0].type, TEST_PING_TYPE, "We should have received the pending ping."); + + // Submit another ping, to make sure it gets sent. + yield TelemetryController.submitExternalPing(TEST_PING_TYPE, {}); + + // Get the ping and check its type. + ping = yield PingServer.promiseNextPings(1); + Assert.equal(ping.length, 1, "We should have received one ping."); + Assert.equal(ping[0].type, TEST_PING_TYPE, "We should have received the new ping."); + + yield PingServer.stop(); +}); diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js new file mode 100644 index 000000000..5914a4235 --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js @@ -0,0 +1,574 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +*/ + +const UINT_SCALAR = "telemetry.test.unsigned_int_kind"; +const STRING_SCALAR = "telemetry.test.string_kind"; +const BOOLEAN_SCALAR = "telemetry.test.boolean_kind"; +const KEYED_UINT_SCALAR = "telemetry.test.keyed_unsigned_int"; + +add_task(function* test_serializationFormat() { + Telemetry.clearScalars(); + + // Set the scalars to a known value. + const expectedUint = 3785; + const expectedString = "some value"; + Telemetry.scalarSet(UINT_SCALAR, expectedUint); + Telemetry.scalarSet(STRING_SCALAR, expectedString); + Telemetry.scalarSet(BOOLEAN_SCALAR, true); + Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, "first_key", 1234); + + // Get a snapshot of the scalars. + const scalars = + Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + + // Check that they are serialized to the correct format. + Assert.equal(typeof(scalars[UINT_SCALAR]), "number", + UINT_SCALAR + " must be serialized to the correct format."); + Assert.ok(Number.isInteger(scalars[UINT_SCALAR]), + UINT_SCALAR + " must be a finite integer."); + Assert.equal(scalars[UINT_SCALAR], expectedUint, + UINT_SCALAR + " must have the correct value."); + Assert.equal(typeof(scalars[STRING_SCALAR]), "string", + STRING_SCALAR + " must be serialized to the correct format."); + Assert.equal(scalars[STRING_SCALAR], expectedString, + STRING_SCALAR + " must have the correct value."); + Assert.equal(typeof(scalars[BOOLEAN_SCALAR]), "boolean", + BOOLEAN_SCALAR + " must be serialized to the correct format."); + Assert.equal(scalars[BOOLEAN_SCALAR], true, + BOOLEAN_SCALAR + " must have the correct value."); + Assert.ok(!(KEYED_UINT_SCALAR in scalars), + "Keyed scalars must be reported in a separate section."); +}); + +add_task(function* test_keyedSerializationFormat() { + Telemetry.clearScalars(); + + const expectedKey = "first_key"; + const expectedOtherKey = "漢語"; + const expectedUint = 3785; + const expectedOtherValue = 1107; + + Telemetry.scalarSet(UINT_SCALAR, expectedUint); + Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, expectedKey, expectedUint); + Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, expectedOtherKey, expectedOtherValue); + + // Get a snapshot of the scalars. + const keyedScalars = + Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + + Assert.ok(!(UINT_SCALAR in keyedScalars), + UINT_SCALAR + " must not be serialized with the keyed scalars."); + Assert.ok(KEYED_UINT_SCALAR in keyedScalars, + KEYED_UINT_SCALAR + " must be serialized with the keyed scalars."); + Assert.equal(Object.keys(keyedScalars[KEYED_UINT_SCALAR]).length, 2, + "The keyed scalar must contain exactly 2 keys."); + Assert.ok(expectedKey in keyedScalars[KEYED_UINT_SCALAR], + KEYED_UINT_SCALAR + " must contain the expected keys."); + Assert.ok(expectedOtherKey in keyedScalars[KEYED_UINT_SCALAR], + KEYED_UINT_SCALAR + " must contain the expected keys."); + Assert.ok(Number.isInteger(keyedScalars[KEYED_UINT_SCALAR][expectedKey]), + KEYED_UINT_SCALAR + "." + expectedKey + " must be a finite integer."); + Assert.equal(keyedScalars[KEYED_UINT_SCALAR][expectedKey], expectedUint, + KEYED_UINT_SCALAR + "." + expectedKey + " must have the correct value."); + Assert.equal(keyedScalars[KEYED_UINT_SCALAR][expectedOtherKey], expectedOtherValue, + KEYED_UINT_SCALAR + "." + expectedOtherKey + " must have the correct value."); +}); + +add_task(function* test_nonexistingScalar() { + const NON_EXISTING_SCALAR = "telemetry.test.non_existing"; + + Telemetry.clearScalars(); + + // Make sure we throw on any operation for non-existing scalars. + Assert.throws(() => Telemetry.scalarAdd(NON_EXISTING_SCALAR, 11715), + /NS_ERROR_ILLEGAL_VALUE/, + "Adding to a non existing scalar must throw."); + Assert.throws(() => Telemetry.scalarSet(NON_EXISTING_SCALAR, 11715), + /NS_ERROR_ILLEGAL_VALUE/, + "Setting a non existing scalar must throw."); + Assert.throws(() => Telemetry.scalarSetMaximum(NON_EXISTING_SCALAR, 11715), + /NS_ERROR_ILLEGAL_VALUE/, + "Setting the maximum of a non existing scalar must throw."); + + // Make sure we throw on any operation for non-existing scalars. + Assert.throws(() => Telemetry.keyedScalarAdd(NON_EXISTING_SCALAR, "some_key", 11715), + /NS_ERROR_ILLEGAL_VALUE/, + "Adding to a non existing keyed scalar must throw."); + Assert.throws(() => Telemetry.keyedScalarSet(NON_EXISTING_SCALAR, "some_key", 11715), + /NS_ERROR_ILLEGAL_VALUE/, + "Setting a non existing keyed scalar must throw."); + Assert.throws(() => Telemetry.keyedScalarSetMaximum(NON_EXISTING_SCALAR, "some_key", 11715), + /NS_ERROR_ILLEGAL_VALUE/, + "Setting the maximum of a non keyed existing scalar must throw."); + + // Get a snapshot of the scalars. + const scalars = + Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + + Assert.ok(!(NON_EXISTING_SCALAR in scalars), "The non existing scalar must not be persisted."); + + const keyedScalars = + Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + + Assert.ok(!(NON_EXISTING_SCALAR in keyedScalars), + "The non existing keyed scalar must not be persisted."); +}); + +add_task(function* test_expiredScalar() { + const EXPIRED_SCALAR = "telemetry.test.expired"; + const EXPIRED_KEYED_SCALAR = "telemetry.test.keyed_expired"; + const UNEXPIRED_SCALAR = "telemetry.test.unexpired"; + + Telemetry.clearScalars(); + + // Try to set the expired scalar to some value. We will not be recording the value, + // but we shouldn't throw. + Telemetry.scalarAdd(EXPIRED_SCALAR, 11715); + Telemetry.scalarSet(EXPIRED_SCALAR, 11715); + Telemetry.scalarSetMaximum(EXPIRED_SCALAR, 11715); + Telemetry.keyedScalarAdd(EXPIRED_KEYED_SCALAR, "some_key", 11715); + Telemetry.keyedScalarSet(EXPIRED_KEYED_SCALAR, "some_key", 11715); + Telemetry.keyedScalarSetMaximum(EXPIRED_KEYED_SCALAR, "some_key", 11715); + + // The unexpired scalar has an expiration version, but far away in the future. + const expectedValue = 11716; + Telemetry.scalarSet(UNEXPIRED_SCALAR, expectedValue); + + // Get a snapshot of the scalars. + const scalars = + Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + const keyedScalars = + Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + + Assert.ok(!(EXPIRED_SCALAR in scalars), "The expired scalar must not be persisted."); + Assert.equal(scalars[UNEXPIRED_SCALAR], expectedValue, + "The unexpired scalar must be persisted with the correct value."); + Assert.ok(!(EXPIRED_KEYED_SCALAR in keyedScalars), + "The expired keyed scalar must not be persisted."); +}); + +add_task(function* test_unsignedIntScalar() { + let checkScalar = (expectedValue) => { + const scalars = + Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + Assert.equal(scalars[UINT_SCALAR], expectedValue, + UINT_SCALAR + " must contain the expected value."); + }; + + Telemetry.clearScalars(); + + // Let's start with an accumulation without a prior set. + Telemetry.scalarAdd(UINT_SCALAR, 1); + Telemetry.scalarAdd(UINT_SCALAR, 2); + // Do we get what we expect? + checkScalar(3); + + // Let's test setting the scalar to a value. + Telemetry.scalarSet(UINT_SCALAR, 3785); + checkScalar(3785); + Telemetry.scalarAdd(UINT_SCALAR, 1); + checkScalar(3786); + + // Does setMaximum work? + Telemetry.scalarSet(UINT_SCALAR, 2); + checkScalar(2); + Telemetry.scalarSetMaximum(UINT_SCALAR, 5); + checkScalar(5); + // The value of the probe should still be 5, as the previous value + // is greater than the one we want to set. + Telemetry.scalarSetMaximum(UINT_SCALAR, 3); + checkScalar(5); + + // Check that non-integer numbers get truncated and set. + Telemetry.scalarSet(UINT_SCALAR, 3.785); + checkScalar(3); + + // Setting or adding a negative number must report an error through + // the console and drop the change (shouldn't throw). + Telemetry.scalarAdd(UINT_SCALAR, -5); + Telemetry.scalarSet(UINT_SCALAR, -5); + Telemetry.scalarSetMaximum(UINT_SCALAR, -1); + checkScalar(3); + + // What happens if we try to set a value of a different type? + Telemetry.scalarSet(UINT_SCALAR, 1); + Assert.throws(() => Telemetry.scalarSet(UINT_SCALAR, "unexpected value"), + /NS_ERROR_ILLEGAL_VALUE/, + "Setting the scalar to an unexpected value type must throw."); + Assert.throws(() => Telemetry.scalarAdd(UINT_SCALAR, "unexpected value"), + /NS_ERROR_ILLEGAL_VALUE/, + "Adding an unexpected value type must throw."); + Assert.throws(() => Telemetry.scalarSetMaximum(UINT_SCALAR, "unexpected value"), + /NS_ERROR_ILLEGAL_VALUE/, + "Setting the scalar to an unexpected value type must throw."); + // The stored value must not be compromised. + checkScalar(1); +}); + +add_task(function* test_stringScalar() { + let checkExpectedString = (expectedString) => { + const scalars = + Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + Assert.equal(scalars[STRING_SCALAR], expectedString, + STRING_SCALAR + " must contain the expected string value."); + }; + + Telemetry.clearScalars(); + + // Let's check simple strings... + let expected = "test string"; + Telemetry.scalarSet(STRING_SCALAR, expected); + checkExpectedString(expected); + expected = "漢語"; + Telemetry.scalarSet(STRING_SCALAR, expected); + checkExpectedString(expected); + + // We have some unsupported operations for strings. + Assert.throws(() => Telemetry.scalarAdd(STRING_SCALAR, 1), + /NS_ERROR_NOT_AVAILABLE/, + "Using an unsupported operation must throw."); + Assert.throws(() => Telemetry.scalarAdd(STRING_SCALAR, "string value"), + /NS_ERROR_NOT_AVAILABLE/, + "Using an unsupported operation must throw."); + Assert.throws(() => Telemetry.scalarSetMaximum(STRING_SCALAR, 1), + /NS_ERROR_NOT_AVAILABLE/, + "Using an unsupported operation must throw."); + Assert.throws(() => Telemetry.scalarSetMaximum(STRING_SCALAR, "string value"), + /NS_ERROR_NOT_AVAILABLE/, + "Using an unsupported operation must throw."); + Assert.throws(() => Telemetry.scalarSet(STRING_SCALAR, 1), + /NS_ERROR_ILLEGAL_VALUE/, + "The string scalar must throw if we're not setting a string."); + + // Try to set the scalar to a string longer than the maximum length limit. + const LONG_STRING = "browser.qaxfiuosnzmhlg.rpvxicawolhtvmbkpnludhedobxvkjwqyeyvmv"; + Telemetry.scalarSet(STRING_SCALAR, LONG_STRING); + checkExpectedString(LONG_STRING.substr(0, 50)); +}); + +add_task(function* test_booleanScalar() { + let checkExpectedBool = (expectedBoolean) => { + const scalars = + Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + Assert.equal(scalars[BOOLEAN_SCALAR], expectedBoolean, + BOOLEAN_SCALAR + " must contain the expected boolean value."); + }; + + Telemetry.clearScalars(); + + // Set a test boolean value. + let expected = false; + Telemetry.scalarSet(BOOLEAN_SCALAR, expected); + checkExpectedBool(expected); + expected = true; + Telemetry.scalarSet(BOOLEAN_SCALAR, expected); + checkExpectedBool(expected); + + // Check that setting a numeric value implicitly converts to boolean. + Telemetry.scalarSet(BOOLEAN_SCALAR, 1); + checkExpectedBool(true); + Telemetry.scalarSet(BOOLEAN_SCALAR, 0); + checkExpectedBool(false); + Telemetry.scalarSet(BOOLEAN_SCALAR, 1.0); + checkExpectedBool(true); + Telemetry.scalarSet(BOOLEAN_SCALAR, 0.0); + checkExpectedBool(false); + + // Check that unsupported operations for booleans throw. + Assert.throws(() => Telemetry.scalarAdd(BOOLEAN_SCALAR, 1), + /NS_ERROR_NOT_AVAILABLE/, + "Using an unsupported operation must throw."); + Assert.throws(() => Telemetry.scalarAdd(BOOLEAN_SCALAR, "string value"), + /NS_ERROR_NOT_AVAILABLE/, + "Using an unsupported operation must throw."); + Assert.throws(() => Telemetry.scalarSetMaximum(BOOLEAN_SCALAR, 1), + /NS_ERROR_NOT_AVAILABLE/, + "Using an unsupported operation must throw."); + Assert.throws(() => Telemetry.scalarSetMaximum(BOOLEAN_SCALAR, "string value"), + /NS_ERROR_NOT_AVAILABLE/, + "Using an unsupported operation must throw."); + Assert.throws(() => Telemetry.scalarSet(BOOLEAN_SCALAR, "true"), + /NS_ERROR_ILLEGAL_VALUE/, + "The boolean scalar must throw if we're not setting a boolean."); +}); + +add_task(function* test_scalarRecording() { + const OPTIN_SCALAR = "telemetry.test.release_optin"; + const OPTOUT_SCALAR = "telemetry.test.release_optout"; + + let checkValue = (scalarName, expectedValue) => { + const scalars = + Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + Assert.equal(scalars[scalarName], expectedValue, + scalarName + " must contain the expected value."); + }; + + let checkNotSerialized = (scalarName) => { + const scalars = + Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + Assert.ok(!(scalarName in scalars), scalarName + " was not recorded."); + }; + + Telemetry.canRecordBase = false; + Telemetry.canRecordExtended = false; + Telemetry.clearScalars(); + + // Check that no scalar is recorded if both base and extended recording are off. + Telemetry.scalarSet(OPTOUT_SCALAR, 3); + Telemetry.scalarSet(OPTIN_SCALAR, 3); + checkNotSerialized(OPTOUT_SCALAR); + checkNotSerialized(OPTIN_SCALAR); + + // Check that opt-out scalars are recorded, while opt-in are not. + Telemetry.canRecordBase = true; + Telemetry.scalarSet(OPTOUT_SCALAR, 3); + Telemetry.scalarSet(OPTIN_SCALAR, 3); + checkValue(OPTOUT_SCALAR, 3); + checkNotSerialized(OPTIN_SCALAR); + + // Check that both opt-out and opt-in scalars are recorded. + Telemetry.canRecordExtended = true; + Telemetry.scalarSet(OPTOUT_SCALAR, 5); + Telemetry.scalarSet(OPTIN_SCALAR, 6); + checkValue(OPTOUT_SCALAR, 5); + checkValue(OPTIN_SCALAR, 6); +}); + +add_task(function* test_keyedScalarRecording() { + const OPTIN_SCALAR = "telemetry.test.keyed_release_optin"; + const OPTOUT_SCALAR = "telemetry.test.keyed_release_optout"; + const testKey = "policy_key"; + + let checkValue = (scalarName, expectedValue) => { + const scalars = + Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + Assert.equal(scalars[scalarName][testKey], expectedValue, + scalarName + " must contain the expected value."); + }; + + let checkNotSerialized = (scalarName) => { + const scalars = + Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + Assert.ok(!(scalarName in scalars), scalarName + " was not recorded."); + }; + + Telemetry.canRecordBase = false; + Telemetry.canRecordExtended = false; + Telemetry.clearScalars(); + + // Check that no scalar is recorded if both base and extended recording are off. + Telemetry.keyedScalarSet(OPTOUT_SCALAR, testKey, 3); + Telemetry.keyedScalarSet(OPTIN_SCALAR, testKey, 3); + checkNotSerialized(OPTOUT_SCALAR); + checkNotSerialized(OPTIN_SCALAR); + + // Check that opt-out scalars are recorded, while opt-in are not. + Telemetry.canRecordBase = true; + Telemetry.keyedScalarSet(OPTOUT_SCALAR, testKey, 3); + Telemetry.keyedScalarSet(OPTIN_SCALAR, testKey, 3); + checkValue(OPTOUT_SCALAR, 3); + checkNotSerialized(OPTIN_SCALAR); + + // Check that both opt-out and opt-in scalars are recorded. + Telemetry.canRecordExtended = true; + Telemetry.keyedScalarSet(OPTOUT_SCALAR, testKey, 5); + Telemetry.keyedScalarSet(OPTIN_SCALAR, testKey, 6); + checkValue(OPTOUT_SCALAR, 5); + checkValue(OPTIN_SCALAR, 6); +}); + +add_task(function* test_subsession() { + Telemetry.clearScalars(); + + // Set the scalars to a known value. + Telemetry.scalarSet(UINT_SCALAR, 3785); + Telemetry.scalarSet(STRING_SCALAR, "some value"); + Telemetry.scalarSet(BOOLEAN_SCALAR, false); + Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, "some_random_key", 12); + + // Get a snapshot and reset the subsession. The value we set must be there. + let scalars = + Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true); + let keyedScalars = + Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true); + + Assert.equal(scalars[UINT_SCALAR], 3785, + UINT_SCALAR + " must contain the expected value."); + Assert.equal(scalars[STRING_SCALAR], "some value", + STRING_SCALAR + " must contain the expected value."); + Assert.equal(scalars[BOOLEAN_SCALAR], false, + BOOLEAN_SCALAR + " must contain the expected value."); + Assert.equal(keyedScalars[KEYED_UINT_SCALAR]["some_random_key"], 12, + KEYED_UINT_SCALAR + " must contain the expected value."); + + // Get a new snapshot and reset the subsession again. Since no new value + // was set, the scalars should not be reported. + scalars = + Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true); + keyedScalars = + Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true); + + Assert.ok(!(UINT_SCALAR in scalars), UINT_SCALAR + " must be empty and not reported."); + Assert.ok(!(STRING_SCALAR in scalars), STRING_SCALAR + " must be empty and not reported."); + Assert.ok(!(BOOLEAN_SCALAR in scalars), BOOLEAN_SCALAR + " must be empty and not reported."); + Assert.ok(!(KEYED_UINT_SCALAR in keyedScalars), KEYED_UINT_SCALAR + " must be empty and not reported."); +}); + +add_task(function* test_keyed_uint() { + Telemetry.clearScalars(); + + const KEYS = [ "a_key", "another_key", "third_key" ]; + let expectedValues = [ 1, 1, 1 ]; + + // Set all the keys to a baseline value. + for (let key of KEYS) { + Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, key, 1); + } + + // Increment only one key. + Telemetry.keyedScalarAdd(KEYED_UINT_SCALAR, KEYS[1], 1); + expectedValues[1]++; + + // Use SetMaximum on the third key. + Telemetry.keyedScalarSetMaximum(KEYED_UINT_SCALAR, KEYS[2], 37); + expectedValues[2] = 37; + + // Get a snapshot of the scalars and make sure the keys contain + // the correct values. + const keyedScalars = + Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + + for (let k = 0; k < 3; k++) { + const keyName = KEYS[k]; + Assert.equal(keyedScalars[KEYED_UINT_SCALAR][keyName], expectedValues[k], + KEYED_UINT_SCALAR + "." + keyName + " must contain the correct value."); + } + + // Are we still throwing when doing unsupported things on uint keyed scalars? + // Just test one single unsupported operation, the other are covered in the plain + // unsigned scalar test. + Assert.throws(() => Telemetry.scalarSet(KEYED_UINT_SCALAR, "new_key", "unexpected value"), + /NS_ERROR_ILLEGAL_VALUE/, + "Setting the scalar to an unexpected value type must throw."); +}); + +add_task(function* test_keyed_boolean() { + Telemetry.clearScalars(); + + const KEYED_BOOLEAN_TYPE = "telemetry.test.keyed_boolean_kind"; + const first_key = "first_key"; + const second_key = "second_key"; + + // Set the initial values. + Telemetry.keyedScalarSet(KEYED_BOOLEAN_TYPE, first_key, true); + Telemetry.keyedScalarSet(KEYED_BOOLEAN_TYPE, second_key, false); + + // Get a snapshot of the scalars and make sure the keys contain + // the correct values. + let keyedScalars = + Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + Assert.equal(keyedScalars[KEYED_BOOLEAN_TYPE][first_key], true, + "The key must contain the expected value."); + Assert.equal(keyedScalars[KEYED_BOOLEAN_TYPE][second_key], false, + "The key must contain the expected value."); + + // Now flip the values and make sure we get the expected values back. + Telemetry.keyedScalarSet(KEYED_BOOLEAN_TYPE, first_key, false); + Telemetry.keyedScalarSet(KEYED_BOOLEAN_TYPE, second_key, true); + + keyedScalars = + Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + Assert.equal(keyedScalars[KEYED_BOOLEAN_TYPE][first_key], false, + "The key must contain the expected value."); + Assert.equal(keyedScalars[KEYED_BOOLEAN_TYPE][second_key], true, + "The key must contain the expected value."); + + // Are we still throwing when doing unsupported things on a boolean keyed scalars? + // Just test one single unsupported operation, the other are covered in the plain + // boolean scalar test. + Assert.throws(() => Telemetry.keyedScalarAdd(KEYED_BOOLEAN_TYPE, "somehey", 1), + /NS_ERROR_NOT_AVAILABLE/, + "Using an unsupported operation must throw."); +}); + +add_task(function* test_keyed_keys_length() { + Telemetry.clearScalars(); + + const LONG_KEY_STRING = + "browser.qaxfiuosnzmhlg.rpvxicawolhtvmbkpnludhedobxvkjwqyeyvmv.somemoresowereach70chars"; + const NORMAL_KEY = "a_key"; + + // Set the value for a key within the length limits. + Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, NORMAL_KEY, 1); + + // Now try to set and modify the value for a very long key. + Assert.throws(() => Telemetry.keyedScalarAdd(KEYED_UINT_SCALAR, LONG_KEY_STRING, 10), + /NS_ERROR_ILLEGAL_VALUE/, + "Using keys longer than 70 characters must throw."); + Assert.throws(() => Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, LONG_KEY_STRING, 1), + /NS_ERROR_ILLEGAL_VALUE/, + "Using keys longer than 70 characters must throw."); + Assert.throws(() => Telemetry.keyedScalarSetMaximum(KEYED_UINT_SCALAR, LONG_KEY_STRING, 10), + /NS_ERROR_ILLEGAL_VALUE/, + "Using keys longer than 70 characters must throw."); + + // Make sure the key with the right length contains the expected value. + let keyedScalars = + Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + Assert.equal(Object.keys(keyedScalars[KEYED_UINT_SCALAR]).length, 1, + "The keyed scalar must contain exactly 1 key."); + Assert.ok(NORMAL_KEY in keyedScalars[KEYED_UINT_SCALAR], + "The keyed scalar must contain the expected key."); + Assert.equal(keyedScalars[KEYED_UINT_SCALAR][NORMAL_KEY], 1, + "The key must contain the expected value."); + Assert.ok(!(LONG_KEY_STRING in keyedScalars[KEYED_UINT_SCALAR]), + "The data for the long key should not have been recorded."); +}); + +add_task(function* test_keyed_max_keys() { + Telemetry.clearScalars(); + + // Generate the names for the first 100 keys. + let keyNamesSet = new Set(); + for (let k = 0; k < 100; k++) { + keyNamesSet.add("key_" + k); + } + + // Add 100 keys to an histogram and set their initial value. + let valueToSet = 0; + keyNamesSet.forEach(keyName => { + Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, keyName, valueToSet++); + }); + + // Perform some operations on the 101th key. This should throw, as + // we're not allowed to have more than 100 keys. + const LAST_KEY_NAME = "overflowing_key"; + Assert.throws(() => Telemetry.keyedScalarAdd(KEYED_UINT_SCALAR, LAST_KEY_NAME, 10), + /NS_ERROR_FAILURE/, + "Using more than 100 keys must throw."); + Assert.throws(() => Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, LAST_KEY_NAME, 1), + /NS_ERROR_FAILURE/, + "Using more than 100 keys must throw."); + Assert.throws(() => Telemetry.keyedScalarSetMaximum(KEYED_UINT_SCALAR, LAST_KEY_NAME, 10), + /NS_ERROR_FAILURE/, + "Using more than 100 keys must throw."); + + // Make sure all the keys except the last one are available and have the correct + // values. + let keyedScalars = + Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + + // Check that the keyed scalar only contain the first 100 keys. + const reportedKeysSet = new Set(Object.keys(keyedScalars[KEYED_UINT_SCALAR])); + Assert.ok([...keyNamesSet].filter(x => reportedKeysSet.has(x)) && + [...reportedKeysSet].filter(x => keyNamesSet.has(x)), + "The keyed scalar must contain all the 100 keys, and drop the others."); + + // Check that all the keys recorded the expected values. + let expectedValue = 0; + keyNamesSet.forEach(keyName => { + Assert.equal(keyedScalars[KEYED_UINT_SCALAR][keyName], expectedValue++, + "The key must contain the expected value."); + }); +}); diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js b/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js new file mode 100644 index 000000000..88ff8cf44 --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js @@ -0,0 +1,427 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +*/ + +// This tests the public Telemetry API for submitting pings. + +"use strict"; + +Cu.import("resource://gre/modules/TelemetryController.jsm", this); +Cu.import("resource://gre/modules/TelemetrySession.jsm", this); +Cu.import("resource://gre/modules/TelemetrySend.jsm", this); +Cu.import("resource://gre/modules/TelemetryUtils.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/Preferences.jsm", this); +Cu.import("resource://gre/modules/osfile.jsm", this); + +const PREF_TELEMETRY_SERVER = "toolkit.telemetry.server"; + +const MS_IN_A_MINUTE = 60 * 1000; + +function countPingTypes(pings) { + let countByType = new Map(); + for (let p of pings) { + countByType.set(p.type, 1 + (countByType.get(p.type) || 0)); + } + return countByType; +} + +function setPingLastModified(id, timestamp) { + const path = OS.Path.join(TelemetryStorage.pingDirectoryPath, id); + return OS.File.setDates(path, null, timestamp); +} + +// Mock out the send timer activity. +function waitForTimer() { + return new Promise(resolve => { + fakePingSendTimer((callback, timeout) => { + resolve([callback, timeout]); + }, () => {}); + }); +} + +// Allow easy faking of readable ping ids. +// This helps with debugging issues with e.g. ordering in the send logic. +function fakePingId(type, number) { + const HEAD = "93bd0011-2c8f-4e1c-bee0-"; + const TAIL = "000000000000"; + const N = String(number); + const id = HEAD + type + TAIL.slice(type.length, - N.length) + N; + fakeGeneratePingId(() => id); + return id; +} + +var checkPingsSaved = Task.async(function* (pingIds) { + let allFound = true; + for (let id of pingIds) { + const path = OS.Path.join(TelemetryStorage.pingDirectoryPath, id); + let exists = false; + try { + exists = yield OS.File.exists(path); + } catch (ex) {} + + if (!exists) { + dump("checkPingsSaved - failed to find ping: " + path + "\n"); + allFound = false; + } + } + + return allFound; +}); + +function histogramValueCount(h) { + return h.counts.reduce((a, b) => a + b); +} + +add_task(function* test_setup() { + // Trigger a proper telemetry init. + do_get_profile(true); + // Make sure we don't generate unexpected pings due to pref changes. + yield setEmptyPrefWatchlist(); + Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true); +}); + +// Test the ping sending logic. +add_task(function* test_sendPendingPings() { + const TYPE_PREFIX = "test-sendPendingPings-"; + const TEST_TYPE_A = TYPE_PREFIX + "A"; + const TEST_TYPE_B = TYPE_PREFIX + "B"; + + const TYPE_A_COUNT = 20; + const TYPE_B_COUNT = 5; + + let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS"); + let histSendTimeSuccess = Telemetry.getHistogramById("TELEMETRY_SEND_SUCCESS"); + let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE"); + histSuccess.clear(); + histSendTimeSuccess.clear(); + histSendTimeFail.clear(); + + // Fake a current date. + let now = TelemetryUtils.truncateToDays(new Date()); + now = fakeNow(futureDate(now, 10 * 60 * MS_IN_A_MINUTE)); + + // Enable test-mode for TelemetrySend, otherwise we won't store pending pings + // before the module is fully initialized later. + TelemetrySend.setTestModeEnabled(true); + + // Submit some pings without the server and telemetry started yet. + for (let i = 0; i < TYPE_A_COUNT; ++i) { + fakePingId("a", i); + const id = yield TelemetryController.submitExternalPing(TEST_TYPE_A, {}); + yield setPingLastModified(id, now.getTime() + (i * 1000)); + } + + Assert.equal(TelemetrySend.pendingPingCount, TYPE_A_COUNT, + "Should have correct pending ping count"); + + // Submit some more pings of a different type. + now = fakeNow(futureDate(now, 5 * MS_IN_A_MINUTE)); + for (let i = 0; i < TYPE_B_COUNT; ++i) { + fakePingId("b", i); + const id = yield TelemetryController.submitExternalPing(TEST_TYPE_B, {}); + yield setPingLastModified(id, now.getTime() + (i * 1000)); + } + + Assert.equal(TelemetrySend.pendingPingCount, TYPE_A_COUNT + TYPE_B_COUNT, + "Should have correct pending ping count"); + + Assert.deepEqual(histSuccess.snapshot().counts, [0, 0, 0], + "Should not have recorded any sending in histograms yet."); + Assert.equal(histSendTimeSuccess.snapshot().sum, 0, + "Should not have recorded any sending in histograms yet."); + Assert.equal(histSendTimeFail.snapshot().sum, 0, + "Should not have recorded any sending in histograms yet."); + + // Now enable sending to the ping server. + now = fakeNow(futureDate(now, MS_IN_A_MINUTE)); + PingServer.start(); + Preferences.set(PREF_TELEMETRY_SERVER, "http://localhost:" + PingServer.port); + + let timerPromise = waitForTimer(); + yield TelemetryController.testReset(); + let [pingSendTimerCallback, pingSendTimeout] = yield timerPromise; + Assert.ok(!!pingSendTimerCallback, "Should have a timer callback"); + + // We should have received 10 pings from the first send batch: + // 5 of type B and 5 of type A, as sending is newest-first. + // The other pings should be delayed by the 10-pings-per-minute limit. + let pings = yield PingServer.promiseNextPings(10); + Assert.equal(TelemetrySend.pendingPingCount, TYPE_A_COUNT - 5, + "Should have correct pending ping count"); + PingServer.registerPingHandler(() => Assert.ok(false, "Should not have received any pings now")); + let countByType = countPingTypes(pings); + + Assert.equal(countByType.get(TEST_TYPE_B), TYPE_B_COUNT, + "Should have received the correct amount of type B pings"); + Assert.equal(countByType.get(TEST_TYPE_A), 10 - TYPE_B_COUNT, + "Should have received the correct amount of type A pings"); + + Assert.deepEqual(histSuccess.snapshot().counts, [0, 10, 0], + "Should have recorded sending success in histograms."); + Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 10, + "Should have recorded successful send times in histograms."); + Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0, + "Should not have recorded any failed sending in histograms yet."); + + // As we hit the ping send limit and still have pending pings, a send tick should + // be scheduled in a minute. + Assert.ok(!!pingSendTimerCallback, "Timer callback should be set"); + Assert.equal(pingSendTimeout, MS_IN_A_MINUTE, "Send tick timeout should be correct"); + + // Trigger the next tick - we should receive the next 10 type A pings. + PingServer.resetPingHandler(); + now = fakeNow(futureDate(now, pingSendTimeout)); + timerPromise = waitForTimer(); + pingSendTimerCallback(); + [pingSendTimerCallback, pingSendTimeout] = yield timerPromise; + + pings = yield PingServer.promiseNextPings(10); + PingServer.registerPingHandler(() => Assert.ok(false, "Should not have received any pings now")); + countByType = countPingTypes(pings); + + Assert.equal(countByType.get(TEST_TYPE_A), 10, "Should have received the correct amount of type A pings"); + + // We hit the ping send limit again and still have pending pings, a send tick should + // be scheduled in a minute. + Assert.equal(pingSendTimeout, MS_IN_A_MINUTE, "Send tick timeout should be correct"); + + // Trigger the next tick - we should receive the remaining type A pings. + PingServer.resetPingHandler(); + now = fakeNow(futureDate(now, pingSendTimeout)); + yield pingSendTimerCallback(); + + pings = yield PingServer.promiseNextPings(5); + PingServer.registerPingHandler(() => Assert.ok(false, "Should not have received any pings now")); + countByType = countPingTypes(pings); + + Assert.equal(countByType.get(TEST_TYPE_A), 5, "Should have received the correct amount of type A pings"); + + yield TelemetrySend.testWaitOnOutgoingPings(); + PingServer.resetPingHandler(); +}); + +add_task(function* test_sendDateHeader() { + fakeNow(new Date(Date.UTC(2011, 1, 1, 11, 0, 0))); + yield TelemetrySend.reset(); + + let pingId = yield TelemetryController.submitExternalPing("test-send-date-header", {}); + let req = yield PingServer.promiseNextRequest(); + let ping = decodeRequestPayload(req); + Assert.equal(req.getHeader("Date"), "Tue, 01 Feb 2011 11:00:00 GMT", + "Telemetry should send the correct Date header with requests."); + Assert.equal(ping.id, pingId, "Should have received the correct ping id."); +}); + +// Test the backoff timeout behavior after send failures. +add_task(function* test_backoffTimeout() { + const TYPE_PREFIX = "test-backoffTimeout-"; + const TEST_TYPE_C = TYPE_PREFIX + "C"; + const TEST_TYPE_D = TYPE_PREFIX + "D"; + const TEST_TYPE_E = TYPE_PREFIX + "E"; + + let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS"); + let histSendTimeSuccess = Telemetry.getHistogramById("TELEMETRY_SEND_SUCCESS"); + let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE"); + + // Failing a ping send now should trigger backoff behavior. + let now = fakeNow(2010, 1, 1, 11, 0, 0); + yield TelemetrySend.reset(); + PingServer.stop(); + + histSuccess.clear(); + histSendTimeSuccess.clear(); + histSendTimeFail.clear(); + + fakePingId("c", 0); + now = fakeNow(futureDate(now, MS_IN_A_MINUTE)); + let sendAttempts = 0; + let timerPromise = waitForTimer(); + yield TelemetryController.submitExternalPing(TEST_TYPE_C, {}); + let [pingSendTimerCallback, pingSendTimeout] = yield timerPromise; + Assert.equal(TelemetrySend.pendingPingCount, 1, "Should have one pending ping."); + ++sendAttempts; + + const MAX_BACKOFF_TIMEOUT = 120 * MS_IN_A_MINUTE; + for (let timeout = 2 * MS_IN_A_MINUTE; timeout <= MAX_BACKOFF_TIMEOUT; timeout *= 2) { + Assert.ok(!!pingSendTimerCallback, "Should have received a timer callback"); + Assert.equal(pingSendTimeout, timeout, "Send tick timeout should be correct"); + + let callback = pingSendTimerCallback; + now = fakeNow(futureDate(now, pingSendTimeout)); + timerPromise = waitForTimer(); + yield callback(); + [pingSendTimerCallback, pingSendTimeout] = yield timerPromise; + ++sendAttempts; + } + + timerPromise = waitForTimer(); + yield pingSendTimerCallback(); + [pingSendTimerCallback, pingSendTimeout] = yield timerPromise; + Assert.equal(pingSendTimeout, MAX_BACKOFF_TIMEOUT, "Tick timeout should be capped"); + ++sendAttempts; + + Assert.deepEqual(histSuccess.snapshot().counts, [sendAttempts, 0, 0], + "Should have recorded sending failure in histograms."); + Assert.equal(histSendTimeSuccess.snapshot().sum, 0, + "Should not have recorded any sending success in histograms yet."); + Assert.greater(histSendTimeFail.snapshot().sum, 0, + "Should have recorded send failure times in histograms."); + Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), sendAttempts, + "Should have recorded send failure times in histograms."); + + // Submitting a new ping should reset the backoff behavior. + fakePingId("d", 0); + now = fakeNow(futureDate(now, MS_IN_A_MINUTE)); + timerPromise = waitForTimer(); + yield TelemetryController.submitExternalPing(TEST_TYPE_D, {}); + [pingSendTimerCallback, pingSendTimeout] = yield timerPromise; + Assert.equal(pingSendTimeout, 2 * MS_IN_A_MINUTE, "Send tick timeout should be correct"); + sendAttempts += 2; + + // With the server running again, we should send out the pending pings immediately + // when a new ping is submitted. + PingServer.start(); + TelemetrySend.setServer("http://localhost:" + PingServer.port); + fakePingId("e", 0); + now = fakeNow(futureDate(now, MS_IN_A_MINUTE)); + timerPromise = waitForTimer(); + yield TelemetryController.submitExternalPing(TEST_TYPE_E, {}); + + let pings = yield PingServer.promiseNextPings(3); + let countByType = countPingTypes(pings); + + Assert.equal(countByType.get(TEST_TYPE_C), 1, "Should have received the correct amount of type C pings"); + Assert.equal(countByType.get(TEST_TYPE_D), 1, "Should have received the correct amount of type D pings"); + Assert.equal(countByType.get(TEST_TYPE_E), 1, "Should have received the correct amount of type E pings"); + + yield TelemetrySend.testWaitOnOutgoingPings(); + Assert.equal(TelemetrySend.pendingPingCount, 0, "Should have no pending pings left"); + + Assert.deepEqual(histSuccess.snapshot().counts, [sendAttempts, 3, 0], + "Should have recorded sending failure in histograms."); + Assert.greater(histSendTimeSuccess.snapshot().sum, 0, + "Should have recorded sending success in histograms."); + Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 3, + "Should have recorded sending success in histograms."); + Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), sendAttempts, + "Should have recorded send failure times in histograms."); +}); + +add_task(function* test_discardBigPings() { + const TEST_PING_TYPE = "test-ping-type"; + + let histSizeExceeded = Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_SEND"); + let histDiscardedSize = Telemetry.getHistogramById("TELEMETRY_DISCARDED_SEND_PINGS_SIZE_MB"); + let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS"); + let histSendTimeSuccess = Telemetry.getHistogramById("TELEMETRY_SEND_SUCCESS"); + let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE"); + for (let h of [histSizeExceeded, histDiscardedSize, histSuccess, histSendTimeSuccess, histSendTimeFail]) { + h.clear(); + } + + // Generate a 2MB string and create an oversized payload. + const OVERSIZED_PAYLOAD = {"data": generateRandomString(2 * 1024 * 1024)}; + + // Reset the histograms. + Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_SEND").clear(); + Telemetry.getHistogramById("TELEMETRY_DISCARDED_SEND_PINGS_SIZE_MB").clear(); + + // Submit a ping of a normal size and check that we don't count it in the histogram. + yield TelemetryController.submitExternalPing(TEST_PING_TYPE, { test: "test" }); + yield TelemetrySend.testWaitOnOutgoingPings(); + + Assert.equal(histSizeExceeded.snapshot().sum, 0, "Telemetry must report no oversized ping submitted."); + Assert.equal(histDiscardedSize.snapshot().sum, 0, "Telemetry must report no oversized pings."); + Assert.deepEqual(histSuccess.snapshot().counts, [0, 1, 0], "Should have recorded sending success."); + Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 1, "Should have recorded send success time."); + Assert.greater(histSendTimeSuccess.snapshot().sum, 0, "Should have recorded send success time."); + Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0, "Should not have recorded send failure time."); + + // Submit an oversized ping and check that it gets discarded. + yield TelemetryController.submitExternalPing(TEST_PING_TYPE, OVERSIZED_PAYLOAD); + yield TelemetrySend.testWaitOnOutgoingPings(); + + Assert.equal(histSizeExceeded.snapshot().sum, 1, "Telemetry must report 1 oversized ping submitted."); + Assert.equal(histDiscardedSize.snapshot().counts[2], 1, "Telemetry must report a 2MB, oversized, ping submitted."); + Assert.deepEqual(histSuccess.snapshot().counts, [0, 1, 0], "Should have recorded sending success."); + Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 1, "Should have recorded send success time."); + Assert.greater(histSendTimeSuccess.snapshot().sum, 0, "Should have recorded send success time."); + Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0, "Should not have recorded send failure time."); +}); + +add_task(function* test_evictedOnServerErrors() { + const TEST_TYPE = "test-evicted"; + + yield TelemetrySend.reset(); + + let histEvicted = Telemetry.getHistogramById("TELEMETRY_PING_EVICTED_FOR_SERVER_ERRORS"); + let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS"); + let histSendTimeSuccess = Telemetry.getHistogramById("TELEMETRY_SEND_SUCCESS"); + let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE"); + for (let h of [histEvicted, histSuccess, histSendTimeSuccess, histSendTimeFail]) { + h.clear(); + } + + // Write a custom ping handler which will return 403. This will trigger ping eviction + // on client side. + PingServer.registerPingHandler((req, res) => { + res.setStatusLine(null, 403, "Forbidden"); + res.processAsync(); + res.finish(); + }); + + // Clear the histogram and submit a ping. + let pingId = yield TelemetryController.submitExternalPing(TEST_TYPE, {}); + yield TelemetrySend.testWaitOnOutgoingPings(); + + Assert.equal(histEvicted.snapshot().sum, 1, + "Telemetry must report a ping evicted due to server errors"); + Assert.deepEqual(histSuccess.snapshot().counts, [0, 1, 0]); + Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 1); + Assert.greater(histSendTimeSuccess.snapshot().sum, 0); + Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0); + + // The ping should not be persisted. + yield Assert.rejects(TelemetryStorage.loadPendingPing(pingId), "The ping must not be persisted."); + + // Reset the ping handler and submit a new ping. + PingServer.resetPingHandler(); + pingId = yield TelemetryController.submitExternalPing(TEST_TYPE, {}); + + let ping = yield PingServer.promiseNextPings(1); + Assert.equal(ping[0].id, pingId, "The correct ping must be received"); + + // We should not have updated the error histogram. + yield TelemetrySend.testWaitOnOutgoingPings(); + Assert.equal(histEvicted.snapshot().sum, 1, "Telemetry must report only one ping evicted due to server errors"); + Assert.deepEqual(histSuccess.snapshot().counts, [0, 2, 0]); + Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 2); + Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0); +}); + +// Test that the current, non-persisted pending pings are properly saved on shutdown. +add_task(function* test_persistCurrentPingsOnShutdown() { + const TEST_TYPE = "test-persistCurrentPingsOnShutdown"; + const PING_COUNT = 5; + yield TelemetrySend.reset(); + PingServer.stop(); + Assert.equal(TelemetrySend.pendingPingCount, 0, "Should have no pending pings yet"); + + // Submit new pings that shouldn't be persisted yet. + let ids = []; + for (let i=0; i<5; ++i) { + ids.push(fakePingId("f", i)); + TelemetryController.submitExternalPing(TEST_TYPE, {}); + } + + Assert.equal(TelemetrySend.pendingPingCount, PING_COUNT, "Should have the correct pending ping count"); + + // Triggering a shutdown should persist the pings. + yield TelemetrySend.shutdown(); + Assert.ok((yield checkPingsSaved(ids)), "All pending pings should have been persisted"); + + // After a restart the pings should have been found when scanning. + yield TelemetrySend.reset(); + Assert.equal(TelemetrySend.pendingPingCount, PING_COUNT, "Should have the correct pending ping count"); +}); diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js b/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js new file mode 100644 index 000000000..221b6bcab --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js @@ -0,0 +1,547 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ + +/** + * This test case populates the profile with some fake stored + * pings, and checks that pending pings are immediatlely sent + * after delayed init. + */ + +"use strict" + +Cu.import("resource://gre/modules/osfile.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/Promise.jsm", this); +Cu.import("resource://gre/modules/TelemetryStorage.jsm", this); +Cu.import("resource://gre/modules/TelemetryController.jsm", this); +Cu.import("resource://gre/modules/TelemetrySend.jsm", this); +Cu.import("resource://gre/modules/Task.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +var {OS: {File, Path, Constants}} = Cu.import("resource://gre/modules/osfile.jsm", {}); + +// We increment TelemetryStorage's MAX_PING_FILE_AGE and +// OVERDUE_PING_FILE_AGE by 1 minute so that our test pings exceed +// those points in time, even taking into account file system imprecision. +const ONE_MINUTE_MS = 60 * 1000; +const OVERDUE_PING_FILE_AGE = TelemetrySend.OVERDUE_PING_FILE_AGE + ONE_MINUTE_MS; + +const PING_SAVE_FOLDER = "saved-telemetry-pings"; +const PING_TIMEOUT_LENGTH = 5000; +const OVERDUE_PINGS = 6; +const OLD_FORMAT_PINGS = 4; +const RECENT_PINGS = 4; + +const TOTAL_EXPECTED_PINGS = OVERDUE_PINGS + RECENT_PINGS + OLD_FORMAT_PINGS; + +const PREF_FHR_UPLOAD = "datareporting.healthreport.uploadEnabled"; + +var gCreatedPings = 0; +var gSeenPings = 0; + +/** + * Creates some Telemetry pings for the and saves them to disk. Each ping gets a + * unique ID based on an incrementor. + * + * @param {Array} aPingInfos An array of ping type objects. Each entry must be an + * object containing a "num" field for the number of pings to create and + * an "age" field. The latter representing the age in milliseconds to offset + * from now. A value of 10 would make the ping 10ms older than now, for + * example. + * @returns Promise + * @resolve an Array with the created pings ids. + */ +var createSavedPings = Task.async(function* (aPingInfos) { + let pingIds = []; + let now = Date.now(); + + for (let type in aPingInfos) { + let num = aPingInfos[type].num; + let age = now - (aPingInfos[type].age || 0); + for (let i = 0; i < num; ++i) { + let pingId = yield TelemetryController.addPendingPing("test-ping", {}, { overwrite: true }); + if (aPingInfos[type].age) { + // savePing writes to the file synchronously, so we're good to + // modify the lastModifedTime now. + let filePath = getSavePathForPingId(pingId); + yield File.setDates(filePath, null, age); + } + gCreatedPings++; + pingIds.push(pingId); + } + } + + return pingIds; +}); + +/** + * Deletes locally saved pings if they exist. + * + * @param aPingIds an Array of ping ids to delete. + * @returns Promise + */ +var clearPings = Task.async(function* (aPingIds) { + for (let pingId of aPingIds) { + yield TelemetryStorage.removePendingPing(pingId); + } +}); + +/** + * Fakes the pending pings storage quota. + * @param {Integer} aPendingQuota The new quota, in bytes. + */ +function fakePendingPingsQuota(aPendingQuota) { + let storage = Cu.import("resource://gre/modules/TelemetryStorage.jsm"); + storage.Policy.getPendingPingsQuota = () => aPendingQuota; +} + +/** + * Returns a handle for the file that a ping should be + * stored in locally. + * + * @returns path + */ +function getSavePathForPingId(aPingId) { + return Path.join(Constants.Path.profileDir, PING_SAVE_FOLDER, aPingId); +} + +/** + * Check if the number of Telemetry pings received by the HttpServer is not equal + * to aExpectedNum. + * + * @param aExpectedNum the number of pings we expect to receive. + */ +function assertReceivedPings(aExpectedNum) { + do_check_eq(gSeenPings, aExpectedNum); +} + +/** + * Throws if any pings with the id in aPingIds is saved locally. + * + * @param aPingIds an Array of pings ids to check. + * @returns Promise + */ +var assertNotSaved = Task.async(function* (aPingIds) { + let saved = 0; + for (let id of aPingIds) { + let filePath = getSavePathForPingId(id); + if (yield File.exists(filePath)) { + saved++; + } + } + if (saved > 0) { + do_throw("Found " + saved + " unexpected saved pings."); + } +}); + +/** + * Our handler function for the HttpServer that simply + * increments the gSeenPings global when it successfully + * receives and decodes a Telemetry payload. + * + * @param aRequest the HTTP request sent from HttpServer. + */ +function pingHandler(aRequest) { + gSeenPings++; +} + +add_task(function* test_setup() { + PingServer.start(); + PingServer.registerPingHandler(pingHandler); + do_get_profile(); + loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); + // Make sure we don't generate unexpected pings due to pref changes. + yield setEmptyPrefWatchlist(); + + Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true); + Services.prefs.setCharPref(TelemetryController.Constants.PREF_SERVER, + "http://localhost:" + PingServer.port); +}); + +/** + * Setup the tests by making sure the ping storage directory is available, otherwise + * |TelemetryController.testSaveDirectoryToFile| could fail. + */ +add_task(function* setupEnvironment() { + // The following tests assume this pref to be true by default. + Services.prefs.setBoolPref(PREF_FHR_UPLOAD, true); + + yield TelemetryController.testSetup(); + + let directory = TelemetryStorage.pingDirectoryPath; + yield File.makeDir(directory, { ignoreExisting: true, unixMode: OS.Constants.S_IRWXU }); + + yield TelemetryStorage.testClearPendingPings(); +}); + +/** + * Test that really recent pings are sent on Telemetry initialization. + */ +add_task(function* test_recent_pings_sent() { + let pingTypes = [{ num: RECENT_PINGS }]; + yield createSavedPings(pingTypes); + + yield TelemetryController.testReset(); + yield TelemetrySend.testWaitOnOutgoingPings(); + assertReceivedPings(RECENT_PINGS); + + yield TelemetryStorage.testClearPendingPings(); +}); + +/** + * Create an overdue ping in the old format and try to send it. + */ +add_task(function* test_overdue_old_format() { + // A test ping in the old, standard format. + const PING_OLD_FORMAT = { + slug: "1234567abcd", + reason: "test-ping", + payload: { + info: { + reason: "test-ping", + OS: "XPCShell", + appID: "SomeId", + appVersion: "1.0", + appName: "XPCShell", + appBuildID: "123456789", + appUpdateChannel: "Test", + platformBuildID: "987654321", + }, + }, + }; + + // A ping with no info section, but with a slug. + const PING_NO_INFO = { + slug: "1234-no-info-ping", + reason: "test-ping", + payload: {} + }; + + // A ping with no payload. + const PING_NO_PAYLOAD = { + slug: "5678-no-payload", + reason: "test-ping", + }; + + // A ping with no info and no slug. + const PING_NO_SLUG = { + reason: "test-ping", + payload: {} + }; + + const PING_FILES_PATHS = [ + getSavePathForPingId(PING_OLD_FORMAT.slug), + getSavePathForPingId(PING_NO_INFO.slug), + getSavePathForPingId(PING_NO_PAYLOAD.slug), + getSavePathForPingId("no-slug-file"), + ]; + + // Write the ping to file and make it overdue. + yield TelemetryStorage.savePing(PING_OLD_FORMAT, true); + yield TelemetryStorage.savePing(PING_NO_INFO, true); + yield TelemetryStorage.savePing(PING_NO_PAYLOAD, true); + yield TelemetryStorage.savePingToFile(PING_NO_SLUG, PING_FILES_PATHS[3], true); + + for (let f in PING_FILES_PATHS) { + yield File.setDates(PING_FILES_PATHS[f], null, Date.now() - OVERDUE_PING_FILE_AGE); + } + + gSeenPings = 0; + yield TelemetryController.testReset(); + yield TelemetrySend.testWaitOnOutgoingPings(); + assertReceivedPings(OLD_FORMAT_PINGS); + + // |TelemetryStorage.cleanup| doesn't know how to remove a ping with no slug or id, + // so remove it manually so that the next test doesn't fail. + yield OS.File.remove(PING_FILES_PATHS[3]); + + yield TelemetryStorage.testClearPendingPings(); +}); + +add_task(function* test_corrupted_pending_pings() { + const TEST_TYPE = "test_corrupted"; + + Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").clear(); + Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").clear(); + + // Save a pending ping and get its id. + let pendingPingId = yield TelemetryController.addPendingPing(TEST_TYPE, {}, {}); + + // Try to load it: there should be no error. + yield TelemetryStorage.loadPendingPing(pendingPingId); + + let h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").snapshot(); + Assert.equal(h.sum, 0, "Telemetry must not report a pending ping load failure"); + h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").snapshot(); + Assert.equal(h.sum, 0, "Telemetry must not report a pending ping parse failure"); + + // Delete it from the disk, so that its id will be kept in the cache but it will + // fail loading the file. + yield OS.File.remove(getSavePathForPingId(pendingPingId)); + + // Try to load a pending ping which isn't there anymore. + yield Assert.rejects(TelemetryStorage.loadPendingPing(pendingPingId), + "Telemetry must fail loading a ping which isn't there"); + + h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").snapshot(); + Assert.equal(h.sum, 1, "Telemetry must report a pending ping load failure"); + h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").snapshot(); + Assert.equal(h.sum, 0, "Telemetry must not report a pending ping parse failure"); + + // Save a new ping, so that it gets in the pending pings cache. + pendingPingId = yield TelemetryController.addPendingPing(TEST_TYPE, {}, {}); + // Overwrite it with a corrupted JSON file and then try to load it. + const INVALID_JSON = "{ invalid,JSON { {1}"; + yield OS.File.writeAtomic(getSavePathForPingId(pendingPingId), INVALID_JSON, { encoding: "utf-8" }); + + // Try to load the ping with the corrupted JSON content. + yield Assert.rejects(TelemetryStorage.loadPendingPing(pendingPingId), + "Telemetry must fail loading a corrupted ping"); + + h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").snapshot(); + Assert.equal(h.sum, 1, "Telemetry must report a pending ping load failure"); + h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").snapshot(); + Assert.equal(h.sum, 1, "Telemetry must report a pending ping parse failure"); + + let exists = yield OS.File.exists(getSavePathForPingId(pendingPingId)); + Assert.ok(!exists, "The unparseable ping should have been removed"); + + yield TelemetryStorage.testClearPendingPings(); +}); + +/** + * Create some recent and overdue pings and verify that they get sent. + */ +add_task(function* test_overdue_pings_trigger_send() { + let pingTypes = [ + { num: RECENT_PINGS }, + { num: OVERDUE_PINGS, age: OVERDUE_PING_FILE_AGE }, + ]; + let pings = yield createSavedPings(pingTypes); + let recentPings = pings.slice(0, RECENT_PINGS); + let overduePings = pings.slice(-OVERDUE_PINGS); + + yield TelemetryController.testReset(); + yield TelemetrySend.testWaitOnOutgoingPings(); + assertReceivedPings(TOTAL_EXPECTED_PINGS); + + yield assertNotSaved(recentPings); + yield assertNotSaved(overduePings); + + Assert.equal(TelemetrySend.overduePingsCount, overduePings.length, + "Should have tracked the correct amount of overdue pings"); + + yield TelemetryStorage.testClearPendingPings(); +}); + +/** + * Create a ping in the old format, send it, and make sure the request URL contains + * the correct version query parameter. + */ +add_task(function* test_overdue_old_format() { + // A test ping in the old, standard format. + const PING_OLD_FORMAT = { + slug: "1234567abcd", + reason: "test-ping", + payload: { + info: { + reason: "test-ping", + OS: "XPCShell", + appID: "SomeId", + appVersion: "1.0", + appName: "XPCShell", + appBuildID: "123456789", + appUpdateChannel: "Test", + platformBuildID: "987654321", + }, + }, + }; + + const filePath = + Path.join(Constants.Path.profileDir, PING_SAVE_FOLDER, PING_OLD_FORMAT.slug); + + // Write the ping to file and make it overdue. + yield TelemetryStorage.savePing(PING_OLD_FORMAT, true); + yield File.setDates(filePath, null, Date.now() - OVERDUE_PING_FILE_AGE); + + let receivedPings = 0; + // Register a new prefix handler to validate the URL. + PingServer.registerPingHandler(request => { + // Check that we have a version query parameter in the URL. + Assert.notEqual(request.queryString, ""); + + // Make sure the version in the query string matches the old ping format version. + let params = request.queryString.split("&"); + Assert.ok(params.find(p => p == "v=1")); + + receivedPings++; + }); + + yield TelemetryController.testReset(); + yield TelemetrySend.testWaitOnOutgoingPings(); + Assert.equal(receivedPings, 1, "We must receive a ping in the old format."); + + yield TelemetryStorage.testClearPendingPings(); + PingServer.resetPingHandler(); +}); + +add_task(function* test_pendingPingsQuota() { + const PING_TYPE = "foo"; + + // Disable upload so pings don't get sent and removed from the pending pings directory. + Services.prefs.setBoolPref(PREF_FHR_UPLOAD, false); + + // Remove all the pending pings then startup and wait for the cleanup task to complete. + // There should be nothing to remove. + yield TelemetryStorage.testClearPendingPings(); + yield TelemetryController.testReset(); + yield TelemetrySend.testWaitOnOutgoingPings(); + yield TelemetryStorage.testPendingQuotaTaskPromise(); + + // Remove the pending deletion ping generated when flipping FHR upload off. + yield TelemetryStorage.testClearPendingPings(); + + let expectedPrunedPings = []; + let expectedNotPrunedPings = []; + + let checkPendingPings = Task.async(function*() { + // Check that the pruned pings are not on disk anymore. + for (let prunedPingId of expectedPrunedPings) { + yield Assert.rejects(TelemetryStorage.loadPendingPing(prunedPingId), + "Ping " + prunedPingId + " should have been pruned."); + const pingPath = getSavePathForPingId(prunedPingId); + Assert.ok(!(yield OS.File.exists(pingPath)), "The ping should not be on the disk anymore."); + } + + // Check that the expected pings are there. + for (let expectedPingId of expectedNotPrunedPings) { + Assert.ok((yield TelemetryStorage.loadPendingPing(expectedPingId)), + "Ping" + expectedPingId + " should be among the pending pings."); + } + }); + + let pendingPingsInfo = []; + let pingsSizeInBytes = 0; + + // Create 10 pings to test the pending pings quota. + for (let days = 1; days < 11; days++) { + const date = fakeNow(2010, 1, days, 1, 1, 0); + const pingId = yield TelemetryController.addPendingPing(PING_TYPE, {}, {}); + + // Find the size of the ping. + const pingFilePath = getSavePathForPingId(pingId); + const pingSize = (yield OS.File.stat(pingFilePath)).size; + // Add the info at the beginning of the array, so that most recent pings come first. + pendingPingsInfo.unshift({id: pingId, size: pingSize, timestamp: date.getTime() }); + + // Set the last modification date. + yield OS.File.setDates(pingFilePath, null, date.getTime()); + + // Add it to the pending ping directory size. + pingsSizeInBytes += pingSize; + } + + // We need to test the pending pings size before we hit the quota, otherwise a special + // value is recorded. + Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_SIZE_MB").clear(); + Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA").clear(); + Telemetry.getHistogramById("TELEMETRY_PENDING_EVICTING_OVER_QUOTA_MS").clear(); + + yield TelemetryController.testReset(); + yield TelemetryStorage.testPendingQuotaTaskPromise(); + + // Check that the correct values for quota probes are reported when no quota is hit. + let h = Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_SIZE_MB").snapshot(); + Assert.equal(h.sum, Math.round(pingsSizeInBytes / 1024 / 1024), + "Telemetry must report the correct pending pings directory size."); + h = Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA").snapshot(); + Assert.equal(h.sum, 0, "Telemetry must report 0 evictions if quota is not hit."); + h = Telemetry.getHistogramById("TELEMETRY_PENDING_EVICTING_OVER_QUOTA_MS").snapshot(); + Assert.equal(h.sum, 0, "Telemetry must report a null elapsed time if quota is not hit."); + + // Set the quota to 80% of the space. + const testQuotaInBytes = pingsSizeInBytes * 0.8; + fakePendingPingsQuota(testQuotaInBytes); + + // The storage prunes pending pings until we reach 90% of the requested storage quota. + // Based on that, find how many pings should be kept. + const safeQuotaSize = Math.round(testQuotaInBytes * 0.9); + let sizeInBytes = 0; + let pingsWithinQuota = []; + let pingsOutsideQuota = []; + + for (let pingInfo of pendingPingsInfo) { + sizeInBytes += pingInfo.size; + if (sizeInBytes >= safeQuotaSize) { + pingsOutsideQuota.push(pingInfo.id); + continue; + } + pingsWithinQuota.push(pingInfo.id); + } + + expectedNotPrunedPings = pingsWithinQuota; + expectedPrunedPings = pingsOutsideQuota; + + // Reset TelemetryController to start the pending pings cleanup. + yield TelemetryController.testReset(); + yield TelemetryStorage.testPendingQuotaTaskPromise(); + yield checkPendingPings(); + + h = Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA").snapshot(); + Assert.equal(h.sum, pingsOutsideQuota.length, + "Telemetry must correctly report the over quota pings evicted from the pending pings directory."); + h = Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_SIZE_MB").snapshot(); + Assert.equal(h.sum, 17, "Pending pings quota was hit, a special size must be reported."); + + // Trigger a cleanup again and make sure we're not removing anything. + yield TelemetryController.testReset(); + yield TelemetryStorage.testPendingQuotaTaskPromise(); + yield checkPendingPings(); + + const OVERSIZED_PING_ID = "9b21ec8f-f762-4d28-a2c1-44e1c4694f24"; + // Create a pending oversized ping. + 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: generateRandomString(2 * 1024 * 1024), + }; + yield TelemetryStorage.savePendingPing(OVERSIZED_PING); + + // Reset the histograms. + Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_PENDING").clear(); + Telemetry.getHistogramById("TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB").clear(); + + // Try to manually load the oversized ping. + yield Assert.rejects(TelemetryStorage.loadPendingPing(OVERSIZED_PING_ID), + "The oversized ping should have been pruned."); + Assert.ok(!(yield OS.File.exists(getSavePathForPingId(OVERSIZED_PING_ID))), + "The ping should not be on the disk anymore."); + + // Make sure we're correctly updating the related histograms. + h = Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_PENDING").snapshot(); + Assert.equal(h.sum, 1, "Telemetry must report 1 oversized ping in the pending pings directory."); + h = Telemetry.getHistogramById("TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB").snapshot(); + Assert.equal(h.counts[2], 1, "Telemetry must report a 2MB, oversized, ping."); + + // Save the ping again to check if it gets pruned when scanning the pings directory. + yield TelemetryStorage.savePendingPing(OVERSIZED_PING); + expectedPrunedPings.push(OVERSIZED_PING_ID); + + // Scan the pending pings directory. + yield TelemetryController.testReset(); + yield TelemetryStorage.testPendingQuotaTaskPromise(); + yield checkPendingPings(); + + // Make sure we're correctly updating the related histograms. + h = Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_PENDING").snapshot(); + Assert.equal(h.sum, 2, "Telemetry must report 1 oversized ping in the pending pings directory."); + h = Telemetry.getHistogramById("TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB").snapshot(); + Assert.equal(h.counts[2], 2, "Telemetry must report two 2MB, oversized, pings."); + + Services.prefs.setBoolPref(PREF_FHR_UPLOAD, true); +}); + +add_task(function* teardown() { + yield PingServer.stop(); +}); diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js new file mode 100644 index 000000000..698133162 --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js @@ -0,0 +1,2029 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +*/ +/* This testcase triggers two telemetry pings. + * + * Telemetry code keeps histograms of past telemetry pings. The first + * ping populates these histograms. One of those histograms is then + * checked in the second request. + */ + +Cu.import("resource://services-common/utils.js"); +Cu.import("resource://gre/modules/ClientID.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/LightweightThemeManager.jsm", this); +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://gre/modules/TelemetryController.jsm", this); +Cu.import("resource://gre/modules/TelemetrySession.jsm", this); +Cu.import("resource://gre/modules/TelemetryStorage.jsm", this); +Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", this); +Cu.import("resource://gre/modules/TelemetrySend.jsm", this); +Cu.import("resource://gre/modules/Task.jsm", this); +Cu.import("resource://gre/modules/Promise.jsm", this); +Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource://gre/modules/osfile.jsm", this); + +const PING_FORMAT_VERSION = 4; +const PING_TYPE_MAIN = "main"; +const PING_TYPE_SAVED_SESSION = "saved-session"; + +const REASON_ABORTED_SESSION = "aborted-session"; +const REASON_SAVED_SESSION = "saved-session"; +const REASON_SHUTDOWN = "shutdown"; +const REASON_TEST_PING = "test-ping"; +const REASON_DAILY = "daily"; +const REASON_ENVIRONMENT_CHANGE = "environment-change"; + +const PLATFORM_VERSION = "1.9.2"; +const APP_VERSION = "1"; +const APP_ID = "xpcshell@tests.mozilla.org"; +const APP_NAME = "XPCShell"; + +const IGNORE_HISTOGRAM_TO_CLONE = "MEMORY_HEAP_ALLOCATED"; +const IGNORE_CLONED_HISTOGRAM = "test::ignore_me_also"; +const ADDON_NAME = "Telemetry test addon"; +const ADDON_HISTOGRAM = "addon-histogram"; +// Add some unicode characters here to ensure that sending them works correctly. +const SHUTDOWN_TIME = 10000; +const FAILED_PROFILE_LOCK_ATTEMPTS = 2; + +// Constants from prio.h for nsIFileOutputStream.init +const PR_WRONLY = 0x2; +const PR_CREATE_FILE = 0x8; +const PR_TRUNCATE = 0x20; +const RW_OWNER = parseInt("0600", 8); + +const NUMBER_OF_THREADS_TO_LAUNCH = 30; +var gNumberOfThreadsLaunched = 0; + +const MS_IN_ONE_HOUR = 60 * 60 * 1000; +const MS_IN_ONE_DAY = 24 * MS_IN_ONE_HOUR; + +const PREF_BRANCH = "toolkit.telemetry."; +const PREF_SERVER = PREF_BRANCH + "server"; +const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled"; + +const DATAREPORTING_DIR = "datareporting"; +const ABORTED_PING_FILE_NAME = "aborted-session-ping"; +const ABORTED_SESSION_UPDATE_INTERVAL_MS = 5 * 60 * 1000; + +XPCOMUtils.defineLazyGetter(this, "DATAREPORTING_PATH", function() { + return OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIR); +}); + +var gClientID = null; +var gMonotonicNow = 0; + +function generateUUID() { + let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString(); + // strip {} + return str.substring(1, str.length - 1); +} + +function truncateDateToDays(date) { + return new Date(date.getFullYear(), + date.getMonth(), + date.getDate(), + 0, 0, 0, 0); +} + +function sendPing() { + TelemetrySession.gatherStartup(); + if (PingServer.started) { + TelemetrySend.setServer("http://localhost:" + PingServer.port); + return TelemetrySession.testPing(); + } + TelemetrySend.setServer("http://doesnotexist"); + return TelemetrySession.testPing(); +} + +function fakeGenerateUUID(sessionFunc, subsessionFunc) { + let session = Cu.import("resource://gre/modules/TelemetrySession.jsm"); + session.Policy.generateSessionUUID = sessionFunc; + session.Policy.generateSubsessionUUID = subsessionFunc; +} + +function fakeIdleNotification(topic) { + let session = Cu.import("resource://gre/modules/TelemetrySession.jsm"); + return session.TelemetryScheduler.observe(null, topic, null); +} + +function setupTestData() { + + Services.startup.interrupted = true; + Telemetry.registerAddonHistogram(ADDON_NAME, ADDON_HISTOGRAM, + Telemetry.HISTOGRAM_LINEAR, + 1, 5, 6); + let h1 = Telemetry.getAddonHistogram(ADDON_NAME, ADDON_HISTOGRAM); + h1.add(1); + let h2 = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT"); + h2.add(); + + let k1 = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT"); + k1.add("a"); + k1.add("a"); + k1.add("b"); +} + +function getSavedPingFile(basename) { + let tmpDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + let pingFile = tmpDir.clone(); + pingFile.append(basename); + if (pingFile.exists()) { + pingFile.remove(true); + } + do_register_cleanup(function () { + try { + pingFile.remove(true); + } catch (e) { + } + }); + return pingFile; +} + +function checkPingFormat(aPing, aType, aHasClientId, aHasEnvironment) { + const MANDATORY_PING_FIELDS = [ + "type", "id", "creationDate", "version", "application", "payload" + ]; + + const APPLICATION_TEST_DATA = { + buildId: gAppInfo.appBuildID, + name: APP_NAME, + version: APP_VERSION, + vendor: "Mozilla", + platformVersion: PLATFORM_VERSION, + xpcomAbi: "noarch-spidermonkey", + }; + + // Check that the ping contains all the mandatory fields. + for (let f of MANDATORY_PING_FIELDS) { + Assert.ok(f in aPing, f + "must be available."); + } + + Assert.equal(aPing.type, aType, "The ping must have the correct type."); + Assert.equal(aPing.version, PING_FORMAT_VERSION, "The ping must have the correct version."); + + // Test the application section. + for (let f in APPLICATION_TEST_DATA) { + Assert.equal(aPing.application[f], APPLICATION_TEST_DATA[f], + f + " must have the correct value."); + } + + // We can't check the values for channel and architecture. Just make + // sure they are in. + Assert.ok("architecture" in aPing.application, + "The application section must have an architecture field."); + Assert.ok("channel" in aPing.application, + "The application section must have a channel field."); + + // Check the clientId and environment fields, as needed. + Assert.equal("clientId" in aPing, aHasClientId); + Assert.equal("environment" in aPing, aHasEnvironment); +} + +function checkPayloadInfo(data) { + const ALLOWED_REASONS = [ + "environment-change", "shutdown", "daily", "saved-session", "test-ping" + ]; + let numberCheck = arg => { return (typeof arg == "number"); }; + let positiveNumberCheck = arg => { return numberCheck(arg) && (arg >= 0); }; + let stringCheck = arg => { return (typeof arg == "string") && (arg != ""); }; + let revisionCheck = arg => { + return (Services.appinfo.isOfficial) ? stringCheck(arg) : (typeof arg == "string"); + }; + let uuidCheck = arg => { + return UUID_REGEX.test(arg); + }; + let isoDateCheck = arg => { + // We expect use of this version of the ISO format: + // 2015-04-12T18:51:19.1+00:00 + const isoDateRegEx = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+[+-]\d{2}:\d{2}$/; + return stringCheck(arg) && !Number.isNaN(Date.parse(arg)) && + isoDateRegEx.test(arg); + }; + + const EXPECTED_INFO_FIELDS_TYPES = { + reason: stringCheck, + revision: revisionCheck, + timezoneOffset: numberCheck, + sessionId: uuidCheck, + subsessionId: uuidCheck, + // Special cases: previousSessionId and previousSubsessionId are null on first run. + previousSessionId: (arg) => { return (arg) ? uuidCheck(arg) : true; }, + previousSubsessionId: (arg) => { return (arg) ? uuidCheck(arg) : true; }, + subsessionCounter: positiveNumberCheck, + profileSubsessionCounter: positiveNumberCheck, + sessionStartDate: isoDateCheck, + subsessionStartDate: isoDateCheck, + subsessionLength: positiveNumberCheck, + }; + + for (let f in EXPECTED_INFO_FIELDS_TYPES) { + Assert.ok(f in data, f + " must be available."); + + let checkFunc = EXPECTED_INFO_FIELDS_TYPES[f]; + Assert.ok(checkFunc(data[f]), + f + " must have the correct type and valid data " + data[f]); + } + + // Previous buildId is not mandatory. + if (data.previousBuildId) { + Assert.ok(stringCheck(data.previousBuildId)); + } + + Assert.ok(ALLOWED_REASONS.find(r => r == data.reason), + "Payload must contain an allowed reason."); + + Assert.ok(Date.parse(data.subsessionStartDate) >= Date.parse(data.sessionStartDate)); + Assert.ok(data.profileSubsessionCounter >= data.subsessionCounter); + Assert.ok(data.timezoneOffset >= -12*60, "The timezone must be in a valid range."); + Assert.ok(data.timezoneOffset <= 12*60, "The timezone must be in a valid range."); +} + +function checkScalars(processes) { + // Check that the scalars section is available in the ping payload. + const parentProcess = processes.parent; + Assert.ok("scalars" in parentProcess, "The scalars section must be available in the parent process."); + Assert.ok("keyedScalars" in parentProcess, "The keyedScalars section must be available in the parent process."); + Assert.equal(typeof parentProcess.scalars, "object", "The scalars entry must be an object."); + Assert.equal(typeof parentProcess.keyedScalars, "object", "The keyedScalars entry must be an object."); + + let checkScalar = function(scalar) { + // Check if the value is of a supported type. + const valueType = typeof(scalar); + switch (valueType) { + case "string": + Assert.ok(scalar.length <= 50, + "String values can't have more than 50 characters"); + break; + case "number": + Assert.ok(scalar >= 0, + "We only support unsigned integer values in scalars."); + break; + case "boolean": + Assert.ok(true, + "Boolean scalar found."); + break; + default: + Assert.ok(false, + name + " contains an unsupported value type (" + valueType + ")"); + } + } + + // Check that we have valid scalar entries. + const scalars = parentProcess.scalars; + for (let name in scalars) { + Assert.equal(typeof name, "string", "Scalar names must be strings."); + checkScalar(scalar[name]); + } + + // Check that we have valid keyed scalar entries. + const keyedScalars = parentProcess.keyedScalars; + for (let name in keyedScalars) { + Assert.equal(typeof name, "string", "Scalar names must be strings."); + Assert.ok(Object.keys(keyedScalars[name]).length, + "The reported keyed scalars must contain at least 1 key."); + for (let key in keyedScalars[name]) { + Assert.equal(typeof key, "string", "Keyed scalar keys must be strings."); + Assert.ok(key.length <= 70, "Keyed scalar keys can't have more than 70 characters."); + checkScalar(scalar[name][key]); + } + } +} + +function checkEvents(processes) { + // Check that the events section is available in the ping payload. + const parent = processes.parent; + Assert.ok("events" in parent, "The events section must be available in the parent process."); + + // Check that the events section has the right format. + Assert.ok(Array.isArray(parent.events), "The events entry must be an array."); + for (let [ts, category, method, object, value, extra] of parent.events) { + Assert.equal(typeof(ts), "number", "Timestamp field should be a number."); + Assert.greaterOrEqual(ts, 0, "Timestamp should be >= 0."); + + Assert.equal(typeof(category), "string", "Category should have the right type."); + Assert.lessOrEqual(category.length, 100, "Category should have the right string length."); + + Assert.equal(typeof(method), "string", "Method should have the right type."); + Assert.lessOrEqual(method.length, 40, "Method should have the right string length."); + + Assert.equal(typeof(object), "string", "Object should have the right type."); + Assert.lessOrEqual(object.length, 40, "Object should have the right string length."); + + Assert.ok(value === null || typeof(value) === "string", + "Value should be null or a string."); + if (value) { + Assert.lessOrEqual(value.length, 100, "Value should have the right string length."); + } + + Assert.ok(extra === null || typeof(extra) === "object", + "Extra should be null or an object."); + if (extra) { + let keys = Object.keys(extra); + let keyTypes = keys.map(k => typeof(k)); + Assert.lessOrEqual(keys.length, 20, "Should not have too many extra keys."); + Assert.ok(keyTypes.every(t => t === "string"), + "All extra keys should be strings."); + Assert.ok(keys.every(k => k.length <= 20), + "All extra keys should have the right string length."); + + let values = Object.values(extra); + let valueTypes = values.map(v => typeof(v)); + Assert.ok(valueTypes.every(t => t === "string"), + "All extra values should be strings."); + Assert.ok(values.every(v => v.length <= 100), + "All extra values should have the right string length."); + } + } +} + +function checkPayload(payload, reason, successfulPings, savedPings) { + Assert.ok("info" in payload, "Payload must contain an info section."); + checkPayloadInfo(payload.info); + + Assert.ok(payload.simpleMeasurements.totalTime >= 0); + Assert.ok(payload.simpleMeasurements.uptime >= 0); + Assert.equal(payload.simpleMeasurements.startupInterrupted, 1); + Assert.equal(payload.simpleMeasurements.shutdownDuration, SHUTDOWN_TIME); + Assert.equal(payload.simpleMeasurements.savedPings, savedPings); + Assert.ok("maximalNumberOfConcurrentThreads" in payload.simpleMeasurements); + Assert.ok(payload.simpleMeasurements.maximalNumberOfConcurrentThreads >= gNumberOfThreadsLaunched); + + let activeTicks = payload.simpleMeasurements.activeTicks; + Assert.ok(activeTicks >= 0); + + Assert.equal(payload.simpleMeasurements.failedProfileLockCount, + FAILED_PROFILE_LOCK_ATTEMPTS); + let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile); + let failedProfileLocksFile = profileDirectory.clone(); + failedProfileLocksFile.append("Telemetry.FailedProfileLocks.txt"); + Assert.ok(!failedProfileLocksFile.exists()); + + + let isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes); + if (isWindows) { + Assert.ok(payload.simpleMeasurements.startupSessionRestoreReadBytes > 0); + Assert.ok(payload.simpleMeasurements.startupSessionRestoreWriteBytes > 0); + } + + const TELEMETRY_SEND_SUCCESS = "TELEMETRY_SEND_SUCCESS"; + const TELEMETRY_SUCCESS = "TELEMETRY_SUCCESS"; + const TELEMETRY_TEST_FLAG = "TELEMETRY_TEST_FLAG"; + const TELEMETRY_TEST_COUNT = "TELEMETRY_TEST_COUNT"; + const TELEMETRY_TEST_KEYED_FLAG = "TELEMETRY_TEST_KEYED_FLAG"; + const TELEMETRY_TEST_KEYED_COUNT = "TELEMETRY_TEST_KEYED_COUNT"; + + if (successfulPings > 0) { + Assert.ok(TELEMETRY_SEND_SUCCESS in payload.histograms); + } + Assert.ok(TELEMETRY_TEST_FLAG in payload.histograms); + Assert.ok(TELEMETRY_TEST_COUNT in payload.histograms); + + Assert.ok(!(IGNORE_CLONED_HISTOGRAM in payload.histograms)); + + // Flag histograms should automagically spring to life. + const expected_flag = { + range: [1, 2], + bucket_count: 3, + histogram_type: 3, + values: {0:1, 1:0}, + sum: 0 + }; + let flag = payload.histograms[TELEMETRY_TEST_FLAG]; + Assert.equal(uneval(flag), uneval(expected_flag)); + + // We should have a test count. + const expected_count = { + range: [1, 2], + bucket_count: 3, + histogram_type: 4, + values: {0:1, 1:0}, + sum: 1, + }; + let count = payload.histograms[TELEMETRY_TEST_COUNT]; + Assert.equal(uneval(count), uneval(expected_count)); + + // There should be one successful report from the previous telemetry ping. + if (successfulPings > 0) { + const expected_tc = { + range: [1, 2], + bucket_count: 3, + histogram_type: 2, + values: {0:2, 1:successfulPings, 2:0}, + sum: successfulPings + }; + let tc = payload.histograms[TELEMETRY_SUCCESS]; + Assert.equal(uneval(tc), uneval(expected_tc)); + } + + // The ping should include data from memory reporters. We can't check that + // this data is correct, because we can't control the values returned by the + // memory reporters. But we can at least check that the data is there. + // + // It's important to check for the presence of reporters with a mix of units, + // because TelemetryController has separate logic for each one. But we can't + // currently check UNITS_COUNT_CUMULATIVE or UNITS_PERCENTAGE because + // Telemetry doesn't touch a memory reporter with these units that's + // available on all platforms. + + Assert.ok('MEMORY_JS_GC_HEAP' in payload.histograms); // UNITS_BYTES + Assert.ok('MEMORY_JS_COMPARTMENTS_SYSTEM' in payload.histograms); // UNITS_COUNT + + // We should have included addon histograms. + Assert.ok("addonHistograms" in payload); + Assert.ok(ADDON_NAME in payload.addonHistograms); + Assert.ok(ADDON_HISTOGRAM in payload.addonHistograms[ADDON_NAME]); + + Assert.ok(("mainThread" in payload.slowSQL) && + ("otherThreads" in payload.slowSQL)); + + Assert.ok(("IceCandidatesStats" in payload.webrtc) && + ("webrtc" in payload.webrtc.IceCandidatesStats)); + + // Check keyed histogram payload. + + Assert.ok("keyedHistograms" in payload); + let keyedHistograms = payload.keyedHistograms; + Assert.ok(!(TELEMETRY_TEST_KEYED_FLAG in keyedHistograms)); + Assert.ok(TELEMETRY_TEST_KEYED_COUNT in keyedHistograms); + + const expected_keyed_count = { + "a": { + range: [1, 2], + bucket_count: 3, + histogram_type: 4, + values: {0:2, 1:0}, + sum: 2, + }, + "b": { + range: [1, 2], + bucket_count: 3, + histogram_type: 4, + values: {0:1, 1:0}, + sum: 1, + }, + }; + Assert.deepEqual(expected_keyed_count, keyedHistograms[TELEMETRY_TEST_KEYED_COUNT]); + + Assert.ok("processes" in payload, "The payload must have a processes section."); + Assert.ok("parent" in payload.processes, "There must be at least a parent process."); + checkScalars(payload.processes); + checkEvents(payload.processes); +} + +function writeStringToFile(file, contents) { + let ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + ostream.init(file, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, + RW_OWNER, ostream.DEFER_OPEN); + ostream.write(contents, contents.length); + ostream.QueryInterface(Ci.nsISafeOutputStream).finish(); + ostream.close(); +} + +function write_fake_shutdown_file() { + let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile); + let file = profileDirectory.clone(); + file.append("Telemetry.ShutdownTime.txt"); + let contents = "" + SHUTDOWN_TIME; + writeStringToFile(file, contents); +} + +function write_fake_failedprofilelocks_file() { + let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile); + let file = profileDirectory.clone(); + file.append("Telemetry.FailedProfileLocks.txt"); + let contents = "" + FAILED_PROFILE_LOCK_ATTEMPTS; + writeStringToFile(file, contents); +} + +add_task(function* test_setup() { + // Addon manager needs a profile directory + do_get_profile(); + loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION); + // Make sure we don't generate unexpected pings due to pref changes. + yield setEmptyPrefWatchlist(); + + Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true); + Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true); + + // Make it look like we've previously failed to lock a profile a couple times. + write_fake_failedprofilelocks_file(); + + // Make it look like we've shutdown before. + write_fake_shutdown_file(); + + let currentMaxNumberOfThreads = Telemetry.maximalNumberOfConcurrentThreads; + do_check_true(currentMaxNumberOfThreads > 0); + + // Try to augment the maximal number of threads currently launched + let threads = []; + try { + for (let i = 0; i < currentMaxNumberOfThreads + 10; ++i) { + threads.push(Services.tm.newThread(0)); + } + } catch (ex) { + // If memory is too low, it is possible that not all threads will be launched. + } + gNumberOfThreadsLaunched = threads.length; + + do_check_true(Telemetry.maximalNumberOfConcurrentThreads >= gNumberOfThreadsLaunched); + + do_register_cleanup(function() { + threads.forEach(function(thread) { + thread.shutdown(); + }); + }); + + yield new Promise(resolve => + Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(resolve))); +}); + +add_task(function* asyncSetup() { + yield TelemetryController.testSetup(); + // Load the client ID from the client ID provider to check for pings sanity. + gClientID = yield ClientID.getClientID(); +}); + +// Ensures that expired histograms are not part of the payload. +add_task(function* test_expiredHistogram() { + + let dummy = Telemetry.getHistogramById("TELEMETRY_TEST_EXPIRED"); + + dummy.add(1); + + do_check_eq(TelemetrySession.getPayload()["histograms"]["TELEMETRY_TEST_EXPIRED"], undefined); +}); + +// Sends a ping to a non existing server. If we remove this test, we won't get +// all the histograms we need in the main ping. +add_task(function* test_noServerPing() { + yield sendPing(); + // We need two pings in order to make sure STARTUP_MEMORY_STORAGE_SQLIE histograms + // are initialised. See bug 1131585. + yield sendPing(); + // Allowing Telemetry to persist unsent pings as pending. If omitted may cause + // problems to the consequent tests. + yield TelemetryController.testShutdown(); +}); + +// Checks that a sent ping is correctly received by a dummy http server. +add_task(function* test_simplePing() { + yield TelemetryStorage.testClearPendingPings(); + PingServer.start(); + Preferences.set(PREF_SERVER, "http://localhost:" + PingServer.port); + + let now = new Date(2020, 1, 1, 12, 0, 0); + let expectedDate = new Date(2020, 1, 1, 0, 0, 0); + fakeNow(now); + gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 5000); + + const expectedSessionUUID = "bd314d15-95bf-4356-b682-b6c4a8942202"; + const expectedSubsessionUUID = "3e2e5f6c-74ba-4e4d-a93f-a48af238a8c7"; + fakeGenerateUUID(() => expectedSessionUUID, () => expectedSubsessionUUID); + yield TelemetryController.testReset(); + + // Session and subsession start dates are faked during TelemetrySession setup. We can + // now fake the session duration. + const SESSION_DURATION_IN_MINUTES = 15; + fakeNow(new Date(2020, 1, 1, 12, SESSION_DURATION_IN_MINUTES, 0)); + gMonotonicNow = fakeMonotonicNow(gMonotonicNow + SESSION_DURATION_IN_MINUTES * 60 * 1000); + + yield sendPing(); + let ping = yield PingServer.promiseNextPing(); + + checkPingFormat(ping, PING_TYPE_MAIN, true, true); + + // Check that we get the data we expect. + let payload = ping.payload; + Assert.equal(payload.info.sessionId, expectedSessionUUID); + Assert.equal(payload.info.subsessionId, expectedSubsessionUUID); + let sessionStartDate = new Date(payload.info.sessionStartDate); + Assert.equal(sessionStartDate.toISOString(), expectedDate.toISOString()); + let subsessionStartDate = new Date(payload.info.subsessionStartDate); + Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString()); + Assert.equal(payload.info.subsessionLength, SESSION_DURATION_IN_MINUTES * 60); + + // Restore the UUID generator so we don't mess with other tests. + fakeGenerateUUID(generateUUID, generateUUID); +}); + +// Saves the current session histograms, reloads them, performs a ping +// and checks that the dummy http server received both the previously +// saved ping and the new one. +add_task(function* test_saveLoadPing() { + // Let's start out with a defined state. + yield TelemetryStorage.testClearPendingPings(); + yield TelemetryController.testReset(); + PingServer.clearRequests(); + + // Setup test data and trigger pings. + setupTestData(); + yield TelemetrySession.testSavePendingPing(); + yield sendPing(); + + // Get requests received by dummy server. + const requests = yield PingServer.promiseNextRequests(2); + + for (let req of requests) { + Assert.equal(req.getHeader("content-type"), "application/json; charset=UTF-8", + "The request must have the correct content-type."); + } + + // We decode both requests to check for the |reason|. + let pings = Array.from(requests, decodeRequestPayload); + + // Check we have the correct two requests. Ordering is not guaranteed. The ping type + // is encoded in the URL. + if (pings[0].type != PING_TYPE_MAIN) { + pings.reverse(); + } + + checkPingFormat(pings[0], PING_TYPE_MAIN, true, true); + checkPayload(pings[0].payload, REASON_TEST_PING, 0, 1); + checkPingFormat(pings[1], PING_TYPE_SAVED_SESSION, true, true); + checkPayload(pings[1].payload, REASON_SAVED_SESSION, 0, 0); +}); + +add_task(function* test_checkSubsessionScalars() { + if (gIsAndroid) { + // We don't support subsessions yet on Android. + return; + } + + // Clear the scalars. + Telemetry.clearScalars(); + yield TelemetryController.testReset(); + + // Set some scalars. + const UINT_SCALAR = "telemetry.test.unsigned_int_kind"; + const STRING_SCALAR = "telemetry.test.string_kind"; + let expectedUint = 37; + let expectedString = "Test value. Yay."; + Telemetry.scalarSet(UINT_SCALAR, expectedUint); + Telemetry.scalarSet(STRING_SCALAR, expectedString); + + // Check that scalars are not available in classic pings but are in subsession + // pings. Also clear the subsession. + let classic = TelemetrySession.getPayload(); + let subsession = TelemetrySession.getPayload("environment-change", true); + + const TEST_SCALARS = [ UINT_SCALAR, STRING_SCALAR ]; + for (let name of TEST_SCALARS) { + // Scalar must be reported in subsession pings (e.g. main). + Assert.ok(name in subsession.processes.parent.scalars, + name + " must be reported in a subsession ping."); + } + // No scalar must be reported in classic pings (e.g. saved-session). + Assert.ok(Object.keys(classic.processes.parent.scalars).length == 0, + "Scalars must not be reported in a classic ping."); + + // And make sure that we're getting the right values in the + // subsession ping. + Assert.equal(subsession.processes.parent.scalars[UINT_SCALAR], expectedUint, + UINT_SCALAR + " must contain the expected value."); + Assert.equal(subsession.processes.parent.scalars[STRING_SCALAR], expectedString, + STRING_SCALAR + " must contain the expected value."); + + // Since we cleared the subsession in the last getPayload(), check that + // breaking subsessions clears the scalars. + subsession = TelemetrySession.getPayload("environment-change"); + for (let name of TEST_SCALARS) { + Assert.ok(!(name in subsession.processes.parent.scalars), + name + " must be cleared with the new subsession."); + } + + // Check if setting the scalars again works as expected. + expectedUint = 85; + expectedString = "A creative different value"; + Telemetry.scalarSet(UINT_SCALAR, expectedUint); + Telemetry.scalarSet(STRING_SCALAR, expectedString); + subsession = TelemetrySession.getPayload("environment-change"); + Assert.equal(subsession.processes.parent.scalars[UINT_SCALAR], expectedUint, + UINT_SCALAR + " must contain the expected value."); + Assert.equal(subsession.processes.parent.scalars[STRING_SCALAR], expectedString, + STRING_SCALAR + " must contain the expected value."); +}); + +add_task(function* test_checkSubsessionEvents() { + if (gIsAndroid) { + // We don't support subsessions yet on Android. + return; + } + + // Clear the events. + Telemetry.clearEvents(); + yield TelemetryController.testReset(); + + // Record some events. + let expected = [ + ["telemetry.test", "test1", "object1", "a", null], + ["telemetry.test", "test1", "object1", null, {key1: "value"}], + ]; + for (let event of expected) { + Telemetry.recordEvent(...event); + } + + // Strip off trailing null values to match the serialized events. + for (let e of expected) { + while ((e.length >= 3) && (e[e.length - 1] === null)) { + e.pop(); + } + } + + // Check that events are not available in classic pings but are in subsession + // pings. Also clear the subsession. + let classic = TelemetrySession.getPayload(); + let subsession = TelemetrySession.getPayload("environment-change", true); + + Assert.ok("events" in classic.processes.parent, "Should have an events field in classic payload."); + Assert.ok("events" in subsession.processes.parent, "Should have an events field in subsession payload."); + + // They should be empty in the classic payload. + Assert.deepEqual(classic.processes.parent.events, [], "Events in classic payload should be empty."); + + // In the subsession payload, they should contain the recorded test events. + let events = subsession.processes.parent.events.filter(e => e[1] === "telemetry.test"); + Assert.equal(events.length, expected.length, "Should have the right amount of events in the payload."); + for (let i = 0; i < expected.length; ++i) { + Assert.deepEqual(events[i].slice(1), expected[i], + "Should have the right event data in the ping."); + } + + // As we cleared the subsession above, the events entry should now be empty. + subsession = TelemetrySession.getPayload("environment-change", false); + Assert.ok("events" in subsession.processes.parent, "Should have an events field in subsession payload."); + events = subsession.processes.parent.events.filter(e => e[1] === "telemetry.test"); + Assert.equal(events.length, 0, "Should have no test events in the subsession payload now."); +}); + +add_task(function* test_checkSubsessionHistograms() { + if (gIsAndroid) { + // We don't support subsessions yet on Android. + return; + } + + let now = new Date(2020, 1, 1, 12, 0, 0); + let expectedDate = new Date(2020, 1, 1, 0, 0, 0); + fakeNow(now); + yield TelemetryController.testReset(); + + const COUNT_ID = "TELEMETRY_TEST_COUNT"; + const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT"; + const count = Telemetry.getHistogramById(COUNT_ID); + const keyed = Telemetry.getKeyedHistogramById(KEYED_ID); + const registeredIds = + new Set(Telemetry.registeredHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, [])); + + const stableHistograms = new Set([ + "TELEMETRY_TEST_FLAG", + "TELEMETRY_TEST_COUNT", + "TELEMETRY_TEST_RELEASE_OPTOUT", + "TELEMETRY_TEST_RELEASE_OPTIN", + "STARTUP_CRASH_DETECTED", + ]); + + const stableKeyedHistograms = new Set([ + "TELEMETRY_TEST_KEYED_FLAG", + "TELEMETRY_TEST_KEYED_COUNT", + "TELEMETRY_TEST_KEYED_RELEASE_OPTIN", + "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT", + ]); + + // Compare the two sets of histograms. + // The "subsession" histograms should match the registered + // "classic" histograms. However, histograms can change + // between us collecting the different payloads, so we only + // check for deep equality on known stable histograms. + checkHistograms = (classic, subsession) => { + for (let id of Object.keys(classic)) { + if (!registeredIds.has(id)) { + continue; + } + + Assert.ok(id in subsession); + if (stableHistograms.has(id)) { + Assert.deepEqual(classic[id], + subsession[id]); + } else { + Assert.equal(classic[id].histogram_type, + subsession[id].histogram_type); + } + } + }; + + // Same as above, except for keyed histograms. + checkKeyedHistograms = (classic, subsession) => { + for (let id of Object.keys(classic)) { + if (!registeredIds.has(id)) { + continue; + } + + Assert.ok(id in subsession); + if (stableKeyedHistograms.has(id)) { + Assert.deepEqual(classic[id], + subsession[id]); + } + } + }; + + // Both classic and subsession payload histograms should start the same. + // The payloads should be identical for now except for the reason. + count.clear(); + keyed.clear(); + let classic = TelemetrySession.getPayload(); + let subsession = TelemetrySession.getPayload("environment-change"); + + Assert.equal(classic.info.reason, "gather-payload"); + Assert.equal(subsession.info.reason, "environment-change"); + Assert.ok(!(COUNT_ID in classic.histograms)); + Assert.ok(!(COUNT_ID in subsession.histograms)); + Assert.ok(!(KEYED_ID in classic.keyedHistograms)); + Assert.ok(!(KEYED_ID in subsession.keyedHistograms)); + + checkHistograms(classic.histograms, subsession.histograms); + checkKeyedHistograms(classic.keyedHistograms, subsession.keyedHistograms); + + // Adding values should get picked up in both. + count.add(1); + keyed.add("a", 1); + keyed.add("b", 1); + classic = TelemetrySession.getPayload(); + subsession = TelemetrySession.getPayload("environment-change"); + + Assert.ok(COUNT_ID in classic.histograms); + Assert.ok(COUNT_ID in subsession.histograms); + Assert.ok(KEYED_ID in classic.keyedHistograms); + Assert.ok(KEYED_ID in subsession.keyedHistograms); + Assert.equal(classic.histograms[COUNT_ID].sum, 1); + Assert.equal(classic.keyedHistograms[KEYED_ID]["a"].sum, 1); + Assert.equal(classic.keyedHistograms[KEYED_ID]["b"].sum, 1); + + checkHistograms(classic.histograms, subsession.histograms); + checkKeyedHistograms(classic.keyedHistograms, subsession.keyedHistograms); + + // Values should still reset properly. + count.clear(); + keyed.clear(); + classic = TelemetrySession.getPayload(); + subsession = TelemetrySession.getPayload("environment-change"); + + Assert.ok(!(COUNT_ID in classic.histograms)); + Assert.ok(!(COUNT_ID in subsession.histograms)); + Assert.ok(!(KEYED_ID in classic.keyedHistograms)); + Assert.ok(!(KEYED_ID in subsession.keyedHistograms)); + + checkHistograms(classic.histograms, subsession.histograms); + checkKeyedHistograms(classic.keyedHistograms, subsession.keyedHistograms); + + // Adding values should get picked up in both. + count.add(1); + keyed.add("a", 1); + keyed.add("b", 1); + classic = TelemetrySession.getPayload(); + subsession = TelemetrySession.getPayload("environment-change"); + + Assert.ok(COUNT_ID in classic.histograms); + Assert.ok(COUNT_ID in subsession.histograms); + Assert.ok(KEYED_ID in classic.keyedHistograms); + Assert.ok(KEYED_ID in subsession.keyedHistograms); + Assert.equal(classic.histograms[COUNT_ID].sum, 1); + Assert.equal(classic.keyedHistograms[KEYED_ID]["a"].sum, 1); + Assert.equal(classic.keyedHistograms[KEYED_ID]["b"].sum, 1); + + checkHistograms(classic.histograms, subsession.histograms); + checkKeyedHistograms(classic.keyedHistograms, subsession.keyedHistograms); + + // We should be able to reset only the subsession histograms. + // First check that "snapshot and clear" still returns the old state... + classic = TelemetrySession.getPayload(); + subsession = TelemetrySession.getPayload("environment-change", true); + + let subsessionStartDate = new Date(classic.info.subsessionStartDate); + Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString()); + subsessionStartDate = new Date(subsession.info.subsessionStartDate); + Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString()); + checkHistograms(classic.histograms, subsession.histograms); + checkKeyedHistograms(classic.keyedHistograms, subsession.keyedHistograms); + + // ... then check that the next snapshot shows the subsession + // histograms got reset. + classic = TelemetrySession.getPayload(); + subsession = TelemetrySession.getPayload("environment-change"); + + Assert.ok(COUNT_ID in classic.histograms); + Assert.ok(COUNT_ID in subsession.histograms); + Assert.equal(classic.histograms[COUNT_ID].sum, 1); + Assert.equal(subsession.histograms[COUNT_ID].sum, 0); + + Assert.ok(KEYED_ID in classic.keyedHistograms); + Assert.ok(!(KEYED_ID in subsession.keyedHistograms)); + Assert.equal(classic.keyedHistograms[KEYED_ID]["a"].sum, 1); + Assert.equal(classic.keyedHistograms[KEYED_ID]["b"].sum, 1); + + // Adding values should get picked up in both again. + count.add(1); + keyed.add("a", 1); + keyed.add("b", 1); + classic = TelemetrySession.getPayload(); + subsession = TelemetrySession.getPayload("environment-change"); + + Assert.ok(COUNT_ID in classic.histograms); + Assert.ok(COUNT_ID in subsession.histograms); + Assert.equal(classic.histograms[COUNT_ID].sum, 2); + Assert.equal(subsession.histograms[COUNT_ID].sum, 1); + + Assert.ok(KEYED_ID in classic.keyedHistograms); + Assert.ok(KEYED_ID in subsession.keyedHistograms); + Assert.equal(classic.keyedHistograms[KEYED_ID]["a"].sum, 2); + Assert.equal(classic.keyedHistograms[KEYED_ID]["b"].sum, 2); + Assert.equal(subsession.keyedHistograms[KEYED_ID]["a"].sum, 1); + Assert.equal(subsession.keyedHistograms[KEYED_ID]["b"].sum, 1); +}); + +add_task(function* test_checkSubsessionData() { + if (gIsAndroid) { + // We don't support subsessions yet on Android. + return; + } + + // Keep track of the active ticks count if the session recorder is available. + let sessionRecorder = TelemetryController.getSessionRecorder(); + let activeTicksAtSubsessionStart = sessionRecorder.activeTicks; + let expectedActiveTicks = activeTicksAtSubsessionStart; + + incrementActiveTicks = () => { + sessionRecorder.incrementActiveTicks(); + ++expectedActiveTicks; + } + + yield TelemetryController.testReset(); + + // Both classic and subsession payload data should be the same on the first subsession. + incrementActiveTicks(); + let classic = TelemetrySession.getPayload(); + let subsession = TelemetrySession.getPayload("environment-change"); + Assert.equal(classic.simpleMeasurements.activeTicks, expectedActiveTicks, + "Classic pings must count active ticks since the beginning of the session."); + Assert.equal(subsession.simpleMeasurements.activeTicks, expectedActiveTicks, + "Subsessions must count active ticks as classic pings on the first subsession."); + + // Start a new subsession and check that the active ticks are correctly reported. + incrementActiveTicks(); + activeTicksAtSubsessionStart = sessionRecorder.activeTicks; + classic = TelemetrySession.getPayload(); + subsession = TelemetrySession.getPayload("environment-change", true); + Assert.equal(classic.simpleMeasurements.activeTicks, expectedActiveTicks, + "Classic pings must count active ticks since the beginning of the session."); + Assert.equal(subsession.simpleMeasurements.activeTicks, expectedActiveTicks, + "Pings must not loose the tick count when starting a new subsession."); + + // Get a new subsession payload without clearing the subsession. + incrementActiveTicks(); + classic = TelemetrySession.getPayload(); + subsession = TelemetrySession.getPayload("environment-change"); + Assert.equal(classic.simpleMeasurements.activeTicks, expectedActiveTicks, + "Classic pings must count active ticks since the beginning of the session."); + Assert.equal(subsession.simpleMeasurements.activeTicks, + expectedActiveTicks - activeTicksAtSubsessionStart, + "Subsessions must count active ticks since the last new subsession."); +}); + +add_task(function* test_dailyCollection() { + if (gIsAndroid) { + // We don't do daily collections yet on Android. + return; + } + + let now = new Date(2030, 1, 1, 12, 0, 0); + let nowDay = new Date(2030, 1, 1, 0, 0, 0); + let schedulerTickCallback = null; + + PingServer.clearRequests(); + + fakeNow(now); + + // Fake scheduler functions to control daily collection flow in tests. + fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {}); + + // Init and check timer. + yield TelemetryStorage.testClearPendingPings(); + yield TelemetryController.testSetup(); + TelemetrySend.setServer("http://localhost:" + PingServer.port); + + // Set histograms to expected state. + const COUNT_ID = "TELEMETRY_TEST_COUNT"; + const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT"; + const count = Telemetry.getHistogramById(COUNT_ID); + const keyed = Telemetry.getKeyedHistogramById(KEYED_ID); + + count.clear(); + keyed.clear(); + count.add(1); + keyed.add("a", 1); + keyed.add("b", 1); + keyed.add("b", 1); + + // Make sure the daily ping gets triggered. + let expectedDate = nowDay; + now = futureDate(nowDay, MS_IN_ONE_DAY); + fakeNow(now); + + Assert.ok(!!schedulerTickCallback); + // Run a scheduler tick: it should trigger the daily ping. + yield schedulerTickCallback(); + + // Collect the daily ping. + let ping = yield PingServer.promiseNextPing(); + Assert.ok(!!ping); + + Assert.equal(ping.type, PING_TYPE_MAIN); + Assert.equal(ping.payload.info.reason, REASON_DAILY); + let subsessionStartDate = new Date(ping.payload.info.subsessionStartDate); + Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString()); + + Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1); + Assert.equal(ping.payload.keyedHistograms[KEYED_ID]["a"].sum, 1); + Assert.equal(ping.payload.keyedHistograms[KEYED_ID]["b"].sum, 2); + + // The daily ping is rescheduled for "tomorrow". + expectedDate = futureDate(expectedDate, MS_IN_ONE_DAY); + now = futureDate(now, MS_IN_ONE_DAY); + fakeNow(now); + + // Run a scheduler tick. Trigger and collect another ping. The histograms should be reset. + yield schedulerTickCallback(); + + ping = yield PingServer.promiseNextPing(); + Assert.ok(!!ping); + + Assert.equal(ping.type, PING_TYPE_MAIN); + Assert.equal(ping.payload.info.reason, REASON_DAILY); + subsessionStartDate = new Date(ping.payload.info.subsessionStartDate); + Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString()); + + Assert.equal(ping.payload.histograms[COUNT_ID].sum, 0); + Assert.ok(!(KEYED_ID in ping.payload.keyedHistograms)); + + // Trigger and collect another daily ping, with the histograms being set again. + count.add(1); + keyed.add("a", 1); + keyed.add("b", 1); + + // The daily ping is rescheduled for "tomorrow". + expectedDate = futureDate(expectedDate, MS_IN_ONE_DAY); + now = futureDate(now, MS_IN_ONE_DAY); + fakeNow(now); + + yield schedulerTickCallback(); + ping = yield PingServer.promiseNextPing(); + Assert.ok(!!ping); + + Assert.equal(ping.type, PING_TYPE_MAIN); + Assert.equal(ping.payload.info.reason, REASON_DAILY); + subsessionStartDate = new Date(ping.payload.info.subsessionStartDate); + Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString()); + + Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1); + Assert.equal(ping.payload.keyedHistograms[KEYED_ID]["a"].sum, 1); + Assert.equal(ping.payload.keyedHistograms[KEYED_ID]["b"].sum, 1); + + // Shutdown to cleanup the aborted-session if it gets created. + yield TelemetryController.testShutdown(); +}); + +add_task(function* test_dailyDuplication() { + if (gIsAndroid) { + // We don't do daily collections yet on Android. + return; + } + + yield TelemetrySend.reset(); + yield TelemetryStorage.testClearPendingPings(); + PingServer.clearRequests(); + + let schedulerTickCallback = null; + let now = new Date(2030, 1, 1, 0, 0, 0); + fakeNow(now); + // Fake scheduler functions to control daily collection flow in tests. + fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {}); + yield TelemetryController.testReset(); + + // Make sure the daily ping gets triggered at midnight. + // We need to make sure that we trigger this after the period where we wait for + // the user to become idle. + let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0); + fakeNow(firstDailyDue); + + // Run a scheduler tick: it should trigger the daily ping. + Assert.ok(!!schedulerTickCallback); + yield schedulerTickCallback(); + + // Get the first daily ping. + let ping = yield PingServer.promiseNextPing(); + Assert.ok(!!ping); + + Assert.equal(ping.type, PING_TYPE_MAIN); + Assert.equal(ping.payload.info.reason, REASON_DAILY); + + // We don't expect to receive any other daily ping in this test, so assert if we do. + PingServer.registerPingHandler((req, res) => { + Assert.ok(false, "No more daily pings should be sent/received in this test."); + }); + + // Set the current time to a bit after midnight. + let secondDailyDue = new Date(firstDailyDue); + secondDailyDue.setHours(0); + secondDailyDue.setMinutes(15); + fakeNow(secondDailyDue); + + // Run a scheduler tick: it should NOT trigger the daily ping. + Assert.ok(!!schedulerTickCallback); + yield schedulerTickCallback(); + + // Shutdown to cleanup the aborted-session if it gets created. + PingServer.resetPingHandler(); + yield TelemetryController.testShutdown(); +}); + +add_task(function* test_dailyOverdue() { + if (gIsAndroid) { + // We don't do daily collections yet on Android. + return; + } + + let schedulerTickCallback = null; + let now = new Date(2030, 1, 1, 11, 0, 0); + fakeNow(now); + // Fake scheduler functions to control daily collection flow in tests. + fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {}); + yield TelemetryStorage.testClearPendingPings(); + yield TelemetryController.testReset(); + + // Skip one hour ahead: nothing should be due. + now.setHours(now.getHours() + 1); + fakeNow(now); + + // Assert if we receive something! + PingServer.registerPingHandler((req, res) => { + Assert.ok(false, "No daily ping should be received if not overdue!."); + }); + + // This tick should not trigger any daily ping. + Assert.ok(!!schedulerTickCallback); + yield schedulerTickCallback(); + + // Restore the non asserting ping handler. + PingServer.resetPingHandler(); + PingServer.clearRequests(); + + // Simulate an overdue ping: we're not close to midnight, but the last daily ping + // time is too long ago. + let dailyOverdue = new Date(2030, 1, 2, 13, 0, 0); + fakeNow(dailyOverdue); + + // Run a scheduler tick: it should trigger the daily ping. + Assert.ok(!!schedulerTickCallback); + yield schedulerTickCallback(); + + // Get the first daily ping. + let ping = yield PingServer.promiseNextPing(); + Assert.ok(!!ping); + + Assert.equal(ping.type, PING_TYPE_MAIN); + Assert.equal(ping.payload.info.reason, REASON_DAILY); + + // Shutdown to cleanup the aborted-session if it gets created. + yield TelemetryController.testShutdown(); +}); + +add_task(function* test_environmentChange() { + if (gIsAndroid) { + // We don't split subsessions on environment changes yet on Android. + return; + } + + yield TelemetryStorage.testClearPendingPings(); + PingServer.clearRequests(); + + let now = fakeNow(2040, 1, 1, 12, 0, 0); + gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE); + + const PREF_TEST = "toolkit.telemetry.test.pref1"; + Preferences.reset(PREF_TEST); + + const PREFS_TO_WATCH = new Map([ + [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_VALUE}], + ]); + + // Setup. + yield TelemetryController.testReset(); + TelemetrySend.setServer("http://localhost:" + PingServer.port); + TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); + + // Set histograms to expected state. + const COUNT_ID = "TELEMETRY_TEST_COUNT"; + const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT"; + const count = Telemetry.getHistogramById(COUNT_ID); + const keyed = Telemetry.getKeyedHistogramById(KEYED_ID); + + count.clear(); + keyed.clear(); + count.add(1); + keyed.add("a", 1); + keyed.add("b", 1); + + // Trigger and collect environment-change ping. + gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE); + let startDay = truncateDateToDays(now); + now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE)); + + Preferences.set(PREF_TEST, 1); + let ping = yield PingServer.promiseNextPing(); + Assert.ok(!!ping); + + Assert.equal(ping.type, PING_TYPE_MAIN); + Assert.equal(ping.environment.settings.userPrefs[PREF_TEST], undefined); + Assert.equal(ping.payload.info.reason, REASON_ENVIRONMENT_CHANGE); + let subsessionStartDate = new Date(ping.payload.info.subsessionStartDate); + Assert.equal(subsessionStartDate.toISOString(), startDay.toISOString()); + + Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1); + Assert.equal(ping.payload.keyedHistograms[KEYED_ID]["a"].sum, 1); + + // Trigger and collect another ping. The histograms should be reset. + startDay = truncateDateToDays(now); + gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE); + now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE)); + + Preferences.set(PREF_TEST, 2); + ping = yield PingServer.promiseNextPing(); + Assert.ok(!!ping); + + Assert.equal(ping.type, PING_TYPE_MAIN); + Assert.equal(ping.environment.settings.userPrefs[PREF_TEST], 1); + Assert.equal(ping.payload.info.reason, REASON_ENVIRONMENT_CHANGE); + subsessionStartDate = new Date(ping.payload.info.subsessionStartDate); + Assert.equal(subsessionStartDate.toISOString(), startDay.toISOString()); + + Assert.equal(ping.payload.histograms[COUNT_ID].sum, 0); + Assert.ok(!(KEYED_ID in ping.payload.keyedHistograms)); +}); + +add_task(function* test_savedPingsOnShutdown() { + // On desktop, we expect both "saved-session" and "shutdown" pings. We only expect + // the former on Android. + const expectedPingCount = (gIsAndroid) ? 1 : 2; + // Assure that we store the ping properly when saving sessions on shutdown. + // We make the TelemetryController shutdown to trigger a session save. + const dir = TelemetryStorage.pingDirectoryPath; + yield OS.File.removeDir(dir, {ignoreAbsent: true}); + yield OS.File.makeDir(dir); + yield TelemetryController.testShutdown(); + + PingServer.clearRequests(); + yield TelemetryController.testReset(); + + const pings = yield PingServer.promiseNextPings(expectedPingCount); + + for (let ping of pings) { + Assert.ok("type" in ping); + + let expectedReason = + (ping.type == PING_TYPE_SAVED_SESSION) ? REASON_SAVED_SESSION : REASON_SHUTDOWN; + + checkPingFormat(ping, ping.type, true, true); + Assert.equal(ping.payload.info.reason, expectedReason); + Assert.equal(ping.clientId, gClientID); + } +}); + +add_task(function* test_savedSessionData() { + // Create the directory which will contain the data file, if it doesn't already + // exist. + yield OS.File.makeDir(DATAREPORTING_PATH); + getHistogram("TELEMETRY_SESSIONDATA_FAILED_LOAD").clear(); + getHistogram("TELEMETRY_SESSIONDATA_FAILED_PARSE").clear(); + getHistogram("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").clear(); + + // Write test data to the session data file. + const dataFilePath = OS.Path.join(DATAREPORTING_PATH, "session-state.json"); + const sessionState = { + sessionId: null, + subsessionId: null, + profileSubsessionCounter: 3785, + }; + yield CommonUtils.writeJSON(sessionState, dataFilePath); + + const PREF_TEST = "toolkit.telemetry.test.pref1"; + Preferences.reset(PREF_TEST); + const PREFS_TO_WATCH = new Map([ + [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_VALUE}], + ]); + + // We expect one new subsession when starting TelemetrySession and one after triggering + // an environment change. + const expectedSubsessions = sessionState.profileSubsessionCounter + 2; + const expectedSessionUUID = "ff602e52-47a1-b7e8-4c1a-ffffffffc87a"; + const expectedSubsessionUUID = "009fd1ad-b85e-4817-b3e5-000000003785"; + fakeGenerateUUID(() => expectedSessionUUID, () => expectedSubsessionUUID); + + if (gIsAndroid) { + // We don't support subsessions yet on Android, so skip the next checks. + return; + } + + // Start TelemetrySession so that it loads the session data file. + yield TelemetryController.testReset(); + Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum); + Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum); + Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum); + + // Watch a test preference, trigger and environment change and wait for it to propagate. + // _watchPreferences triggers a subsession notification + gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE); + fakeNow(new Date(2050, 1, 1, 12, 0, 0)); + TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); + let changePromise = new Promise(resolve => + TelemetryEnvironment.registerChangeListener("test_fake_change", resolve)); + Preferences.set(PREF_TEST, 1); + yield changePromise; + TelemetryEnvironment.unregisterChangeListener("test_fake_change"); + + let payload = TelemetrySession.getPayload(); + Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions); + yield TelemetryController.testShutdown(); + + // Restore the UUID generator so we don't mess with other tests. + fakeGenerateUUID(generateUUID, generateUUID); + + // Load back the serialised session data. + let data = yield CommonUtils.readJSON(dataFilePath); + Assert.equal(data.profileSubsessionCounter, expectedSubsessions); + Assert.equal(data.sessionId, expectedSessionUUID); + Assert.equal(data.subsessionId, expectedSubsessionUUID); +}); + +add_task(function* test_sessionData_ShortSession() { + if (gIsAndroid) { + // We don't support subsessions yet on Android, so skip the next checks. + return; + } + + const SESSION_STATE_PATH = OS.Path.join(DATAREPORTING_PATH, "session-state.json"); + + // Shut down Telemetry and remove the session state file. + yield TelemetryController.testReset(); + yield OS.File.remove(SESSION_STATE_PATH, { ignoreAbsent: true }); + getHistogram("TELEMETRY_SESSIONDATA_FAILED_LOAD").clear(); + getHistogram("TELEMETRY_SESSIONDATA_FAILED_PARSE").clear(); + getHistogram("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").clear(); + + const expectedSessionUUID = "ff602e52-47a1-b7e8-4c1a-ffffffffc87a"; + const expectedSubsessionUUID = "009fd1ad-b85e-4817-b3e5-000000003785"; + fakeGenerateUUID(() => expectedSessionUUID, () => expectedSubsessionUUID); + + // We intentionally don't wait for the setup to complete and shut down to simulate + // short sessions. We expect the profile subsession counter to be 1. + TelemetryController.testReset(); + yield TelemetryController.testShutdown(); + + Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum); + Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum); + Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum); + + // Restore the UUID generation functions. + fakeGenerateUUID(generateUUID, generateUUID); + + // Start TelemetryController so that it loads the session data file. We expect the profile + // subsession counter to be incremented by 1 again. + yield TelemetryController.testReset(); + + // We expect 2 profile subsession counter updates. + let payload = TelemetrySession.getPayload(); + Assert.equal(payload.info.profileSubsessionCounter, 2); + Assert.equal(payload.info.previousSessionId, expectedSessionUUID); + Assert.equal(payload.info.previousSubsessionId, expectedSubsessionUUID); + Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum); + Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum); + Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum); +}); + +add_task(function* test_invalidSessionData() { + // Create the directory which will contain the data file, if it doesn't already + // exist. + yield OS.File.makeDir(DATAREPORTING_PATH); + getHistogram("TELEMETRY_SESSIONDATA_FAILED_LOAD").clear(); + getHistogram("TELEMETRY_SESSIONDATA_FAILED_PARSE").clear(); + getHistogram("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").clear(); + + // Write test data to the session data file. This should fail to parse. + const dataFilePath = OS.Path.join(DATAREPORTING_PATH, "session-state.json"); + const unparseableData = "{asdf:@äü"; + OS.File.writeAtomic(dataFilePath, unparseableData, + {encoding: "utf-8", tmpPath: dataFilePath + ".tmp"}); + + // Start TelemetryController so that it loads the session data file. + yield TelemetryController.testReset(); + + // The session data file should not load. Only expect the current subsession. + Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum); + Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum); + Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum); + + // Write test data to the session data file. This should fail validation. + const sessionState = { + profileSubsessionCounter: "not-a-number?", + someOtherField: 12, + }; + yield CommonUtils.writeJSON(sessionState, dataFilePath); + + // The session data file should not load. Only expect the current subsession. + const expectedSubsessions = 1; + const expectedSessionUUID = "ff602e52-47a1-b7e8-4c1a-ffffffffc87a"; + const expectedSubsessionUUID = "009fd1ad-b85e-4817-b3e5-000000003785"; + fakeGenerateUUID(() => expectedSessionUUID, () => expectedSubsessionUUID); + + // Start TelemetryController so that it loads the session data file. + yield TelemetryController.testReset(); + + let payload = TelemetrySession.getPayload(); + Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions); + Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum); + Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum); + Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum); + + yield TelemetryController.testShutdown(); + + // Restore the UUID generator so we don't mess with other tests. + fakeGenerateUUID(generateUUID, generateUUID); + + // Load back the serialised session data. + let data = yield CommonUtils.readJSON(dataFilePath); + Assert.equal(data.profileSubsessionCounter, expectedSubsessions); + Assert.equal(data.sessionId, expectedSessionUUID); + Assert.equal(data.subsessionId, expectedSubsessionUUID); +}); + +add_task(function* test_abortedSession() { + if (gIsAndroid || gIsGonk) { + // We don't have the aborted session ping here. + return; + } + + const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME); + + // Make sure the aborted sessions directory does not exist to test its creation. + yield OS.File.removeDir(DATAREPORTING_PATH, { ignoreAbsent: true }); + + let schedulerTickCallback = null; + let now = new Date(2040, 1, 1, 0, 0, 0); + fakeNow(now); + // Fake scheduler functions to control aborted-session flow in tests. + fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {}); + yield TelemetryController.testReset(); + + Assert.ok((yield OS.File.exists(DATAREPORTING_PATH)), + "Telemetry must create the aborted session directory when starting."); + + // Fake now again so that the scheduled aborted-session save takes place. + now = futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS); + fakeNow(now); + // The first aborted session checkpoint must take place right after the initialisation. + Assert.ok(!!schedulerTickCallback); + // Execute one scheduler tick. + yield schedulerTickCallback(); + // Check that the aborted session is due at the correct time. + Assert.ok((yield OS.File.exists(ABORTED_FILE)), + "There must be an aborted session ping."); + + // This ping is not yet in the pending pings folder, so we can't access it using + // TelemetryStorage.popPendingPings(). + let pingContent = yield OS.File.read(ABORTED_FILE, { encoding: "utf-8" }); + let abortedSessionPing = JSON.parse(pingContent); + + // Validate the ping. + checkPingFormat(abortedSessionPing, PING_TYPE_MAIN, true, true); + Assert.equal(abortedSessionPing.payload.info.reason, REASON_ABORTED_SESSION); + + // Trigger a another aborted-session ping and check that it overwrites the previous one. + now = futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS); + fakeNow(now); + yield schedulerTickCallback(); + + pingContent = yield OS.File.read(ABORTED_FILE, { encoding: "utf-8" }); + let updatedAbortedSessionPing = JSON.parse(pingContent); + checkPingFormat(updatedAbortedSessionPing, PING_TYPE_MAIN, true, true); + Assert.equal(updatedAbortedSessionPing.payload.info.reason, REASON_ABORTED_SESSION); + Assert.notEqual(abortedSessionPing.id, updatedAbortedSessionPing.id); + Assert.notEqual(abortedSessionPing.creationDate, updatedAbortedSessionPing.creationDate); + + yield TelemetryController.testShutdown(); + Assert.ok(!(yield OS.File.exists(ABORTED_FILE)), + "No aborted session ping must be available after a shutdown."); + + // Write the ping to the aborted-session file. TelemetrySession will add it to the + // saved pings directory when it starts. + yield TelemetryStorage.savePingToFile(abortedSessionPing, ABORTED_FILE, false); + Assert.ok((yield OS.File.exists(ABORTED_FILE)), + "The aborted session ping must exist in the aborted session ping directory."); + + yield TelemetryStorage.testClearPendingPings(); + PingServer.clearRequests(); + yield TelemetryController.testReset(); + + Assert.ok(!(yield OS.File.exists(ABORTED_FILE)), + "The aborted session ping must be removed from the aborted session ping directory."); + + // Restarting Telemetry again to trigger sending pings in TelemetrySend. + yield TelemetryController.testReset(); + + // We should have received an aborted-session ping. + const receivedPing = yield PingServer.promiseNextPing(); + Assert.equal(receivedPing.type, PING_TYPE_MAIN, "Should have the correct type"); + Assert.equal(receivedPing.payload.info.reason, REASON_ABORTED_SESSION, "Ping should have the correct reason"); + + yield TelemetryController.testShutdown(); +}); + +add_task(function* test_abortedSession_Shutdown() { + if (gIsAndroid || gIsGonk) { + // We don't have the aborted session ping here. + return; + } + + const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME); + + let schedulerTickCallback = null; + let now = fakeNow(2040, 1, 1, 0, 0, 0); + // Fake scheduler functions to control aborted-session flow in tests. + fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {}); + yield TelemetryController.testReset(); + + Assert.ok((yield OS.File.exists(DATAREPORTING_PATH)), + "Telemetry must create the aborted session directory when starting."); + + // Fake now again so that the scheduled aborted-session save takes place. + fakeNow(futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS)); + // The first aborted session checkpoint must take place right after the initialisation. + Assert.ok(!!schedulerTickCallback); + // Execute one scheduler tick. + yield schedulerTickCallback(); + // Check that the aborted session is due at the correct time. + Assert.ok((yield OS.File.exists(ABORTED_FILE)), "There must be an aborted session ping."); + + // Remove the aborted session file and then shut down to make sure exceptions (e.g file + // not found) do not compromise the shutdown. + yield OS.File.remove(ABORTED_FILE); + + yield TelemetryController.testShutdown(); +}); + +add_task(function* test_abortedDailyCoalescing() { + if (gIsAndroid || gIsGonk) { + // We don't have the aborted session or the daily ping here. + return; + } + + const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME); + + // Make sure the aborted sessions directory does not exist to test its creation. + yield OS.File.removeDir(DATAREPORTING_PATH, { ignoreAbsent: true }); + + let schedulerTickCallback = null; + PingServer.clearRequests(); + + let nowDate = new Date(2009, 10, 18, 0, 0, 0); + fakeNow(nowDate); + + // Fake scheduler functions to control aborted-session flow in tests. + fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {}); + yield TelemetryStorage.testClearPendingPings(); + PingServer.clearRequests(); + yield TelemetryController.testReset(); + + Assert.ok((yield OS.File.exists(DATAREPORTING_PATH)), + "Telemetry must create the aborted session directory when starting."); + + // Delay the callback around midnight so that the aborted-session ping gets merged with the + // daily ping. + let dailyDueDate = futureDate(nowDate, MS_IN_ONE_DAY); + fakeNow(dailyDueDate); + // Trigger both the daily ping and the saved-session. + Assert.ok(!!schedulerTickCallback); + // Execute one scheduler tick. + yield schedulerTickCallback(); + + // Wait for the daily ping. + let dailyPing = yield PingServer.promiseNextPing(); + Assert.equal(dailyPing.payload.info.reason, REASON_DAILY); + + // Check that an aborted session ping was also written to disk. + Assert.ok((yield OS.File.exists(ABORTED_FILE)), + "There must be an aborted session ping."); + + // Read aborted session ping and check that the session/subsession ids equal the + // ones in the daily ping. + let pingContent = yield OS.File.read(ABORTED_FILE, { encoding: "utf-8" }); + let abortedSessionPing = JSON.parse(pingContent); + Assert.equal(abortedSessionPing.payload.info.sessionId, dailyPing.payload.info.sessionId); + Assert.equal(abortedSessionPing.payload.info.subsessionId, dailyPing.payload.info.subsessionId); + + yield TelemetryController.testShutdown(); +}); + +add_task(function* test_schedulerComputerSleep() { + if (gIsAndroid || gIsGonk) { + // We don't have the aborted session or the daily ping here. + return; + } + + const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME); + + yield TelemetryStorage.testClearPendingPings(); + yield TelemetryController.testReset(); + PingServer.clearRequests(); + + // Remove any aborted-session ping from the previous tests. + yield OS.File.removeDir(DATAREPORTING_PATH, { ignoreAbsent: true }); + + // Set a fake current date and start Telemetry. + let nowDate = fakeNow(2009, 10, 18, 0, 0, 0); + let schedulerTickCallback = null; + fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {}); + yield TelemetryController.testReset(); + + // Set the current time 3 days in the future at midnight, before running the callback. + nowDate = fakeNow(futureDate(nowDate, 3 * MS_IN_ONE_DAY)); + Assert.ok(!!schedulerTickCallback); + // Execute one scheduler tick. + yield schedulerTickCallback(); + + let dailyPing = yield PingServer.promiseNextPing(); + Assert.equal(dailyPing.payload.info.reason, REASON_DAILY, + "The wake notification should have triggered a daily ping."); + Assert.equal(dailyPing.creationDate, nowDate.toISOString(), + "The daily ping date should be correct."); + + Assert.ok((yield OS.File.exists(ABORTED_FILE)), + "There must be an aborted session ping."); + + // Now also test if we are sending a daily ping if we wake up on the next + // day even when the timer doesn't trigger. + // This can happen due to timeouts not running out during sleep times, + // see bug 1262386, bug 1204823 et al. + // Note that we don't get wake notifications on Linux due to bug 758848. + nowDate = fakeNow(futureDate(nowDate, 1 * MS_IN_ONE_DAY)); + + // We emulate the mentioned timeout behavior by sending the wake notification + // instead of triggering the timeout callback. + // This should trigger a daily ping, because we passed midnight. + Services.obs.notifyObservers(null, "wake_notification", null); + + dailyPing = yield PingServer.promiseNextPing(); + Assert.equal(dailyPing.payload.info.reason, REASON_DAILY, + "The wake notification should have triggered a daily ping."); + Assert.equal(dailyPing.creationDate, nowDate.toISOString(), + "The daily ping date should be correct."); + + yield TelemetryController.testShutdown(); +}); + +add_task(function* test_schedulerEnvironmentReschedules() { + if (gIsAndroid || gIsGonk) { + // We don't have the aborted session or the daily ping here. + return; + } + + // Reset the test preference. + const PREF_TEST = "toolkit.telemetry.test.pref1"; + Preferences.reset(PREF_TEST); + const PREFS_TO_WATCH = new Map([ + [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_VALUE}], + ]); + + yield TelemetryStorage.testClearPendingPings(); + PingServer.clearRequests(); + yield TelemetryController.testReset(); + + // Set a fake current date and start Telemetry. + let nowDate = fakeNow(2060, 10, 18, 0, 0, 0); + gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE); + let schedulerTickCallback = null; + fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {}); + yield TelemetryController.testReset(); + TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); + + // Set the current time at midnight. + fakeNow(futureDate(nowDate, MS_IN_ONE_DAY)); + gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE); + + // Trigger the environment change. + Preferences.set(PREF_TEST, 1); + + // Wait for the environment-changed ping. + yield PingServer.promiseNextPing(); + + // We don't expect to receive any daily ping in this test, so assert if we do. + PingServer.registerPingHandler((req, res) => { + Assert.ok(false, "No ping should be sent/received in this test."); + }); + + // Execute one scheduler tick. It should not trigger a daily ping. + Assert.ok(!!schedulerTickCallback); + yield schedulerTickCallback(); + yield TelemetryController.testShutdown(); +}); + +add_task(function* test_schedulerNothingDue() { + if (gIsAndroid || gIsGonk) { + // We don't have the aborted session or the daily ping here. + return; + } + + const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME); + + // Remove any aborted-session ping from the previous tests. + yield OS.File.removeDir(DATAREPORTING_PATH, { ignoreAbsent: true }); + yield TelemetryStorage.testClearPendingPings(); + yield TelemetryController.testReset(); + + // We don't expect to receive any ping in this test, so assert if we do. + PingServer.registerPingHandler((req, res) => { + Assert.ok(false, "No ping should be sent/received in this test."); + }); + + // Set a current date/time away from midnight, so that the daily ping doesn't get + // sent. + let nowDate = new Date(2009, 10, 18, 11, 0, 0); + fakeNow(nowDate); + let schedulerTickCallback = null; + fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {}); + yield TelemetryController.testReset(); + + // Delay the callback execution to a time when no ping should be due. + let nothingDueDate = futureDate(nowDate, ABORTED_SESSION_UPDATE_INTERVAL_MS / 2); + fakeNow(nothingDueDate); + Assert.ok(!!schedulerTickCallback); + // Execute one scheduler tick. + yield schedulerTickCallback(); + + // Check that no aborted session ping was written to disk. + Assert.ok(!(yield OS.File.exists(ABORTED_FILE))); + + yield TelemetryController.testShutdown(); + PingServer.resetPingHandler(); +}); + +add_task(function* test_pingExtendedStats() { + const EXTENDED_PAYLOAD_FIELDS = [ + "chromeHangs", "threadHangStats", "log", "slowSQL", "fileIOReports", "lateWrites", + "addonHistograms", "addonDetails", "UIMeasurements", "webrtc" + ]; + + // Reset telemetry and disable sending extended statistics. + yield TelemetryStorage.testClearPendingPings(); + PingServer.clearRequests(); + yield TelemetryController.testReset(); + Telemetry.canRecordExtended = false; + + yield sendPing(); + + let ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, PING_TYPE_MAIN, true, true); + + // Check that the payload does not contain extended statistics fields. + for (let f in EXTENDED_PAYLOAD_FIELDS) { + Assert.ok(!(EXTENDED_PAYLOAD_FIELDS[f] in ping.payload), + EXTENDED_PAYLOAD_FIELDS[f] + " must not be in the payload if the extended set is off."); + } + + // We check this one separately so that we can reuse EXTENDED_PAYLOAD_FIELDS below, since + // slowSQLStartup might not be there. + Assert.ok(!("slowSQLStartup" in ping.payload), + "slowSQLStartup must not be sent if the extended set is off"); + + Assert.ok(!("addonManager" in ping.payload.simpleMeasurements), + "addonManager must not be sent if the extended set is off."); + Assert.ok(!("UITelemetry" in ping.payload.simpleMeasurements), + "UITelemetry must not be sent if the extended set is off."); + + // Restore the preference. + Telemetry.canRecordExtended = true; + + // Send a new ping that should contain the extended data. + yield sendPing(); + ping = yield PingServer.promiseNextPing(); + checkPingFormat(ping, PING_TYPE_MAIN, true, true); + + // Check that the payload now contains extended statistics fields. + for (let f in EXTENDED_PAYLOAD_FIELDS) { + Assert.ok(EXTENDED_PAYLOAD_FIELDS[f] in ping.payload, + EXTENDED_PAYLOAD_FIELDS[f] + " must be in the payload if the extended set is on."); + } + + Assert.ok("addonManager" in ping.payload.simpleMeasurements, + "addonManager must be sent if the extended set is on."); + Assert.ok("UITelemetry" in ping.payload.simpleMeasurements, + "UITelemetry must be sent if the extended set is on."); +}); + +add_task(function* test_schedulerUserIdle() { + if (gIsAndroid || gIsGonk) { + // We don't have the aborted session or the daily ping here. + return; + } + + const SCHEDULER_TICK_INTERVAL_MS = 5 * 60 * 1000; + const SCHEDULER_TICK_IDLE_INTERVAL_MS = 60 * 60 * 1000; + + let now = new Date(2010, 1, 1, 11, 0, 0); + fakeNow(now); + + let schedulerTimeout = 0; + fakeSchedulerTimer((callback, timeout) => { + schedulerTimeout = timeout; + }, () => {}); + yield TelemetryController.testReset(); + yield TelemetryStorage.testClearPendingPings(); + PingServer.clearRequests(); + + // When not idle, the scheduler should have a 5 minutes tick interval. + Assert.equal(schedulerTimeout, SCHEDULER_TICK_INTERVAL_MS); + + // Send an "idle" notification to the scheduler. + fakeIdleNotification("idle"); + + // When idle, the scheduler should have a 1hr tick interval. + Assert.equal(schedulerTimeout, SCHEDULER_TICK_IDLE_INTERVAL_MS); + + // Send an "active" notification to the scheduler. + fakeIdleNotification("active"); + + // When user is back active, the scheduler tick should be 5 minutes again. + Assert.equal(schedulerTimeout, SCHEDULER_TICK_INTERVAL_MS); + + // We should not miss midnight when going to idle. + now.setHours(23); + now.setMinutes(50); + fakeNow(now); + fakeIdleNotification("idle"); + Assert.equal(schedulerTimeout, 10 * 60 * 1000); + + yield TelemetryController.testShutdown(); +}); + +add_task(function* test_DailyDueAndIdle() { + if (gIsAndroid || gIsGonk) { + // We don't have the aborted session or the daily ping here. + return; + } + + yield TelemetryStorage.testClearPendingPings(); + PingServer.clearRequests(); + + let receivedPingRequest = null; + // Register a ping handler that will assert when receiving multiple daily pings. + PingServer.registerPingHandler(req => { + Assert.ok(!receivedPingRequest, "Telemetry must only send one daily ping."); + receivedPingRequest = req; + }); + + // Faking scheduler timer has to happen before resetting TelemetryController + // to be effective. + let schedulerTickCallback = null; + let now = new Date(2030, 1, 1, 0, 0, 0); + fakeNow(now); + // Fake scheduler functions to control daily collection flow in tests. + fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {}); + yield TelemetryController.testReset(); + + // Trigger the daily ping. + let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0); + fakeNow(firstDailyDue); + + // Run a scheduler tick: it should trigger the daily ping. + Assert.ok(!!schedulerTickCallback); + let tickPromise = schedulerTickCallback(); + + // Send an idle and then an active user notification. + fakeIdleNotification("idle"); + fakeIdleNotification("active"); + + // Wait on the tick promise. + yield tickPromise; + + yield TelemetrySend.testWaitOnOutgoingPings(); + + // Decode the ping contained in the request and check that's a daily ping. + Assert.ok(receivedPingRequest, "Telemetry must send one daily ping."); + const receivedPing = decodeRequestPayload(receivedPingRequest); + checkPingFormat(receivedPing, PING_TYPE_MAIN, true, true); + Assert.equal(receivedPing.payload.info.reason, REASON_DAILY); + + yield TelemetryController.testShutdown(); +}); + +add_task(function* test_userIdleAndSchedlerTick() { + if (gIsAndroid || gIsGonk) { + // We don't have the aborted session or the daily ping here. + return; + } + + let receivedPingRequest = null; + // Register a ping handler that will assert when receiving multiple daily pings. + PingServer.registerPingHandler(req => { + Assert.ok(!receivedPingRequest, "Telemetry must only send one daily ping."); + receivedPingRequest = req; + }); + + let schedulerTickCallback = null; + let now = new Date(2030, 1, 1, 0, 0, 0); + fakeNow(now); + // Fake scheduler functions to control daily collection flow in tests. + fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {}); + yield TelemetryStorage.testClearPendingPings(); + yield TelemetryController.testReset(); + PingServer.clearRequests(); + + // Move the current date/time to midnight. + let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0); + fakeNow(firstDailyDue); + + // The active notification should trigger a scheduler tick. The latter will send the + // due daily ping. + fakeIdleNotification("active"); + + // Immediately running another tick should not send a daily ping again. + Assert.ok(!!schedulerTickCallback); + yield schedulerTickCallback(); + + // A new "idle" notification should not send a new daily ping. + fakeIdleNotification("idle"); + + yield TelemetrySend.testWaitOnOutgoingPings(); + + // Decode the ping contained in the request and check that's a daily ping. + Assert.ok(receivedPingRequest, "Telemetry must send one daily ping."); + const receivedPing = decodeRequestPayload(receivedPingRequest); + checkPingFormat(receivedPing, PING_TYPE_MAIN, true, true); + Assert.equal(receivedPing.payload.info.reason, REASON_DAILY); + + PingServer.resetPingHandler(); + yield TelemetryController.testShutdown(); +}); + +add_task(function* test_changeThrottling() { + if (gIsAndroid) { + // We don't support subsessions yet on Android. + return; + } + + let getSubsessionCount = () => { + return TelemetrySession.getPayload().info.subsessionCounter; + }; + + const PREF_TEST = "toolkit.telemetry.test.pref1"; + const PREFS_TO_WATCH = new Map([ + [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_STATE}], + ]); + Preferences.reset(PREF_TEST); + + let now = fakeNow(2050, 1, 2, 0, 0, 0); + gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE); + yield TelemetryController.testReset(); + Assert.equal(getSubsessionCount(), 1); + + // Set the Environment preferences to watch. + TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH); + + // The first pref change should not trigger a notification. + Preferences.set(PREF_TEST, 1); + Assert.equal(getSubsessionCount(), 1); + + // We should get a change notification after the 5min throttling interval. + fakeNow(futureDate(now, 5 * MILLISECONDS_PER_MINUTE + 1)); + gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 5 * MILLISECONDS_PER_MINUTE + 1); + Preferences.set(PREF_TEST, 2); + Assert.equal(getSubsessionCount(), 2); + + // After that, changes should be throttled again. + now = fakeNow(futureDate(now, 1 * MILLISECONDS_PER_MINUTE)); + gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 1 * MILLISECONDS_PER_MINUTE); + Preferences.set(PREF_TEST, 3); + Assert.equal(getSubsessionCount(), 2); + + // ... for 5min. + now = fakeNow(futureDate(now, 4 * MILLISECONDS_PER_MINUTE + 1)); + gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 4 * MILLISECONDS_PER_MINUTE + 1); + Preferences.set(PREF_TEST, 4); + Assert.equal(getSubsessionCount(), 3); + + // Unregister the listener. + TelemetryEnvironment.unregisterChangeListener("testWatchPrefs_throttling"); +}); + +add_task(function* stopServer() { + yield PingServer.stop(); +}); diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryStopwatch.js b/toolkit/components/telemetry/tests/unit/test_TelemetryStopwatch.js new file mode 100644 index 000000000..d162d9b17 --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryStopwatch.js @@ -0,0 +1,156 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var tmpScope = {}; +Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", tmpScope); +var TelemetryStopwatch = tmpScope.TelemetryStopwatch; + +const HIST_NAME = "TELEMETRY_SEND_SUCCESS"; +const HIST_NAME2 = "RANGE_CHECKSUM_ERRORS"; +const KEYED_HIST = { id: "TELEMETRY_INVALID_PING_TYPE_SUBMITTED", key: "TEST" }; + +var refObj = {}, refObj2 = {}; + +var originalCount1, originalCount2; + +function run_test() { + let histogram = Telemetry.getHistogramById(HIST_NAME); + let snapshot = histogram.snapshot(); + originalCount1 = snapshot.counts.reduce((a, b) => a += b); + + histogram = Telemetry.getHistogramById(HIST_NAME2); + snapshot = histogram.snapshot(); + originalCount2 = snapshot.counts.reduce((a, b) => a += b); + + histogram = Telemetry.getKeyedHistogramById(KEYED_HIST.id); + snapshot = histogram.snapshot(KEYED_HIST.key); + originalCount3 = snapshot.counts.reduce((a, b) => a += b); + + do_check_false(TelemetryStopwatch.start(3)); + do_check_false(TelemetryStopwatch.start({})); + do_check_false(TelemetryStopwatch.start("", 3)); + do_check_false(TelemetryStopwatch.start("", "")); + do_check_false(TelemetryStopwatch.start({}, {})); + + do_check_true(TelemetryStopwatch.start("mark1")); + do_check_true(TelemetryStopwatch.start("mark2")); + + do_check_true(TelemetryStopwatch.start("mark1", refObj)); + do_check_true(TelemetryStopwatch.start("mark2", refObj)); + + // Same timer can't be re-started before being stopped + do_check_false(TelemetryStopwatch.start("mark1")); + do_check_false(TelemetryStopwatch.start("mark1", refObj)); + + // Can't stop a timer that was accidentaly started twice + do_check_false(TelemetryStopwatch.finish("mark1")); + do_check_false(TelemetryStopwatch.finish("mark1", refObj)); + + do_check_true(TelemetryStopwatch.start("NON-EXISTENT_HISTOGRAM")); + do_check_false(TelemetryStopwatch.finish("NON-EXISTENT_HISTOGRAM")); + + do_check_true(TelemetryStopwatch.start("NON-EXISTENT_HISTOGRAM", refObj)); + do_check_false(TelemetryStopwatch.finish("NON-EXISTENT_HISTOGRAM", refObj)); + + do_check_true(TelemetryStopwatch.start(HIST_NAME)); + do_check_true(TelemetryStopwatch.start(HIST_NAME2)); + do_check_true(TelemetryStopwatch.start(HIST_NAME, refObj)); + do_check_true(TelemetryStopwatch.start(HIST_NAME2, refObj)); + do_check_true(TelemetryStopwatch.start(HIST_NAME, refObj2)); + do_check_true(TelemetryStopwatch.start(HIST_NAME2, refObj2)); + + do_check_true(TelemetryStopwatch.finish(HIST_NAME)); + do_check_true(TelemetryStopwatch.finish(HIST_NAME2)); + do_check_true(TelemetryStopwatch.finish(HIST_NAME, refObj)); + do_check_true(TelemetryStopwatch.finish(HIST_NAME2, refObj)); + do_check_true(TelemetryStopwatch.finish(HIST_NAME, refObj2)); + do_check_true(TelemetryStopwatch.finish(HIST_NAME2, refObj2)); + + // Verify that TS.finish deleted the timers + do_check_false(TelemetryStopwatch.finish(HIST_NAME)); + do_check_false(TelemetryStopwatch.finish(HIST_NAME, refObj)); + + // Verify that they can be used again + do_check_true(TelemetryStopwatch.start(HIST_NAME)); + do_check_true(TelemetryStopwatch.start(HIST_NAME, refObj)); + do_check_true(TelemetryStopwatch.finish(HIST_NAME)); + do_check_true(TelemetryStopwatch.finish(HIST_NAME, refObj)); + + do_check_false(TelemetryStopwatch.finish("unknown-mark")); // Unknown marker + do_check_false(TelemetryStopwatch.finish("unknown-mark", {})); // Unknown object + do_check_false(TelemetryStopwatch.finish(HIST_NAME, {})); // Known mark on unknown object + + // Test cancel + do_check_true(TelemetryStopwatch.start(HIST_NAME)); + do_check_true(TelemetryStopwatch.start(HIST_NAME, refObj)); + do_check_true(TelemetryStopwatch.cancel(HIST_NAME)); + do_check_true(TelemetryStopwatch.cancel(HIST_NAME, refObj)); + + // Verify that can not cancel twice + do_check_false(TelemetryStopwatch.cancel(HIST_NAME)); + do_check_false(TelemetryStopwatch.cancel(HIST_NAME, refObj)); + + // Verify that cancel removes the timers + do_check_false(TelemetryStopwatch.finish(HIST_NAME)); + do_check_false(TelemetryStopwatch.finish(HIST_NAME, refObj)); + + // Verify that keyed stopwatch reject invalid keys. + for (let key of [3, {}, ""]) { + do_check_false(TelemetryStopwatch.startKeyed(KEYED_HIST.id, key)); + } + + // Verify that keyed histograms can be started. + do_check_true(TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY1")); + do_check_true(TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY2")); + do_check_true(TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY1", refObj)); + do_check_true(TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY2", refObj)); + + // Restarting keyed histograms should fail. + do_check_false(TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY1")); + do_check_false(TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY1", refObj)); + + // Finishing a stopwatch of a non existing histogram should return false. + do_check_false(TelemetryStopwatch.finishKeyed("HISTOGRAM", "KEY2")); + do_check_false(TelemetryStopwatch.finishKeyed("HISTOGRAM", "KEY2", refObj)); + + // Starting & finishing a keyed stopwatch for an existing histogram should work. + do_check_true(TelemetryStopwatch.startKeyed(KEYED_HIST.id, KEYED_HIST.key)); + do_check_true(TelemetryStopwatch.finishKeyed(KEYED_HIST.id, KEYED_HIST.key)); + // Verify that TS.finish deleted the timers + do_check_false(TelemetryStopwatch.finishKeyed(KEYED_HIST.id, KEYED_HIST.key)); + + // Verify that they can be used again + do_check_true(TelemetryStopwatch.startKeyed(KEYED_HIST.id, KEYED_HIST.key)); + do_check_true(TelemetryStopwatch.finishKeyed(KEYED_HIST.id, KEYED_HIST.key)); + + do_check_false(TelemetryStopwatch.finishKeyed("unknown-mark", "unknown-key")); + do_check_false(TelemetryStopwatch.finishKeyed(KEYED_HIST.id, "unknown-key")); + + // Verify that keyed histograms can only be canceled through "keyed" API. + do_check_true(TelemetryStopwatch.startKeyed(KEYED_HIST.id, KEYED_HIST.key)); + do_check_false(TelemetryStopwatch.cancel(KEYED_HIST.id, KEYED_HIST.key)); + do_check_true(TelemetryStopwatch.cancelKeyed(KEYED_HIST.id, KEYED_HIST.key)); + do_check_false(TelemetryStopwatch.cancelKeyed(KEYED_HIST.id, KEYED_HIST.key)); + + finishTest(); +} + +function finishTest() { + let histogram = Telemetry.getHistogramById(HIST_NAME); + let snapshot = histogram.snapshot(); + let newCount = snapshot.counts.reduce((a, b) => a += b); + + do_check_eq(newCount - originalCount1, 5, "The correct number of histograms were added for histogram 1."); + + histogram = Telemetry.getHistogramById(HIST_NAME2); + snapshot = histogram.snapshot(); + newCount = snapshot.counts.reduce((a, b) => a += b); + + do_check_eq(newCount - originalCount2, 3, "The correct number of histograms were added for histogram 2."); + + histogram = Telemetry.getKeyedHistogramById(KEYED_HIST.id); + snapshot = histogram.snapshot(KEYED_HIST.key); + newCount = snapshot.counts.reduce((a, b) => a += b); + + do_check_eq(newCount - originalCount3, 2, "The correct number of histograms were added for histogram 3."); +} diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js b/toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js new file mode 100644 index 000000000..75bf3157a --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var Cu = Components.utils; +var Cc = Components.classes; +var Ci = Components.interfaces; +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/TelemetryController.jsm", this); +Cu.import("resource://gre/modules/TelemetrySession.jsm", this); +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); + +// The @mozilla/xre/app-info;1 XPCOM object provided by the xpcshell test harness doesn't +// implement the nsIXULAppInfo interface, which is needed by Services.jsm and +// TelemetrySession.jsm. updateAppInfo() creates and registers a minimal mock app-info. +Cu.import("resource://testing-common/AppInfo.jsm"); +updateAppInfo(); + +var gGlobalScope = this; + +function getSimpleMeasurementsFromTelemetryController() { + return TelemetrySession.getPayload().simpleMeasurements; +} + +add_task(function* test_setup() { + // Telemetry needs the AddonManager. + loadAddonManager(); + // Make profile available for |TelemetryController.testShutdown()|. + do_get_profile(); + + // Make sure we don't generate unexpected pings due to pref changes. + yield setEmptyPrefWatchlist(); + + yield new Promise(resolve => + Services.telemetry.asyncFetchTelemetryData(resolve)); +}); + +add_task(function* actualTest() { + yield TelemetryController.testSetup(); + + // Test the module logic + let tmp = {}; + Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", tmp); + let TelemetryTimestamps = tmp.TelemetryTimestamps; + let now = Date.now(); + TelemetryTimestamps.add("foo"); + do_check_true(TelemetryTimestamps.get().foo != null); // foo was added + do_check_true(TelemetryTimestamps.get().foo >= now); // foo has a reasonable value + + // Add timestamp with value + // Use a value far in the future since TelemetryController substracts the time of + // process initialization. + const YEAR_4000_IN_MS = 64060588800000; + TelemetryTimestamps.add("bar", YEAR_4000_IN_MS); + do_check_eq(TelemetryTimestamps.get().bar, YEAR_4000_IN_MS); // bar has the right value + + // Can't add the same timestamp twice + TelemetryTimestamps.add("bar", 2); + do_check_eq(TelemetryTimestamps.get().bar, YEAR_4000_IN_MS); // bar wasn't overwritten + + let threw = false; + try { + TelemetryTimestamps.add("baz", "this isn't a number"); + } catch (ex) { + threw = true; + } + do_check_true(threw); // adding non-number threw + do_check_null(TelemetryTimestamps.get().baz); // no baz was added + + // Test that the data gets added to the telemetry ping properly + let simpleMeasurements = getSimpleMeasurementsFromTelemetryController(); + do_check_true(simpleMeasurements != null); // got simple measurements from ping data + do_check_true(simpleMeasurements.foo > 1); // foo was included + do_check_true(simpleMeasurements.bar > 1); // bar was included + do_check_eq(undefined, simpleMeasurements.baz); // baz wasn't included since it wasn't added + + yield TelemetryController.testShutdown(); +}); diff --git a/toolkit/components/telemetry/tests/unit/test_ThreadHangStats.js b/toolkit/components/telemetry/tests/unit/test_ThreadHangStats.js new file mode 100644 index 000000000..e8c9f868a --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_ThreadHangStats.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +Cu.import("resource://gre/modules/Services.jsm"); + +function getMainThreadHangStats() { + let threads = Services.telemetry.threadHangStats; + return threads.find((thread) => (thread.name === "Gecko")); +} + +function run_test() { + let startHangs = getMainThreadHangStats(); + + // We disable hang reporting in several situations (e.g. debug builds, + // official releases). In those cases, we don't have hang stats available + // and should exit the test early. + if (!startHangs) { + ok("Hang reporting not enabled."); + return; + } + + if (Services.appinfo.OS === 'Linux' || Services.appinfo.OS === 'Android') { + // We use the rt_tgsigqueueinfo syscall on Linux which requires a + // certain kernel version. It's not an error if the system running + // the test is older than that. + let kernel = Services.sysinfo.get('kernel_version') || + Services.sysinfo.get('version'); + if (Services.vc.compare(kernel, '2.6.31') < 0) { + ok("Hang reporting not supported for old kernel."); + return; + } + } + + // Run three events in the event loop: + // the first event causes a transient hang; + // the second event causes a permanent hang; + // the third event checks results from previous events. + + do_execute_soon(() => { + // Cause a hang lasting 1 second (transient hang). + let startTime = Date.now(); + while ((Date.now() - startTime) < 1000); + }); + + do_execute_soon(() => { + // Cause a hang lasting 10 seconds (permanent hang). + let startTime = Date.now(); + while ((Date.now() - startTime) < 10000); + }); + + do_execute_soon(() => { + do_test_pending(); + + let check_results = () => { + let endHangs = getMainThreadHangStats(); + + // Because hangs are recorded asynchronously, if we don't see new hangs, + // we should wait for pending hangs to be recorded. On the other hand, + // if hang monitoring is broken, this test will time out. + if (endHangs.hangs.length === startHangs.hangs.length) { + do_timeout(100, check_results); + return; + } + + let check_histogram = (histogram) => { + equal(typeof histogram, "object"); + equal(histogram.histogram_type, 0); + equal(typeof histogram.min, "number"); + equal(typeof histogram.max, "number"); + equal(typeof histogram.sum, "number"); + ok(Array.isArray(histogram.ranges)); + ok(Array.isArray(histogram.counts)); + equal(histogram.counts.length, histogram.ranges.length); + }; + + // Make sure the hang stats structure is what we expect. + equal(typeof endHangs, "object"); + check_histogram(endHangs.activity); + + ok(Array.isArray(endHangs.hangs)); + notEqual(endHangs.hangs.length, 0); + + ok(Array.isArray(endHangs.hangs[0].stack)); + notEqual(endHangs.hangs[0].stack.length, 0); + equal(typeof endHangs.hangs[0].stack[0], "string"); + + // Make sure one of the hangs is a permanent + // hang containing a native stack. + ok(endHangs.hangs.some((hang) => ( + Array.isArray(hang.nativeStack) && + hang.nativeStack.length !== 0 && + typeof hang.nativeStack[0] === "string" + ))); + + check_histogram(endHangs.hangs[0].histogram); + + do_test_finished(); + }; + + check_results(); + }); +} diff --git a/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js b/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js new file mode 100644 index 000000000..8dc552604 --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js @@ -0,0 +1,883 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const INT_MAX = 0x7FFFFFFF; + +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/TelemetryUtils.jsm", this); + +// Return an array of numbers from lower up to, excluding, upper +function numberRange(lower, upper) +{ + let a = []; + for (let i=lower; i<upper; ++i) { + a.push(i); + } + return a; +} + +function expect_fail(f) { + let failed = false; + try { + f(); + failed = false; + } catch (e) { + failed = true; + } + do_check_true(failed); +} + +function expect_success(f) { + let succeeded = false; + try { + f(); + succeeded = true; + } catch (e) { + succeeded = false; + } + do_check_true(succeeded); +} + +function compareHistograms(h1, h2) { + let s1 = h1.snapshot(); + let s2 = h2.snapshot(); + + do_check_eq(s1.histogram_type, s2.histogram_type); + do_check_eq(s1.min, s2.min); + do_check_eq(s1.max, s2.max); + do_check_eq(s1.sum, s2.sum); + + do_check_eq(s1.counts.length, s2.counts.length); + for (let i = 0; i < s1.counts.length; i++) + do_check_eq(s1.counts[i], s2.counts[i]); + + do_check_eq(s1.ranges.length, s2.ranges.length); + for (let i = 0; i < s1.ranges.length; i++) + do_check_eq(s1.ranges[i], s2.ranges[i]); +} + +function check_histogram(histogram_type, name, min, max, bucket_count) { + var h = Telemetry.getHistogramById(name); + var r = h.snapshot().ranges; + var sum = 0; + for (let i=0;i<r.length;i++) { + var v = r[i]; + sum += v; + h.add(v); + } + var s = h.snapshot(); + // verify properties + do_check_eq(sum, s.sum); + + // there should be exactly one element per bucket + for (let i of s.counts) { + do_check_eq(i, 1); + } + var hgrams = Telemetry.histogramSnapshots + let gh = hgrams[name] + do_check_eq(gh.histogram_type, histogram_type); + + do_check_eq(gh.min, min) + do_check_eq(gh.max, max) + + // Check that booleans work with nonboolean histograms + h.add(false); + h.add(true); + s = h.snapshot().counts; + do_check_eq(s[0], 2) + do_check_eq(s[1], 2) + + // Check that clearing works. + h.clear(); + s = h.snapshot(); + for (var i of s.counts) { + do_check_eq(i, 0); + } + do_check_eq(s.sum, 0); + + h.add(0); + h.add(1); + var c = h.snapshot().counts; + do_check_eq(c[0], 1); + do_check_eq(c[1], 1); +} + +// This MUST be the very first test of this file. +add_task({ + skip_if: () => gIsAndroid +}, +function* test_instantiate() { + const ID = "TELEMETRY_TEST_COUNT"; + let h = Telemetry.getHistogramById(ID); + + // Instantiate the subsession histogram through |add| and make sure they match. + // This MUST be the first use of "TELEMETRY_TEST_COUNT" in this file, otherwise + // |add| will not instantiate the histogram. + h.add(1); + let snapshot = h.snapshot(); + let subsession = Telemetry.snapshotSubsessionHistograms(); + Assert.equal(snapshot.sum, subsession[ID].sum, + "Histogram and subsession histogram sum must match."); + // Clear the histogram, so we don't void the assumptions from the other tests. + h.clear(); +}); + +add_task(function* test_parameterChecks() { + let kinds = [Telemetry.HISTOGRAM_EXPONENTIAL, Telemetry.HISTOGRAM_LINEAR] + let testNames = ["TELEMETRY_TEST_EXPONENTIAL", "TELEMETRY_TEST_LINEAR"] + for (let i = 0; i < kinds.length; i++) { + let histogram_type = kinds[i]; + let test_type = testNames[i]; + let [min, max, bucket_count] = [1, INT_MAX - 1, 10] + check_histogram(histogram_type, test_type, min, max, bucket_count); + } +}); + +add_task(function* test_noSerialization() { + // Instantiate the storage for this histogram and make sure it doesn't + // get reflected into JS, as it has no interesting data in it. + Telemetry.getHistogramById("NEWTAB_PAGE_PINNED_SITES_COUNT"); + do_check_false("NEWTAB_PAGE_PINNED_SITES_COUNT" in Telemetry.histogramSnapshots); +}); + +add_task(function* test_boolean_histogram() { + var h = Telemetry.getHistogramById("TELEMETRY_TEST_BOOLEAN"); + var r = h.snapshot().ranges; + // boolean histograms ignore numeric parameters + do_check_eq(uneval(r), uneval([0, 1, 2])) + for (var i=0;i<r.length;i++) { + var v = r[i]; + h.add(v); + } + h.add(true); + h.add(false); + var s = h.snapshot(); + do_check_eq(s.histogram_type, Telemetry.HISTOGRAM_BOOLEAN); + // last bucket should always be 0 since .add parameters are normalized to either 0 or 1 + do_check_eq(s.counts[2], 0); + do_check_eq(s.sum, 3); + do_check_eq(s.counts[0], 2); +}); + +add_task(function* test_flag_histogram() { + var h = Telemetry.getHistogramById("TELEMETRY_TEST_FLAG"); + var r = h.snapshot().ranges; + // Flag histograms ignore numeric parameters. + do_check_eq(uneval(r), uneval([0, 1, 2])); + // Should already have a 0 counted. + var c = h.snapshot().counts; + var s = h.snapshot().sum; + do_check_eq(uneval(c), uneval([1, 0, 0])); + do_check_eq(s, 0); + // Should switch counts. + h.add(1); + var c2 = h.snapshot().counts; + var s2 = h.snapshot().sum; + do_check_eq(uneval(c2), uneval([0, 1, 0])); + do_check_eq(s2, 1); + // Should only switch counts once. + h.add(1); + var c3 = h.snapshot().counts; + var s3 = h.snapshot().sum; + do_check_eq(uneval(c3), uneval([0, 1, 0])); + do_check_eq(s3, 1); + do_check_eq(h.snapshot().histogram_type, Telemetry.HISTOGRAM_FLAG); +}); + +add_task(function* test_count_histogram() { + let h = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT2"); + let s = h.snapshot(); + do_check_eq(uneval(s.ranges), uneval([0, 1, 2])); + do_check_eq(uneval(s.counts), uneval([0, 0, 0])); + do_check_eq(s.sum, 0); + h.add(); + s = h.snapshot(); + do_check_eq(uneval(s.counts), uneval([1, 0, 0])); + do_check_eq(s.sum, 1); + h.add(); + s = h.snapshot(); + do_check_eq(uneval(s.counts), uneval([2, 0, 0])); + do_check_eq(s.sum, 2); +}); + +add_task(function* test_categorical_histogram() +{ + let h1 = Telemetry.getHistogramById("TELEMETRY_TEST_CATEGORICAL"); + for (let v of ["CommonLabel", "Label2", "Label3", "Label3", 0, 0, 1]) { + h1.add(v); + } + for (let s of ["", "Label4", "1234"]) { + Assert.throws(() => h1.add(s)); + } + + let snapshot = h1.snapshot(); + Assert.equal(snapshot.sum, 6); + Assert.deepEqual(snapshot.ranges, [0, 1, 2, 3]); + Assert.deepEqual(snapshot.counts, [3, 2, 2, 0]); + + let h2 = Telemetry.getHistogramById("TELEMETRY_TEST_CATEGORICAL_OPTOUT"); + for (let v of ["CommonLabel", "CommonLabel", "Label4", "Label5", "Label6", 0, 1]) { + h2.add(v); + } + for (let s of ["", "Label3", "1234"]) { + Assert.throws(() => h2.add(s)); + } + + snapshot = h2.snapshot(); + Assert.equal(snapshot.sum, 7); + Assert.deepEqual(snapshot.ranges, [0, 1, 2, 3, 4]); + Assert.deepEqual(snapshot.counts, [3, 2, 1, 1, 0]); +}); + +add_task(function* test_getHistogramById() { + try { + Telemetry.getHistogramById("nonexistent"); + do_throw("This can't happen"); + } catch (e) { + + } + var h = Telemetry.getHistogramById("CYCLE_COLLECTOR"); + var s = h.snapshot(); + do_check_eq(s.histogram_type, Telemetry.HISTOGRAM_EXPONENTIAL); + do_check_eq(s.min, 1); + do_check_eq(s.max, 10000); +}); + +add_task(function* test_getSlowSQL() { + var slow = Telemetry.slowSQL; + do_check_true(("mainThread" in slow) && ("otherThreads" in slow)); +}); + +add_task(function* test_getWebrtc() { + var webrtc = Telemetry.webrtcStats; + do_check_true("IceCandidatesStats" in webrtc); + var icestats = webrtc.IceCandidatesStats; + do_check_true("webrtc" in icestats); +}); + +// Check that telemetry doesn't record in private mode +add_task(function* test_privateMode() { + var h = Telemetry.getHistogramById("TELEMETRY_TEST_BOOLEAN"); + var orig = h.snapshot(); + Telemetry.canRecordExtended = false; + h.add(1); + do_check_eq(uneval(orig), uneval(h.snapshot())); + Telemetry.canRecordExtended = true; + h.add(1); + do_check_neq(uneval(orig), uneval(h.snapshot())); +}); + +// Check that telemetry records only when it is suppose to. +add_task(function* test_histogramRecording() { + // Check that no histogram is recorded if both base and extended recording are off. + Telemetry.canRecordBase = false; + Telemetry.canRecordExtended = false; + + let h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT"); + h.clear(); + let orig = h.snapshot(); + h.add(1); + Assert.equal(orig.sum, h.snapshot().sum); + + // Check that only base histograms are recorded. + Telemetry.canRecordBase = true; + h.add(1); + Assert.equal(orig.sum + 1, h.snapshot().sum, + "Histogram value should have incremented by 1 due to recording."); + + // Extended histograms should not be recorded. + h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTIN"); + orig = h.snapshot(); + h.add(1); + Assert.equal(orig.sum, h.snapshot().sum, + "Histograms should be equal after recording."); + + // Runtime created histograms should not be recorded. + h = Telemetry.getHistogramById("TELEMETRY_TEST_BOOLEAN"); + orig = h.snapshot(); + h.add(1); + Assert.equal(orig.sum, h.snapshot().sum, + "Histograms should be equal after recording."); + + // Check that extended histograms are recorded when required. + Telemetry.canRecordExtended = true; + + h.add(1); + Assert.equal(orig.sum + 1, h.snapshot().sum, + "Runtime histogram value should have incremented by 1 due to recording."); + + h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTIN"); + orig = h.snapshot(); + h.add(1); + Assert.equal(orig.sum + 1, h.snapshot().sum, + "Histogram value should have incremented by 1 due to recording."); + + // Check that base histograms are still being recorded. + h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT"); + h.clear(); + orig = h.snapshot(); + h.add(1); + Assert.equal(orig.sum + 1, h.snapshot().sum, + "Histogram value should have incremented by 1 due to recording."); +}); + +add_task(function* test_addons() { + var addon_id = "testing-addon"; + var fake_addon_id = "fake-addon"; + var name1 = "testing-histogram1"; + var register = Telemetry.registerAddonHistogram; + expect_success(() => + register(addon_id, name1, Telemetry.HISTOGRAM_LINEAR, 1, 5, 6)); + // Can't register the same histogram multiple times. + expect_fail(() => + register(addon_id, name1, Telemetry.HISTOGRAM_LINEAR, 1, 5, 6)); + // Make sure we can't get at it with another name. + expect_fail(() => Telemetry.getAddonHistogram(fake_addon_id, name1)); + + // Check for reflection capabilities. + var h1 = Telemetry.getAddonHistogram(addon_id, name1); + // Verify that although we've created storage for it, we don't reflect it into JS. + var snapshots = Telemetry.addonHistogramSnapshots; + do_check_false(name1 in snapshots[addon_id]); + h1.add(1); + h1.add(3); + var s1 = h1.snapshot(); + do_check_eq(s1.histogram_type, Telemetry.HISTOGRAM_LINEAR); + do_check_eq(s1.min, 1); + do_check_eq(s1.max, 5); + do_check_eq(s1.counts[1], 1); + do_check_eq(s1.counts[3], 1); + + var name2 = "testing-histogram2"; + expect_success(() => + register(addon_id, name2, Telemetry.HISTOGRAM_LINEAR, 2, 4, 4)); + + var h2 = Telemetry.getAddonHistogram(addon_id, name2); + h2.add(2); + h2.add(3); + var s2 = h2.snapshot(); + do_check_eq(s2.histogram_type, Telemetry.HISTOGRAM_LINEAR); + do_check_eq(s2.min, 2); + do_check_eq(s2.max, 4); + do_check_eq(s2.counts[1], 1); + do_check_eq(s2.counts[2], 1); + + // Check that we can register histograms for a different addon with + // identical names. + var extra_addon = "testing-extra-addon"; + expect_success(() => + register(extra_addon, name1, Telemetry.HISTOGRAM_BOOLEAN)); + + // Check that we can register flag histograms. + var flag_addon = "testing-flag-addon"; + var flag_histogram = "flag-histogram"; + expect_success(() => + register(flag_addon, flag_histogram, Telemetry.HISTOGRAM_FLAG)); + expect_success(() => + register(flag_addon, name2, Telemetry.HISTOGRAM_LINEAR, 2, 4, 4)); + + // Check that we reflect registered addons and histograms. + snapshots = Telemetry.addonHistogramSnapshots; + do_check_true(addon_id in snapshots) + do_check_true(extra_addon in snapshots); + do_check_true(flag_addon in snapshots); + + // Check that we have data for our created histograms. + do_check_true(name1 in snapshots[addon_id]); + do_check_true(name2 in snapshots[addon_id]); + var s1_alt = snapshots[addon_id][name1]; + var s2_alt = snapshots[addon_id][name2]; + do_check_eq(s1_alt.min, s1.min); + do_check_eq(s1_alt.max, s1.max); + do_check_eq(s1_alt.histogram_type, s1.histogram_type); + do_check_eq(s2_alt.min, s2.min); + do_check_eq(s2_alt.max, s2.max); + do_check_eq(s2_alt.histogram_type, s2.histogram_type); + + // Even though we've registered it, it shouldn't show up until data is added to it. + do_check_false(name1 in snapshots[extra_addon]); + + // Flag histograms should show up automagically. + do_check_true(flag_histogram in snapshots[flag_addon]); + do_check_false(name2 in snapshots[flag_addon]); + + // Check that we can remove addon histograms. + Telemetry.unregisterAddonHistograms(addon_id); + snapshots = Telemetry.addonHistogramSnapshots; + do_check_false(addon_id in snapshots); + // Make sure other addons are unaffected. + do_check_true(extra_addon in snapshots); +}); + +add_task(function* test_expired_histogram() { + var test_expired_id = "TELEMETRY_TEST_EXPIRED"; + var dummy = Telemetry.getHistogramById(test_expired_id); + var rh = Telemetry.registeredHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, []); + Assert.ok(!!rh); + + dummy.add(1); + + do_check_eq(Telemetry.histogramSnapshots["__expired__"], undefined); + do_check_eq(Telemetry.histogramSnapshots[test_expired_id], undefined); + do_check_eq(rh[test_expired_id], undefined); +}); + +add_task(function* test_keyed_histogram() { + // Check that invalid names get rejected. + + let threw = false; + try { + Telemetry.getKeyedHistogramById("test::unknown histogram", "never", Telemetry.HISTOGRAM_BOOLEAN); + } catch (e) { + // This should throw as it is an unknown ID + threw = true; + } + Assert.ok(threw, "getKeyedHistogramById should have thrown"); +}); + +add_task(function* test_keyed_boolean_histogram() { + const KEYED_ID = "TELEMETRY_TEST_KEYED_BOOLEAN"; + let KEYS = numberRange(0, 2).map(i => "key" + (i + 1)); + KEYS.push("漢語"); + let histogramBase = { + "min": 1, + "max": 2, + "histogram_type": 2, + "sum": 1, + "ranges": [0, 1, 2], + "counts": [0, 1, 0] + }; + let testHistograms = numberRange(0, 3).map(i => JSON.parse(JSON.stringify(histogramBase))); + let testKeys = []; + let testSnapShot = {}; + + let h = Telemetry.getKeyedHistogramById(KEYED_ID); + for (let i=0; i<2; ++i) { + let key = KEYS[i]; + h.add(key, true); + testSnapShot[key] = testHistograms[i]; + testKeys.push(key); + + Assert.deepEqual(h.keys().sort(), testKeys); + Assert.deepEqual(h.snapshot(), testSnapShot); + } + + h = Telemetry.getKeyedHistogramById(KEYED_ID); + Assert.deepEqual(h.keys().sort(), testKeys); + Assert.deepEqual(h.snapshot(), testSnapShot); + + let key = KEYS[2]; + h.add(key, false); + testKeys.push(key); + testSnapShot[key] = testHistograms[2]; + testSnapShot[key].sum = 0; + testSnapShot[key].counts = [1, 0, 0]; + Assert.deepEqual(h.keys().sort(), testKeys); + Assert.deepEqual(h.snapshot(), testSnapShot); + + let allSnapshots = Telemetry.keyedHistogramSnapshots; + Assert.deepEqual(allSnapshots[KEYED_ID], testSnapShot); + + h.clear(); + Assert.deepEqual(h.keys(), []); + Assert.deepEqual(h.snapshot(), {}); +}); + +add_task(function* test_keyed_count_histogram() { + const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT"; + const KEYS = numberRange(0, 5).map(i => "key" + (i + 1)); + let histogramBase = { + "min": 1, + "max": 2, + "histogram_type": 4, + "sum": 0, + "ranges": [0, 1, 2], + "counts": [1, 0, 0] + }; + let testHistograms = numberRange(0, 5).map(i => JSON.parse(JSON.stringify(histogramBase))); + let testKeys = []; + let testSnapShot = {}; + + let h = Telemetry.getKeyedHistogramById(KEYED_ID); + for (let i=0; i<4; ++i) { + let key = KEYS[i]; + let value = i*2 + 1; + + for (let k=0; k<value; ++k) { + h.add(key); + } + testHistograms[i].counts[0] = value; + testHistograms[i].sum = value; + testSnapShot[key] = testHistograms[i]; + testKeys.push(key); + + Assert.deepEqual(h.keys().sort(), testKeys); + Assert.deepEqual(h.snapshot(key), testHistograms[i]); + Assert.deepEqual(h.snapshot(), testSnapShot); + } + + h = Telemetry.getKeyedHistogramById(KEYED_ID); + Assert.deepEqual(h.keys().sort(), testKeys); + Assert.deepEqual(h.snapshot(), testSnapShot); + + let key = KEYS[4]; + h.add(key); + testKeys.push(key); + testHistograms[4].counts[0] = 1; + testHistograms[4].sum = 1; + testSnapShot[key] = testHistograms[4]; + + Assert.deepEqual(h.keys().sort(), testKeys); + Assert.deepEqual(h.snapshot(), testSnapShot); + + let allSnapshots = Telemetry.keyedHistogramSnapshots; + Assert.deepEqual(allSnapshots[KEYED_ID], testSnapShot); + + h.clear(); + Assert.deepEqual(h.keys(), []); + Assert.deepEqual(h.snapshot(), {}); +}); + +add_task(function* test_keyed_flag_histogram() { + const KEYED_ID = "TELEMETRY_TEST_KEYED_FLAG"; + let h = Telemetry.getKeyedHistogramById(KEYED_ID); + + const KEY = "default"; + h.add(KEY, true); + + let testSnapshot = {}; + testSnapshot[KEY] = { + "min": 1, + "max": 2, + "histogram_type": 3, + "sum": 1, + "ranges": [0, 1, 2], + "counts": [0, 1, 0] + }; + + Assert.deepEqual(h.keys().sort(), [KEY]); + Assert.deepEqual(h.snapshot(), testSnapshot); + + let allSnapshots = Telemetry.keyedHistogramSnapshots; + Assert.deepEqual(allSnapshots[KEYED_ID], testSnapshot); + + h.clear(); + Assert.deepEqual(h.keys(), []); + Assert.deepEqual(h.snapshot(), {}); +}); + +add_task(function* test_keyed_histogram_recording() { + // Check that no histogram is recorded if both base and extended recording are off. + Telemetry.canRecordBase = false; + Telemetry.canRecordExtended = false; + + const TEST_KEY = "record_foo"; + let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"); + h.clear(); + h.add(TEST_KEY, 1); + Assert.equal(h.snapshot(TEST_KEY).sum, 0); + + // Check that only base histograms are recorded. + Telemetry.canRecordBase = true; + h.add(TEST_KEY, 1); + Assert.equal(h.snapshot(TEST_KEY).sum, 1, + "The keyed histogram should record the correct value."); + + // Extended set keyed histograms should not be recorded. + h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTIN"); + h.clear(); + h.add(TEST_KEY, 1); + Assert.equal(h.snapshot(TEST_KEY).sum, 0, + "The keyed histograms should not record any data."); + + // Check that extended histograms are recorded when required. + Telemetry.canRecordExtended = true; + + h.add(TEST_KEY, 1); + Assert.equal(h.snapshot(TEST_KEY).sum, 1, + "The runtime keyed histogram should record the correct value."); + + h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTIN"); + h.clear(); + h.add(TEST_KEY, 1); + Assert.equal(h.snapshot(TEST_KEY).sum, 1, + "The keyed histogram should record the correct value."); + + // Check that base histograms are still being recorded. + h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"); + h.clear(); + h.add(TEST_KEY, 1); + Assert.equal(h.snapshot(TEST_KEY).sum, 1); +}); + +add_task(function* test_histogram_recording_enabled() { + Telemetry.canRecordBase = true; + Telemetry.canRecordExtended = true; + + // Check that a "normal" histogram respects recording-enabled on/off + var h = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT"); + var orig = h.snapshot(); + + h.add(1); + Assert.equal(orig.sum + 1, h.snapshot().sum, + "add should record by default."); + + // Check that when recording is disabled - add is ignored + Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT", false); + h.add(1); + Assert.equal(orig.sum + 1, h.snapshot().sum, + "When recording is disabled add should not record."); + + // Check that we're back to normal after recording is enabled + Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT", true); + h.add(1); + Assert.equal(orig.sum + 2, h.snapshot().sum, + "When recording is re-enabled add should record."); + + // Check that we're correctly accumulating values other than 1. + h.clear(); + h.add(3); + Assert.equal(3, h.snapshot().sum, "Recording counts greater than 1 should work."); + + // Check that a histogram with recording disabled by default behaves correctly + h = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT_INIT_NO_RECORD"); + orig = h.snapshot(); + + h.add(1); + Assert.equal(orig.sum, h.snapshot().sum, + "When recording is disabled by default, add should not record by default."); + + Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT_INIT_NO_RECORD", true); + h.add(1); + Assert.equal(orig.sum + 1, h.snapshot().sum, + "When recording is enabled add should record."); + + // Restore to disabled + Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT_INIT_NO_RECORD", false); + h.add(1); + Assert.equal(orig.sum + 1, h.snapshot().sum, + "When recording is disabled add should not record."); +}); + +add_task(function* test_keyed_histogram_recording_enabled() { + Telemetry.canRecordBase = true; + Telemetry.canRecordExtended = true; + + // Check RecordingEnabled for keyed histograms which are recording by default + const TEST_KEY = "record_foo"; + let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"); + + h.clear(); + h.add(TEST_KEY, 1); + Assert.equal(h.snapshot(TEST_KEY).sum, 1, + "Keyed histogram add should record by default"); + + Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT", false); + h.add(TEST_KEY, 1); + Assert.equal(h.snapshot(TEST_KEY).sum, 1, + "Keyed histogram add should not record when recording is disabled"); + + Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT", true); + h.clear(); + h.add(TEST_KEY, 1); + Assert.equal(h.snapshot(TEST_KEY).sum, 1, + "Keyed histogram add should record when recording is re-enabled"); + + // Check that a histogram with recording disabled by default behaves correctly + h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD"); + h.clear(); + + h.add(TEST_KEY, 1); + Assert.equal(h.snapshot(TEST_KEY).sum, 0, + "Keyed histogram add should not record by default for histograms which don't record by default"); + + Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD", true); + h.add(TEST_KEY, 1); + Assert.equal(h.snapshot(TEST_KEY).sum, 1, + "Keyed histogram add should record when recording is enabled"); + + // Restore to disabled + Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD", false); + h.add(TEST_KEY, 1); + Assert.equal(h.snapshot(TEST_KEY).sum, 1, + "Keyed histogram add should not record when recording is disabled"); +}); + +add_task(function* test_datasets() { + // Check that datasets work as expected. + + const RELEASE_CHANNEL_OPTOUT = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTOUT; + const RELEASE_CHANNEL_OPTIN = Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN; + + // Histograms should default to the extended dataset + let h = Telemetry.getHistogramById("TELEMETRY_TEST_FLAG"); + Assert.equal(h.dataset(), RELEASE_CHANNEL_OPTIN); + h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_FLAG"); + Assert.equal(h.dataset(), RELEASE_CHANNEL_OPTIN); + + // Check test histograms with explicit dataset definitions + h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTIN"); + Assert.equal(h.dataset(), RELEASE_CHANNEL_OPTIN); + h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT"); + Assert.equal(h.dataset(), RELEASE_CHANNEL_OPTOUT); + h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTIN"); + Assert.equal(h.dataset(), RELEASE_CHANNEL_OPTIN); + h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"); + Assert.equal(h.dataset(), RELEASE_CHANNEL_OPTOUT); + + // Check that registeredHistogram works properly + let registered = Telemetry.registeredHistograms(RELEASE_CHANNEL_OPTIN, []); + registered = new Set(registered); + Assert.ok(registered.has("TELEMETRY_TEST_FLAG")); + Assert.ok(registered.has("TELEMETRY_TEST_RELEASE_OPTIN")); + Assert.ok(registered.has("TELEMETRY_TEST_RELEASE_OPTOUT")); + registered = Telemetry.registeredHistograms(RELEASE_CHANNEL_OPTOUT, []); + registered = new Set(registered); + Assert.ok(!registered.has("TELEMETRY_TEST_FLAG")); + Assert.ok(!registered.has("TELEMETRY_TEST_RELEASE_OPTIN")); + Assert.ok(registered.has("TELEMETRY_TEST_RELEASE_OPTOUT")); + + // Check that registeredKeyedHistograms works properly + registered = Telemetry.registeredKeyedHistograms(RELEASE_CHANNEL_OPTIN, []); + registered = new Set(registered); + Assert.ok(registered.has("TELEMETRY_TEST_KEYED_FLAG")); + Assert.ok(registered.has("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT")); + registered = Telemetry.registeredKeyedHistograms(RELEASE_CHANNEL_OPTOUT, []); + registered = new Set(registered); + Assert.ok(!registered.has("TELEMETRY_TEST_KEYED_FLAG")); + Assert.ok(registered.has("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT")); +}); + +add_task({ + skip_if: () => gIsAndroid +}, +function* test_subsession() { + const ID = "TELEMETRY_TEST_COUNT"; + const FLAG = "TELEMETRY_TEST_FLAG"; + let h = Telemetry.getHistogramById(ID); + let flag = Telemetry.getHistogramById(FLAG); + + // Both original and duplicate should start out the same. + h.clear(); + let snapshot = Telemetry.histogramSnapshots; + let subsession = Telemetry.snapshotSubsessionHistograms(); + Assert.ok(!(ID in snapshot)); + Assert.ok(!(ID in subsession)); + + // They should instantiate and pick-up the count. + h.add(1); + snapshot = Telemetry.histogramSnapshots; + subsession = Telemetry.snapshotSubsessionHistograms(); + Assert.ok(ID in snapshot); + Assert.ok(ID in subsession); + Assert.equal(snapshot[ID].sum, 1); + Assert.equal(subsession[ID].sum, 1); + + // They should still reset properly. + h.clear(); + snapshot = Telemetry.histogramSnapshots; + subsession = Telemetry.snapshotSubsessionHistograms(); + Assert.ok(!(ID in snapshot)); + Assert.ok(!(ID in subsession)); + + // Both should instantiate and pick-up the count. + h.add(1); + snapshot = Telemetry.histogramSnapshots; + subsession = Telemetry.snapshotSubsessionHistograms(); + Assert.equal(snapshot[ID].sum, 1); + Assert.equal(subsession[ID].sum, 1); + + // Check that we are able to only reset the duplicate histogram. + h.clear(true); + snapshot = Telemetry.histogramSnapshots; + subsession = Telemetry.snapshotSubsessionHistograms(); + Assert.ok(ID in snapshot); + Assert.ok(ID in subsession); + Assert.equal(snapshot[ID].sum, 1); + Assert.equal(subsession[ID].sum, 0); + + // Both should register the next count. + h.add(1); + snapshot = Telemetry.histogramSnapshots; + subsession = Telemetry.snapshotSubsessionHistograms(); + Assert.equal(snapshot[ID].sum, 2); + Assert.equal(subsession[ID].sum, 1); + + // Retrieve a subsession snapshot and pass the flag to + // clear subsession histograms too. + h.clear(); + flag.clear(); + h.add(1); + flag.add(1); + snapshot = Telemetry.histogramSnapshots; + subsession = Telemetry.snapshotSubsessionHistograms(true); + Assert.ok(ID in snapshot); + Assert.ok(ID in subsession); + Assert.ok(FLAG in snapshot); + Assert.ok(FLAG in subsession); + Assert.equal(snapshot[ID].sum, 1); + Assert.equal(subsession[ID].sum, 1); + Assert.equal(snapshot[FLAG].sum, 1); + Assert.equal(subsession[FLAG].sum, 1); + + // The next subsesssion snapshot should show the histograms + // got reset. + snapshot = Telemetry.histogramSnapshots; + subsession = Telemetry.snapshotSubsessionHistograms(); + Assert.ok(ID in snapshot); + Assert.ok(ID in subsession); + Assert.ok(FLAG in snapshot); + Assert.ok(FLAG in subsession); + Assert.equal(snapshot[ID].sum, 1); + Assert.equal(subsession[ID].sum, 0); + Assert.equal(snapshot[FLAG].sum, 1); + Assert.equal(subsession[FLAG].sum, 0); +}); + +add_task({ + skip_if: () => gIsAndroid +}, +function* test_keyed_subsession() { + let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_FLAG"); + const KEY = "foo"; + + // Both original and subsession should start out the same. + h.clear(); + Assert.ok(!(KEY in h.snapshot())); + Assert.ok(!(KEY in h.subsessionSnapshot())); + Assert.equal(h.snapshot(KEY).sum, 0); + Assert.equal(h.subsessionSnapshot(KEY).sum, 0); + + // Both should register the flag. + h.add(KEY, 1); + Assert.ok(KEY in h.snapshot()); + Assert.ok(KEY in h.subsessionSnapshot()); + Assert.equal(h.snapshot(KEY).sum, 1); + Assert.equal(h.subsessionSnapshot(KEY).sum, 1); + + // Check that we are able to only reset the subsession histogram. + h.clear(true); + Assert.ok(KEY in h.snapshot()); + Assert.ok(!(KEY in h.subsessionSnapshot())); + Assert.equal(h.snapshot(KEY).sum, 1); + Assert.equal(h.subsessionSnapshot(KEY).sum, 0); + + // Setting the flag again should make both match again. + h.add(KEY, 1); + Assert.ok(KEY in h.snapshot()); + Assert.ok(KEY in h.subsessionSnapshot()); + Assert.equal(h.snapshot(KEY).sum, 1); + Assert.equal(h.subsessionSnapshot(KEY).sum, 1); + + // Check that "snapshot and clear" works properly. + let snapshot = h.snapshot(); + let subsession = h.snapshotSubsessionAndClear(); + Assert.ok(KEY in snapshot); + Assert.ok(KEY in subsession); + Assert.equal(snapshot[KEY].sum, 1); + Assert.equal(subsession[KEY].sum, 1); + + subsession = h.subsessionSnapshot(); + Assert.ok(!(KEY in subsession)); + Assert.equal(h.subsessionSnapshot(KEY).sum, 0); +}); diff --git a/toolkit/components/telemetry/tests/unit/xpcshell.ini b/toolkit/components/telemetry/tests/unit/xpcshell.ini new file mode 100644 index 000000000..74067580a --- /dev/null +++ b/toolkit/components/telemetry/tests/unit/xpcshell.ini @@ -0,0 +1,63 @@ +[DEFAULT] +head = head.js +tail = +firefox-appdir = browser +# The *.xpi files are only needed for test_TelemetryEnvironment.js, but +# xpcshell fails to install tests if we move them under the test entry. +support-files = + ../search/chrome.manifest + ../search/searchTest.jar + dictionary.xpi + experiment.xpi + extension.xpi + extension-2.xpi + engine.xml + system.xpi + restartless.xpi + theme.xpi + !/toolkit/mozapps/extensions/test/xpcshell/head_addons.js +generated-files = + dictionary.xpi + experiment.xpi + extension.xpi + extension-2.xpi + system.xpi + restartless.xpi + theme.xpi + +[test_nsITelemetry.js] +[test_SubsessionChaining.js] +tags = addons +[test_TelemetryEnvironment.js] +skip-if = os == "android" +tags = addons +[test_PingAPI.js] +skip-if = os == "android" +[test_TelemetryFlagClear.js] +[test_TelemetryLateWrites.js] +[test_TelemetryLockCount.js] +[test_TelemetryLog.js] +[test_TelemetryController.js] +tags = addons +[test_TelemetryController_idle.js] +[test_TelemetryControllerShutdown.js] +tags = addons +[test_TelemetryStopwatch.js] +[test_TelemetryControllerBuildID.js] +[test_TelemetrySendOldPings.js] +skip-if = os == "android" # Disabled due to intermittent orange on Android +tags = addons +[test_TelemetrySession.js] +tags = addons +[test_ThreadHangStats.js] +run-sequentially = Bug 1046307, test can fail intermittently when CPU load is high +[test_TelemetrySend.js] +[test_ChildHistograms.js] +skip-if = os == "android" +tags = addons +[test_TelemetryReportingPolicy.js] +tags = addons +[test_TelemetryScalars.js] +[test_TelemetryTimestamps.js] +skip-if = toolkit == 'android' +[test_TelemetryEvents.js] |