diff --git a/services/metrics/Makefile.in b/services/metrics/Makefile.in index b46acf5f2d4b..d32bcb72347f 100644 --- a/services/metrics/Makefile.in +++ b/services/metrics/Makefile.in @@ -10,9 +10,11 @@ VPATH = @srcdir@ include $(DEPTH)/config/autoconf.mk modules := \ + dataprovider.jsm \ $(NULL) testing_modules := \ + mocks.jsm \ $(NULL) TEST_DIRS += tests diff --git a/services/metrics/dataprovider.jsm b/services/metrics/dataprovider.jsm new file mode 100644 index 000000000000..c82af71167c1 --- /dev/null +++ b/services/metrics/dataprovider.jsm @@ -0,0 +1,476 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "MetricsCollectionResult", + "MetricsMeasurement", + "MetricsProvider", +]; + +const {utils: Cu} = Components; + +Cu.import("resource://gre/modules/commonjs/promise/core.js"); +Cu.import("resource://services-common/log4moz.js"); + + +/** + * Represents a measurement of data. + * + * This is how data is recorded and represented. Each instance of this type + * represents a related set of data. + * + * Each data set has some basic metadata associated with it. This includes a + * name and version. + * + * This type is meant to be an abstract base type. Child types should define + * a `fields` property which is a mapping of field names to metadata describing + * that field. This field constitutes the "schema" of the measurement/type. + * + * Data is added to instances by calling `setValue()`. Values are validated + * against the schema at add time. + * + * Field Specification + * =================== + * + * The `fields` property is a mapping of string field names to a mapping of + * metadata describing the field. This mapping can have the following + * properties: + * + * type -- A string corresponding to the TYPE_* property name describing a + * field type. The TYPE_* properties are defined on this type. e.g. + * "TYPE_STRING". + * + * optional -- If true, this field is optional. If omitted, the field is + * required. + * + * @param name + * (string) Name of this data set. + * @param version + * (Number) Integer version of the data in this set. + */ +this.MetricsMeasurement = function MetricsMeasurement(name, version) { + if (!this.fields) { + throw new Error("fields not defined on instance. You are likely using " + + "this type incorrectly."); + } + + if (!name) { + throw new Error("Must define a name for this measurement."); + } + + if (!version) { + throw new Error("Must define a version for this measurement."); + } + + if (!Number.isInteger(version)) { + throw new Error("version must be an integer: " + version); + } + + this.name = name; + this.version = version; + + this.values = new Map(); +} + +MetricsMeasurement.prototype = { + /** + * An unsigned integer field stored in 32 bits. + * + * This holds values from 0 to 2^32 - 1. + */ + TYPE_UINT32: { + validate: function validate(value) { + if (!Number.isInteger(value)) { + throw new Error("UINT32 field expects an integer. Got " + value); + } + + if (value < 0) { + throw new Error("UINT32 field expects a positive integer. Got " + value); + } + + if (value >= 0xffffffff) { + throw new Error("Value is too large to fit within 32 bits: " + value); + } + }, + }, + + /** + * A string field. + * + * Values must be valid UTF-8 strings. + */ + TYPE_STRING: { + validate: function validate(value) { + if (typeof(value) != "string") { + throw new Error("STRING field expects a string. Got " + typeof(value)); + } + }, + }, + + /** + * Set the value of a field. + * + * This is ultimately how fields are set. All field sets should go through + * this function. + * + * Values are validated when they are set. If the value passed does not + * validate against the field's specification, an Error will be thrown. + * + * @param name + * (string) The name of the field whose value to set. + * @param value + * The value to set the field to. + */ + setValue: function setValue(name, value) { + if (!this.fields[name]) { + throw new Error("Attempting to set unknown field: " + name); + } + + let type = this.fields[name].type; + + if (!(type in this)) { + throw new Error("Unknown field type: " + type); + } + + this[type].validate(value); + this.values.set(name, value); + }, + + /** + * Obtain the value of a named field. + * + * @param name + * (string) The name of the field to retrieve. + */ + getValue: function getValue(name) { + return this.values.get(name); + }, + + /** + * Validate that this instance is in conformance with the specification. + * + * This ensures all required fields are present. Field value validation + * occurs when individual fields are set. + */ + validate: function validate() { + for (let field in this.fields) { + let spec = this.fields[field]; + + if (!spec.optional && !(field in this.values)) { + throw new Error("Required field not defined: " + field); + } + } + }, + + toJSON: function toJSON() { + let fields = {}; + for (let [k, v] of this.values) { + fields[k] = v; + } + + return { + name: this.name, + version: this.version, + fields: fields, + }; + }, +}; + +Object.freeze(MetricsMeasurement.prototype); + + +/** + * Entity which provides metrics data for recording. + * + * This essentially provides an interface that different systems must implement + * to provide collected metrics data. + * + * This type consists of various collect* functions. These functions are called + * by the metrics collector at different points during the application's + * lifetime. These functions return a `MetricsCollectionResult` instance. + * This type behaves a lot like a promise. It has a `onFinished()` that can chain + * deferred events until after the result is populated. + * + * Implementations of collect* functions should call `createResult()` to create + * a new `MetricsCollectionResult` instance. When called, they should + * initiate population of this instance. Once population has finished (perhaps + * asynchronously), they should call `finish()` on the instance. + * + * Receivers of created `MetricsCollectionResult` instances should wait + * until population has finished. They can do this by chaining on to the + * promise inside that instance by calling `onFinished()`. + * + * The collect* functions can return null to signify that they will never + * provide any data. This is the default implementation. An implemented + * collect* function should *never* return null. Instead, it should return + * a `MetricsCollectionResult` with expected measurements that has finished + * populating (i.e. an empty result). + * + * @param name + * (string) The name of this provider. + */ +this.MetricsProvider = function MetricsProvider(name) { + if (!name) { + throw new Error("MetricsProvider must have a name."); + } + + if (typeof(name) != "string") { + throw new Error("name must be a string. Got: " + typeof(name)); + } + + this._log = Log4Moz.repository.getLogger("Services.Metrics.MetricsProvider"); + + this.name = name; +} + +MetricsProvider.prototype = { + /** + * Collects constant measurements. + * + * Constant measurements are data that doesn't change during the lifetime of + * the application/process. The metrics collector only needs to call this + * once per `MetricsProvider` instance per process lifetime. + */ + collectConstantMeasurements: function collectConstantMeasurements() { + return null; + }, + + /** + * Create a new `MetricsCollectionResult` tied to this provider. + */ + createResult: function createResult() { + return new MetricsCollectionResult(this.name); + }, +}; + +Object.freeze(MetricsProvider.prototype); + + +/** + * Holds the result of metrics collection. + * + * This is the type eventually returned by the MetricsProvider.collect* + * functions. It holds all results and any state/errors that occurred while + * collecting. + * + * This type is essentially a container for `MetricsMeasurement` instances that + * provides some smarts useful for capturing state. + * + * The first things consumers of new instances should do is define the set of + * expected measurements this result will contain via `expectMeasurement`. If + * population of this instance is aborted or times out, downstream consumers + * will know there is missing data. + * + * Next, they should add empty `MetricsMeasurement` instances to it via + * `addMeasurement`. Finally, they should populate these measurements with + * `setValue`. + * + * It is preferred to populate via this type instead of directly on + * `MetricsMeasurement` instances so errors with data population can be + * captured and reported. + * + * Once population has finished, `finish()` must be called. + * + * @param name + * (string) The name of the provider this result came from. + */ +this.MetricsCollectionResult = function MetricsCollectionResult(name) { + if (!name || typeof(name) != "string") { + throw new Error("Must provide name argument to MetricsCollectionResult."); + } + + this._log = Log4Moz.repository.getLogger("Services.Metrics.MetricsCollectionResult"); + + this.name = name; + + this.measurements = new Map(); + this.expectedMeasurements = new Set(); + this.errors = []; + + this._deferred = Promise.defer(); +} + +MetricsCollectionResult.prototype = { + /** + * The Set of `MetricsMeasurement` names currently missing from this result. + */ + get missingMeasurements() { + let missing = new Set(); + + for (let name of this.expectedMeasurements) { + if (this.measurements.has(name)) { + continue; + } + + missing.add(name); + } + + return missing; + }, + + /** + * Record that this result is expected to provide a named measurement. + * + * This function should be called ASAP on new `MetricsCollectionResult` + * instances. It defines expectations about what data should be present. + * + * @param name + * (string) The name of the measurement this result should contain. + */ + expectMeasurement: function expectMeasurement(name) { + this.expectedMeasurements.add(name); + }, + + /** + * Add a `MetricsMeasurement` to this result. + */ + addMeasurement: function addMeasurement(data) { + if (!(data instanceof MetricsMeasurement)) { + throw new Error("addMeasurement expects a MetricsMeasurement instance."); + } + + if (!this.expectedMeasurements.has(data.name)) { + throw new Error("Not expecting this measurement: " + data.name); + } + + if (this.measurements.has(data.name)) { + throw new Error("Measurement of this name already present: " + data.name); + } + + this.measurements.set(data.name, data); + }, + + /** + * Sets the value of a field in a registered measurement instance. + * + * This is a convenience function to set a field on a measurement. If an + * error occurs, it will record that error in the errors container. + * + * Attempting to set a value on a measurement that does not exist results + * in an Error being thrown. Attempting a bad assignment on an existing + * measurement will not throw unless `rethrow` is true. + * + * @param name + * (string) The `MetricsMeasurement` on which to set the value. + * @param field + * (string) The field we are setting. + * @param value + * The value being set. + * @param rethrow + * (bool) Whether to rethrow any errors encountered. + * + * @return bool + * Whether the assignment was successful. + */ + setValue: function setValue(name, field, value, rethrow=false) { + let m = this.measurements.get(name); + if (!m) { + throw new Error("Attempting to operate on an undefined measurement: " + + name); + } + + try { + m.setValue(field, value); + return true; + } catch (ex) { + this.addError(ex); + + if (rethrow) { + throw ex; + } + + return false; + } + }, + + /** + * Record an error that was encountered when populating this result. + */ + addError: function addError(error) { + this.errors.push(error); + }, + + /** + * Aggregate another MetricsCollectionResult into this one. + * + * Instances can only be aggregated together if they belong to the same + * provider (they have the same name). + */ + aggregate: function aggregate(other) { + if (!(other instanceof MetricsCollectionResult)) { + throw new Error("aggregate expects a MetricsCollectionResult instance."); + } + + if (this.name != other.name) { + throw new Error("Can only aggregate MetricsCollectionResult from " + + "the same provider. " + this.name + " != " + other.name); + } + + for (let name of other.expectedMeasurements) { + this.expectedMeasurements.add(name); + } + + for (let [name, m] of other.measurements) { + if (this.measurements.has(name)) { + throw new Error("Incoming result has same measurement as us: " + name); + } + + this.measurements.set(name, m); + } + + this.errors = this.errors.concat(other.errors); + }, + + toJSON: function toJSON() { + let o = { + measurements: {}, + missing: [], + errors: [], + }; + + for (let [name, value] of this.measurements) { + o.measurements[name] = value; + } + + for (let missing of this.missingMeasurements) { + o.missing.push(missing); + } + + for (let error of this.errors) { + if (error.message) { + o.errors.push(error.message); + } else { + o.errors.push(error); + } + } + + return o; + }, + + /** + * Signal that population of the result has finished. + * + * This will resolve the internal promise. + */ + finish: function finish() { + this._deferred.resolve(this); + }, + + /** + * Chain deferred behavior until after the result has finished population. + * + * This is a wrapped around the internal promise's `then`. + * + * We can't call this "then" because the core promise library will get + * confused. + */ + onFinished: function onFinished(onFulfill, onError) { + return this._deferred.promise.then(onFulfill, onError); + }, +}; + +Object.freeze(MetricsCollectionResult.prototype); + diff --git a/services/metrics/modules-testing/mocks.jsm b/services/metrics/modules-testing/mocks.jsm new file mode 100644 index 000000000000..729501dc030f --- /dev/null +++ b/services/metrics/modules-testing/mocks.jsm @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "DummyMeasurement", + "DummyProvider", +]; + +const {utils: Cu} = Components; + +Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm"); + +this.DummyMeasurement = function DummyMeasurement(name="DummyMeasurement") { + MetricsMeasurement.call(this, name, 2); +} +DummyMeasurement.prototype = { + __proto__: MetricsMeasurement.prototype, + + fields: { + "string": { + type: "TYPE_STRING", + }, + + "uint32": { + type: "TYPE_UINT32", + optional: true, + }, + }, +}; + + +this.DummyProvider = function DummyProvider(name="DummyProvider") { + MetricsProvider.call(this, name); + + this.constantMeasurementName = "DummyMeasurement"; +} +DummyProvider.prototype = { + __proto__: MetricsProvider.prototype, + + collectConstantMeasurements: function collectConstantMeasurements() { + let result = this.createResult(); + result.expectMeasurement(this.constantMeasurementName); + result.addMeasurement(new DummyMeasurement(this.constantMeasurementName)); + + result.setValue(this.constantMeasurementName, "string", "foo"); + result.setValue(this.constantMeasurementName, "uint32", 24); + + result.finish(); + + return result; + }, +}; diff --git a/services/metrics/tests/xpcshell/test_load_modules.js b/services/metrics/tests/xpcshell/test_load_modules.js index ce91fbab24fe..9d17c15c8c3e 100644 --- a/services/metrics/tests/xpcshell/test_load_modules.js +++ b/services/metrics/tests/xpcshell/test_load_modules.js @@ -4,9 +4,11 @@ "use strict"; const modules = [ + "dataprovider.jsm", ]; const test_modules = [ + "mocks.jsm", ]; function run_test() { diff --git a/services/metrics/tests/xpcshell/test_metrics_collection_result.js b/services/metrics/tests/xpcshell/test_metrics_collection_result.js new file mode 100644 index 000000000000..3e61a7928b6e --- /dev/null +++ b/services/metrics/tests/xpcshell/test_metrics_collection_result.js @@ -0,0 +1,231 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const {utils: Cu} = Components; + +Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm"); +Cu.import("resource://testing-common/services/metrics/mocks.jsm"); + + +function run_test() { + run_next_test(); +}; + +add_test(function test_constructor() { + let result = new MetricsCollectionResult("foo"); + do_check_eq(result.name, "foo"); + + let failed = false; + try { + new MetricsCollectionResult(); + } catch(ex) { + do_check_true(ex.message.startsWith("Must provide name argument to Metrics")); + failed = true; + } finally { + do_check_true(failed); + } + + run_next_test(); +}); + +add_test(function test_expected_measurements() { + let result = new MetricsCollectionResult("foo"); + do_check_eq(result.missingMeasurements.size(), 0); + + result.expectMeasurement("foo"); + result.expectMeasurement("bar"); + do_check_eq(result.missingMeasurements.size(), 2); + do_check_true(result.missingMeasurements.has("foo")); + do_check_true(result.missingMeasurements.has("bar")); + + run_next_test(); +}); + +add_test(function test_missing_measurements() { + let result = new MetricsCollectionResult("foo"); + + let missing = result.missingMeasurements; + do_check_eq(missing.size(), 0); + + result.expectMeasurement("DummyMeasurement"); + result.expectMeasurement("b"); + + missing = result.missingMeasurements; + do_check_eq(missing.size(), 2); + do_check_true(missing.has("DummyMeasurement")); + do_check_true(missing.has("b")); + + result.addMeasurement(new DummyMeasurement()); + missing = result.missingMeasurements; + do_check_eq(missing.size(), 1); + do_check_true(missing.has("b")); + + run_next_test(); +}); + +add_test(function test_add_measurement() { + let result = new MetricsCollectionResult("add_measurement"); + + let failed = false; + try { + result.addMeasurement(new DummyMeasurement()); + } catch (ex) { + do_check_true(ex.message.startsWith("Not expecting this measurement")); + failed = true; + } finally { + do_check_true(failed); + failed = false; + } + + result.expectMeasurement("foo"); + result.addMeasurement(new DummyMeasurement("foo")); + + do_check_eq(result.measurements.size(), 1); + do_check_true(result.measurements.has("foo")); + + run_next_test(); +}); + +add_test(function test_set_value() { + let result = new MetricsCollectionResult("set_value"); + result.expectMeasurement("DummyMeasurement"); + result.addMeasurement(new DummyMeasurement()); + + do_check_true(result.setValue("DummyMeasurement", "string", "hello world")); + + let failed = false; + try { + result.setValue("unknown", "irrelevant", "irrelevant"); + } catch (ex) { + do_check_true(ex.message.startsWith("Attempting to operate on an undefined measurement")); + failed = true; + } finally { + do_check_true(failed); + failed = false; + } + + do_check_eq(result.errors.length, 0); + do_check_false(result.setValue("DummyMeasurement", "string", 42)); + do_check_eq(result.errors.length, 1); + + try { + result.setValue("DummyMeasurement", "string", 42, true); + } catch (ex) { + failed = true; + } finally { + do_check_true(failed); + failed = false; + } + + run_next_test(); +}); + +add_test(function test_aggregate_bad_argument() { + let result = new MetricsCollectionResult("bad_argument"); + + let failed = false; + try { + result.aggregate(null); + } catch (ex) { + do_check_true(ex.message.startsWith("aggregate expects a MetricsCollection")); + failed = true; + } finally { + do_check_true(failed); + failed = false; + } + + try { + let result2 = new MetricsCollectionResult("bad_argument2"); + result.aggregate(result2); + } catch (ex) { + do_check_true(ex.message.startsWith("Can only aggregate")); + failed = true; + } finally { + do_check_true(failed); + failed = false; + } + + run_next_test(); +}); + +add_test(function test_aggregate_side_effects() { + let result1 = new MetricsCollectionResult("aggregate"); + let result2 = new MetricsCollectionResult("aggregate"); + + result1.expectMeasurement("dummy1"); + result1.expectMeasurement("foo"); + + result2.expectMeasurement("dummy2"); + result2.expectMeasurement("bar"); + + result1.addMeasurement(new DummyMeasurement("dummy1")); + result1.setValue("dummy1", "invalid", "invalid"); + + result2.addMeasurement(new DummyMeasurement("dummy2")); + result2.setValue("dummy2", "another", "invalid"); + + result1.aggregate(result2); + + do_check_eq(result1.expectedMeasurements.size(), 4); + do_check_true(result1.expectedMeasurements.has("bar")); + + do_check_eq(result1.measurements.size(), 2); + do_check_true(result1.measurements.has("dummy1")); + do_check_true(result1.measurements.has("dummy2")); + + do_check_eq(result1.missingMeasurements.size(), 2); + do_check_true(result1.missingMeasurements.has("bar")); + + do_check_eq(result1.errors.length, 2); + + run_next_test(); +}); + +add_test(function test_json() { + let result = new MetricsCollectionResult("json"); + result.expectMeasurement("dummy1"); + result.expectMeasurement("dummy2"); + result.expectMeasurement("missing1"); + result.expectMeasurement("missing2"); + + result.addMeasurement(new DummyMeasurement("dummy1")); + result.addMeasurement(new DummyMeasurement("dummy2")); + + result.setValue("dummy1", "string", "hello world"); + result.setValue("dummy2", "uint32", 42); + result.setValue("dummy1", "invalid", "irrelevant"); + + let json = JSON.parse(JSON.stringify(result)); + + do_check_eq(Object.keys(json).length, 3); + do_check_true("measurements" in json); + do_check_true("missing" in json); + do_check_true("errors" in json); + + do_check_eq(Object.keys(json.measurements).length, 2); + do_check_true("dummy1" in json.measurements); + do_check_true("dummy2" in json.measurements); + + do_check_eq(json.missing.length, 2); + let missing = new Set(json.missing); + do_check_true(missing.has("missing1")); + do_check_true(missing.has("missing2")); + + do_check_eq(json.errors.length, 1); + + run_next_test(); +}); + +add_test(function test_finish() { + let result = new MetricsCollectionResult("finish"); + + result.onFinished(function onFinished(result2) { + do_check_eq(result, result2); + + run_next_test(); + }); + + result.finish(); +}); diff --git a/services/metrics/tests/xpcshell/test_metrics_measurement.js b/services/metrics/tests/xpcshell/test_metrics_measurement.js new file mode 100644 index 000000000000..6daac3f036c2 --- /dev/null +++ b/services/metrics/tests/xpcshell/test_metrics_measurement.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const {utils: Cu} = Components; + +Cu.import("resource://testing-common/services/metrics/mocks.jsm"); + + +function run_test() { + run_next_test(); +}; + +add_test(function test_constructor() { + let m = new DummyMeasurement(); + do_check_eq(m.name, "DummyMeasurement"); + do_check_eq(m.version, 2); + + run_next_test(); +}); + +add_test(function test_add_string() { + let m = new DummyMeasurement(); + + m.setValue("string", "hello world"); + do_check_eq(m.getValue("string"), "hello world"); + + let failed = false; + try { + m.setValue("string", 46); + } catch (ex) { + do_check_true(ex.message.startsWith("STRING field expects a string")); + failed = true; + } finally { + do_check_true(failed); + } + + run_next_test(); +}); + +add_test(function test_add_uint32() { + let m = new DummyMeasurement(); + + m.setValue("uint32", 52342); + do_check_eq(m.getValue("uint32"), 52342); + + let failed = false; + try { + m.setValue("uint32", -1); + } catch (ex) { + failed = true; + do_check_true(ex.message.startsWith("UINT32 field expects a positive")); + } finally { + do_check_true(failed); + failed = false; + } + + try { + m.setValue("uint32", "foo"); + } catch (ex) { + failed = true; + do_check_true(ex.message.startsWith("UINT32 field expects an integer")); + } finally { + do_check_true(failed); + failed = false; + } + + try { + m.setValue("uint32", Math.pow(2, 32)); + } catch (ex) { + failed = true; + do_check_true(ex.message.startsWith("Value is too large")); + } finally { + do_check_true(failed); + failed = false; + } + + run_next_test(); +}); + +add_test(function test_validate() { + let m = new DummyMeasurement(); + + let failed = false; + try { + m.validate(); + } catch (ex) { + failed = true; + do_check_true(ex.message.startsWith("Required field not defined")); + } finally { + do_check_true(failed); + failed = false; + } + + run_next_test(); +}); + +add_test(function test_toJSON() { + let m = new DummyMeasurement(); + + m.setValue("string", "foo bar"); + + let json = JSON.parse(JSON.stringify(m)); + do_check_eq(Object.keys(json).length, 3); + do_check_eq(json.name, "DummyMeasurement"); + do_check_eq(json.version, 2); + do_check_true("fields" in json); + + do_check_eq(Object.keys(json.fields).length, 1); + do_check_eq(json.fields.string, "foo bar"); + + run_next_test(); +}); + diff --git a/services/metrics/tests/xpcshell/test_metrics_provider.js b/services/metrics/tests/xpcshell/test_metrics_provider.js new file mode 100644 index 000000000000..b7a2f5d1ace0 --- /dev/null +++ b/services/metrics/tests/xpcshell/test_metrics_provider.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const {utils: Cu} = Components; + +Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm"); +Cu.import("resource://testing-common/services/metrics/mocks.jsm"); + + +function run_test() { + run_next_test(); +}; + +add_test(function test_constructor() { + let provider = new MetricsProvider("foo"); + + let failed = false; + try { + new MetricsProvider(); + } catch(ex) { + do_check_true(ex.message.startsWith("MetricsProvider must have a name")); + failed = true; + } + finally { + do_check_true(failed); + } + + run_next_test(); +}); + +add_test(function test_default_collectors() { + let provider = new MetricsProvider("foo"); + + for (let property in MetricsProvider.prototype) { + if (!property.startsWith("collect")) { + continue; + } + + let result = provider[property](); + do_check_null(result); + } + + run_next_test(); +}); + +add_test(function test_collect_synchronous() { + let provider = new DummyProvider(); + + let result = provider.collectConstantMeasurements(); + do_check_true(result instanceof MetricsCollectionResult); + + result.onFinished(function onResult(res2) { + do_check_eq(result, res2); + + let m = result.measurements.get("DummyMeasurement"); + do_check_eq(m.getValue("uint32"), 24); + + run_next_test(); + }); +}); + diff --git a/services/metrics/tests/xpcshell/xpcshell.ini b/services/metrics/tests/xpcshell/xpcshell.ini index fd7581cfb799..1f96d51f441d 100644 --- a/services/metrics/tests/xpcshell/xpcshell.ini +++ b/services/metrics/tests/xpcshell/xpcshell.ini @@ -3,3 +3,6 @@ head = head.js tail = [test_load_modules.js] +[test_metrics_collection_result.js] +[test_metrics_measurement.js] +[test_metrics_provider.js]