summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/tests/unit
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /toolkit/components/telemetry/tests/unit
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-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')
-rw-r--r--toolkit/components/telemetry/tests/unit/.eslintrc.js7
-rw-r--r--toolkit/components/telemetry/tests/unit/TelemetryArchiveTesting.jsm86
-rw-r--r--toolkit/components/telemetry/tests/unit/engine.xml7
-rw-r--r--toolkit/components/telemetry/tests/unit/head.js319
-rw-r--r--toolkit/components/telemetry/tests/unit/test_ChildHistograms.js107
-rw-r--r--toolkit/components/telemetry/tests/unit/test_PingAPI.js502
-rw-r--r--toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js236
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryController.js507
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryControllerBuildID.js70
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js70
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js73
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js1528
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js249
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryFlagClear.js14
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryLateWrites.js127
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryLockCount.js53
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryLog.js51
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js268
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js574
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetrySend.js427
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js547
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetrySession.js2029
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryStopwatch.js156
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js77
-rw-r--r--toolkit/components/telemetry/tests/unit/test_ThreadHangStats.js102
-rw-r--r--toolkit/components/telemetry/tests/unit/test_nsITelemetry.js883
-rw-r--r--toolkit/components/telemetry/tests/unit/xpcshell.ini63
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]