/* 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. Filtered events: ${JSON.stringify( filtered )}` ); 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}` ); } } }, };