Bug 1527299 - Add TelemetryTestUtils.assertEvents r=janerik
authorChris H-C <chutten@mozilla.com>
Wed, 27 Feb 2019 10:26:02 +0000
changeset 519663 574433d7795ed30b77cecaf20a0ba7695961ca0d
parent 519662 484f62205c8daf3510511e60bc331d189321a02a
child 519664 034dce9d9393f0b0a3a9ac7f61bebe4e34ff2915
push id10862
push userffxbld-merge
push dateMon, 11 Mar 2019 13:01:11 +0000
treeherdermozilla-beta@a2e7f5c935da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjanerik
bugs1527299
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1527299 - Add TelemetryTestUtils.assertEvents r=janerik This adds a general-purpose testing API for testing Telemetry Events, and a handy extra API (assertNumberOfEvents) for if you just want to count. Differential Revision: https://phabricator.services.mozilla.com/D20863
toolkit/components/telemetry/tests/utils/TelemetryTestUtils.jsm
--- a/toolkit/components/telemetry/tests/utils/TelemetryTestUtils.jsm
+++ b/toolkit/components/telemetry/tests/utils/TelemetryTestUtils.jsm
@@ -69,34 +69,130 @@ var TelemetryTestUtils = {
       Services.telemetry.getSnapshotForScalars("main", aClear)[aProcessName];
     Services.telemetry.canRecordExtended = currentExtended;
     return scalars || {};
   },
 
   /* Events */
 
   /**
-   * Asserts if snapshotted events telemetry match the expected values.
+   * 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);
+  },
+
+  /**
+   * Asserts that, after optional filtering, the current events snapshot
+   * matches expectedEvents.
    *
-   * @param {Array} events Snapshotted telemetry events to test.
-   * @param {Array} expectedEvents The expected event data.
+   * @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(events, expectedEvents) {
-    if (!Services.telemetry.canRecordExtended) {
-      console.log("Not asserting event telemetry - canRecordExtended is disabled.");
+  assertEvents(expectedEvents, filter = {}, {clear = true, process = "parent"} = {}) {
+    // Step 0: Snapshot and clear.
+    let snapshots = Services.telemetry.snapshotEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, 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.
+    let {category: filterCategory, method: filterMethod, object: filterObject} = filter;
+    let matches = (expected, actual) => {
+      if (expected === undefined) {
+        return true;
+      } else if (expected instanceof 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(expectedEvents.length, filtered.length,
+      "After filtering we must have the expected number of events.");
+    if (expectedEvents.length === 0) {
+      // Job's done!
       return;
     }
 
-    Assert.equal(events.length, expectedEvents.length, "Should have matching amount of events.");
+    // 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];
 
-    // Strip timestamps from the events for easier comparison.
-    events = events.map(e => e.slice(1));
+      // 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");
+      }
 
-    for (let i = 0; i < events.length; ++i) {
-      Assert.deepEqual(events[i], expectedEvents[i], "Events should match.");
+      // 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.
    *