diff options
Diffstat (limited to 'toolkit/components/telemetry/tests/utils/TelemetryTestUtils.sys.mjs')
-rw-r--r-- | toolkit/components/telemetry/tests/utils/TelemetryTestUtils.sys.mjs | 428 |
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}` + ); + } + } + }, +}; |