summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/tests/utils/TelemetryTestUtils.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/telemetry/tests/utils/TelemetryTestUtils.sys.mjs')
-rw-r--r--toolkit/components/telemetry/tests/utils/TelemetryTestUtils.sys.mjs428
1 files changed, 428 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/tests/utils/TelemetryTestUtils.sys.mjs b/toolkit/components/telemetry/tests/utils/TelemetryTestUtils.sys.mjs
new file mode 100644
index 0000000000..075c0afbe7
--- /dev/null
+++ b/toolkit/components/telemetry/tests/utils/TelemetryTestUtils.sys.mjs
@@ -0,0 +1,428 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+import { Assert } from "resource://testing-common/Assert.sys.mjs";
+
+export var TelemetryTestUtils = {
+ /* Scalars */
+
+ /**
+ * A helper that asserts the value of a scalar.
+ *
+ * @param {Object} scalars The snapshot of the scalars.
+ * @param {String} scalarName The name of the scalar to check.
+ * @param {Boolean|Number|String} value The expected value for the scalar.
+ * @param {String} msg The message to print when checking the value.
+ */
+ assertScalar(scalars, scalarName, value, msg) {
+ Assert.equal(scalars[scalarName], value, msg);
+ },
+
+ /**
+ * A helper that asserts a scalar is not set.
+ *
+ * @param {Object} scalars The snapshot of the scalars.
+ * @param {String} scalarName The name of the scalar to check.
+ */
+ assertScalarUnset(scalars, scalarName) {
+ Assert.ok(!(scalarName in scalars), scalarName + " must not be reported.");
+ },
+
+ /**
+ * Asserts if the snapshotted keyed scalars contain the expected
+ * data.
+ *
+ * @param {Object} scalars The snapshot of the keyed scalars.
+ * @param {String} scalarName The name of the keyed scalar to check.
+ * @param {String} key The key that must be within the keyed scalar.
+ * @param {String|Boolean|Number} expectedValue The expected value for the
+ * provided key in the scalar.
+ */
+ assertKeyedScalar(scalars, scalarName, key, expectedValue) {
+ Assert.ok(scalarName in scalars, scalarName + " must be recorded.");
+ Assert.ok(
+ key in scalars[scalarName],
+ scalarName + " must contain the '" + key + "' key."
+ );
+ Assert.equal(
+ scalars[scalarName][key],
+ expectedValue,
+ scalarName + "['" + key + "'] must contain the expected value"
+ );
+ },
+
+ /**
+ * Returns a snapshot of scalars from the specified process.
+ *
+ * @param {String} aProcessName Name of the process. Could be parent or
+ * something else.
+ * @param {boolean} [aKeyed] Set to true if keyed scalars rather than normal
+ * scalars should be snapshotted.
+ * @param {boolean} [aClear] Set to true to clear the scalars once the snapshot
+ * has been obtained.
+ * @param {Number} aChannel The channel dataset type from nsITelemetry.
+ * @returns {Object} The snapshotted scalars from the parent process.
+ */
+ getProcessScalars(
+ aProcessName,
+ aKeyed = false,
+ aClear = false,
+ aChannel = Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS
+ ) {
+ const extended = aChannel == Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS;
+ const currentExtended = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = extended;
+ const scalars = aKeyed
+ ? Services.telemetry.getSnapshotForKeyedScalars("main", aClear)[
+ aProcessName
+ ]
+ : Services.telemetry.getSnapshotForScalars("main", aClear)[aProcessName];
+ Services.telemetry.canRecordExtended = currentExtended;
+ return scalars || {};
+ },
+
+ /* Events */
+
+ /**
+ * Asserts that the number of events, after filtering, is equal to numEvents.
+ *
+ * @param {Number} numEvents The number of events to assert.
+ * @param {Object} filter As per assertEvents.
+ * @param {Object} options As per assertEvents.
+ */
+ assertNumberOfEvents(numEvents, filter, options) {
+ // Create an array of empty objects of length numEvents
+ TelemetryTestUtils.assertEvents(
+ Array.from({ length: numEvents }, () => ({})),
+ filter,
+ options
+ );
+ },
+
+ /**
+ * Returns the events in a snapshot, after optional filtering.
+ *
+ * @param {Object} filter An object of strings or RegExps for first filtering
+ * the event snapshot. Of the form {category, method, object}.
+ * Absent filters filter nothing.
+ * @param {Object} options An object containing any of
+ * - process {string} the process to examine. Default parent.
+ */
+ getEvents(filter = {}, { process = "parent" } = {}) {
+ // Step 0: Snapshot and clear.
+ let snapshots = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+
+ if (!(process in snapshots)) {
+ return [];
+ }
+
+ let snapshot = snapshots[process];
+
+ // Step 1: Filter.
+ // Shared code with the below function
+ let {
+ category: filterCategory,
+ method: filterMethod,
+ object: filterObject,
+ } = filter;
+ let matches = (expected, actual) => {
+ if (expected === undefined) {
+ return true;
+ } else if (expected && expected.test) {
+ // Possibly a RegExp.
+ return expected.test(actual);
+ } else if (typeof expected === "function") {
+ return expected(actual);
+ }
+ return expected === actual;
+ };
+
+ return snapshot
+ .map(([, /* timestamp */ category, method, object, value, extra]) => {
+ // We don't care about the `timestamp` value.
+ // Tests that examine that value should use `snapshotEvents` directly.
+ return [category, method, object, value, extra];
+ })
+ .filter(([category, method, object]) => {
+ return (
+ matches(filterCategory, category) &&
+ matches(filterMethod, method) &&
+ matches(filterObject, object)
+ );
+ })
+ .map(([category, method, object, value, extra]) => {
+ return { category, method, object, value, extra };
+ });
+ },
+
+ /**
+ * Asserts that, after optional filtering, the current events snapshot
+ * matches expectedEvents.
+ *
+ * @param {Array} expectedEvents An array of event structures of the form
+ * [category, method, object, value, extra]
+ * or the same as an object with fields named as above.
+ * The array can be empty to assert that there are no events
+ * that match the filter.
+ * Each field can be absent/undefined (to match
+ * everything), a string or null (to match that value), a
+ * RegExp to match what it can match, or a function which
+ * matches by returning true when called with the field.
+ * `extra` is slightly different. If present it must be an
+ * object whose fields are treated the same way as the others.
+ * @param {Object} filter An object of strings or RegExps for first filtering
+ * the event snapshot. Of the form {category, method, object}.
+ * Absent filters filter nothing.
+ * @param {Object} options An object containing any of
+ * - clear {bool} clear events. Default true.
+ * - process {string} the process to examine. Default parent.
+ */
+ assertEvents(
+ expectedEvents,
+ filter = {},
+ { clear = true, process = "parent" } = {}
+ ) {
+ // Step 0: Snapshot and clear.
+ let snapshots = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ clear
+ );
+ if (expectedEvents.length === 0 && !(process in snapshots)) {
+ // Job's done!
+ return;
+ }
+ Assert.ok(
+ process in snapshots,
+ `${process} must be in snapshot. Has [${Object.keys(snapshots)}].`
+ );
+ let snapshot = snapshots[process];
+
+ // Step 1: Filter.
+ // Shared code with the above function
+ let {
+ category: filterCategory,
+ method: filterMethod,
+ object: filterObject,
+ } = filter;
+ let matches = (expected, actual) => {
+ if (expected === undefined) {
+ return true;
+ } else if (expected && expected.test) {
+ // Possibly a RegExp.
+ return expected.test(actual);
+ } else if (typeof expected === "function") {
+ return expected(actual);
+ }
+ return expected === actual;
+ };
+
+ let filtered = snapshot
+ .map(([, /* timestamp */ category, method, object, value, extra]) => {
+ // We don't care about the `timestamp` value.
+ // Tests that examine that value should use `snapshotEvents` directly.
+ return [category, method, object, value, extra];
+ })
+ .filter(([category, method, object]) => {
+ return (
+ matches(filterCategory, category) &&
+ matches(filterMethod, method) &&
+ matches(filterObject, object)
+ );
+ });
+
+ // Step 2: Match.
+ Assert.equal(
+ filtered.length,
+ expectedEvents.length,
+ "After filtering we must have the expected number of events."
+ );
+ if (expectedEvents.length === 0) {
+ // Job's done!
+ return;
+ }
+
+ // Transform object-type expected events to array-type to match snapshot.
+ if (!Array.isArray(expectedEvents[0])) {
+ expectedEvents = expectedEvents.map(
+ ({ category, method, object, value, extra }) => [
+ category,
+ method,
+ object,
+ value,
+ extra,
+ ]
+ );
+ }
+
+ const FIELD_NAMES = ["category", "method", "object", "value", "extra"];
+ const EXTRA_INDEX = 4;
+ for (let i = 0; i < expectedEvents.length; ++i) {
+ let expected = expectedEvents[i];
+ let actual = filtered[i];
+
+ // Match everything up to `extra`
+ for (let j = 0; j < EXTRA_INDEX; ++j) {
+ if (expected[j] === undefined) {
+ // Don't spam the assert log with unspecified fields.
+ continue;
+ }
+ Assert.report(
+ !matches(expected[j], actual[j]),
+ actual[j],
+ expected[j],
+ `${FIELD_NAMES[j]} in event ${actual[0]}#${actual[1]}#${actual[2]} must match.`,
+ "matches"
+ );
+ }
+
+ // Match extra
+ if (
+ expected.length > EXTRA_INDEX &&
+ expected[EXTRA_INDEX] !== undefined
+ ) {
+ Assert.ok(
+ actual.length > EXTRA_INDEX,
+ `Actual event ${actual[0]}#${actual[1]}#${actual[2]} expected to have extra.`
+ );
+ let expectedExtra = expected[EXTRA_INDEX];
+ let actualExtra = actual[EXTRA_INDEX];
+ for (let [key, value] of Object.entries(expectedExtra)) {
+ Assert.ok(
+ key in actualExtra,
+ `Expected key ${key} must be in actual extra. Actual keys: [${Object.keys(
+ actualExtra
+ )}].`
+ );
+ Assert.report(
+ !matches(value, actualExtra[key]),
+ actualExtra[key],
+ value,
+ `extra[${key}] must match in event ${actual[0]}#${actual[1]}#${actual[2]}.`,
+ "matches"
+ );
+ }
+ }
+ }
+ },
+
+ /* Histograms */
+
+ /**
+ * Clear and get the named histogram.
+ *
+ * @param {String} name The name of the histogram
+ * @returns {Object} The obtained histogram.
+ */
+ getAndClearHistogram(name) {
+ let histogram = Services.telemetry.getHistogramById(name);
+ histogram.clear();
+ return histogram;
+ },
+
+ /**
+ * Clear and get the named keyed histogram.
+ *
+ * @param {String} name The name of the keyed histogram
+ * @returns {Object} The obtained keyed histogram.
+ */
+ getAndClearKeyedHistogram(name) {
+ let histogram = Services.telemetry.getKeyedHistogramById(name);
+ histogram.clear();
+ return histogram;
+ },
+
+ /**
+ * Assert that the histogram index is the right value. It expects that
+ * other indexes are all zero.
+ *
+ * @param {Object} histogram The histogram to check.
+ * @param {Number} index The index to check against the expected value.
+ * @param {Number} expected The expected value of the index.
+ */
+ assertHistogram(histogram, index, expected) {
+ const snapshot = histogram.snapshot();
+ let found = false;
+ for (let [i, val] of Object.entries(snapshot.values)) {
+ if (i == index) {
+ found = true;
+ Assert.equal(
+ val,
+ expected,
+ `expected counts should match for ${histogram.name()} at index ${i}`
+ );
+ } else {
+ Assert.equal(
+ val,
+ 0,
+ `unexpected counts should be zero for ${histogram.name()} at index ${i}`
+ );
+ }
+ }
+ Assert.ok(
+ found,
+ `Should have found an entry for ${histogram.name()} at index ${index}`
+ );
+ },
+
+ /**
+ * Assert that a key within a keyed histogram contains the required sum.
+ *
+ * @param {Object} histogram The keyed histogram to check.
+ * @param {String} key The key to check.
+ * @param {Number} [expected] The expected sum for the key.
+ */
+ assertKeyedHistogramSum(histogram, key, expected) {
+ const snapshot = histogram.snapshot();
+ if (expected === undefined) {
+ Assert.ok(
+ !(key in snapshot),
+ `The histogram ${histogram.name()} must not contain ${key}.`
+ );
+ return;
+ }
+ Assert.ok(
+ key in snapshot,
+ `The histogram ${histogram.name()} must contain ${key}.`
+ );
+ Assert.equal(
+ snapshot[key].sum,
+ expected,
+ `The key ${key} must contain the expected sum in ${histogram.name()}.`
+ );
+ },
+
+ /**
+ * Assert that the value of a key within a keyed histogram is the right value.
+ * It expects that other values are all zero.
+ *
+ * @param {Object} histogram The keyed histogram to check.
+ * @param {String} key The key to check.
+ * @param {Number} index The index to check against the expected value.
+ * @param {Number} [expected] The expected values for the key.
+ */
+ assertKeyedHistogramValue(histogram, key, index, expected) {
+ const snapshot = histogram.snapshot();
+ if (!(key in snapshot)) {
+ Assert.ok(false, `The histogram ${histogram.name()} must contain ${key}`);
+ return;
+ }
+ for (let [i, val] of Object.entries(snapshot[key].values)) {
+ if (i == index) {
+ Assert.equal(
+ val,
+ expected,
+ `expected counts should match for ${histogram.name()} at index ${i}`
+ );
+ } else {
+ Assert.equal(
+ val,
+ 0,
+ `unexpected counts should be zero for ${histogram.name()} at index ${i}`
+ );
+ }
+ }
+ },
+};