Source code

Revision control

Copy as Markdown

Other Tools

/* Any copyright is dedicated to the Public Domain.
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}`
);
}
}
},
};