430 lines
14 KiB
JavaScript
430 lines
14 KiB
JavaScript
/* 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}`
|
|
);
|
|
}
|
|
}
|
|
},
|
|
};
|