Bug 812608 - Part 2: Refactor FHR on top of new Metrics APIs; r=rnewman

This also includes a lot of revamped Firefox Health Report features. The
payload format has changed. There is now robust service shutdown logic.
There are more prefs to control behavior. It's almost a rewritten
service.
This commit is contained in:
Gregory Szorc 2013-01-06 12:13:27 -08:00
parent 0489cc1183
commit 2f46a3aa01
9 changed files with 876 additions and 478 deletions

View File

@ -10,25 +10,46 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://services-common/preferences.js");
const INITIAL_STARTUP_DELAY_MSEC = 10 * 1000;
const BRANCH = "healthreport.";
const JS_PROVIDERS_CATEGORY = "healthreport-js-provider";
const DEFAULT_LOAD_DELAY_MSEC = 10 * 1000;
/**
* The Firefox Health Report XPCOM service.
*
* This instantiates an instance of HealthReporter (assuming it is enabled)
* and starts it upon application startup.
* External consumers will be interested in the "reporter" property of this
* service. This property is a `HealthReporter` instance that powers the
* service. The property may be null if the Health Report service is not
* enabled.
*
* One can obtain a reference to the underlying HealthReporter instance by
* accessing .reporter. If this property is null, the reporter isn't running
* yet or has been disabled.
* EXAMPLE USAGE
* =============
*
* let reporter = Cc["@mozilla.org/healthreport/service;1"]
* .getService(Ci.nsISupports)
* .wrappedJSObject
* .reporter;
*
* if (reporter.haveRemoteData) {
* // ...
* }
*
* IMPLEMENTATION NOTES
* ====================
*
* In order to not adversely impact application start time, the `HealthReporter`
* instance is not initialized until a few seconds after "final-ui-startup."
* The exact delay is configurable via preferences so it can be adjusted with
* a hotfix extension if the default value is ever problematic.
*
* Shutdown of the `HealthReporter` instance is handled completely within the
* instance (it registers observers on initialization). See the notes on that
* type for more.
*/
this.HealthReportService = function HealthReportService() {
this.wrappedJSObject = this;
this.prefs = new Preferences(BRANCH);
this._prefs = new Preferences(BRANCH);
this._reporter = null;
}
@ -40,7 +61,7 @@ HealthReportService.prototype = {
observe: function observe(subject, topic, data) {
// If the background service is disabled, don't do anything.
if (!this.prefs.get("serviceEnabled", true)) {
if (!this._prefs.get("service.enabled", true)) {
return;
}
@ -54,7 +75,9 @@ HealthReportService.prototype = {
case "final-ui-startup":
os.removeObserver(this, "final-ui-startup");
os.addObserver(this, "quit-application", true);
let delayInterval = this._prefs.get("service.loadDelayMsec") ||
DEFAULT_LOAD_DELAY_MSEC;
// Delay service loading a little more so things have an opportunity
// to cool down first.
@ -66,25 +89,21 @@ HealthReportService.prototype = {
let reporter = this.reporter;
delete this.timer;
}.bind(this),
}, INITIAL_STARTUP_DELAY_MSEC, this.timer.TYPE_ONE_SHOT);
}, delayInterval, this.timer.TYPE_ONE_SHOT);
break;
case "quit-application-granted":
if (this.reporter) {
this.reporter.stop();
}
os.removeObserver(this, "quit-application");
break;
}
},
/**
* The HealthReporter instance associated with this service.
*
* If the service is disabled, this will return null.
*
* The obtained instance may not be fully initialized.
*/
get reporter() {
if (!this.prefs.get("serviceEnabled", true)) {
if (!this._prefs.get("service.enabled", true)) {
return null;
}
@ -92,18 +111,19 @@ HealthReportService.prototype = {
return this._reporter;
}
// Lazy import so application startup isn't adversely affected.
let ns = {};
Cu.import("resource://services-common/log4moz.js", ns);
// Lazy import so application startup isn't adversely affected.
Cu.import("resource://gre/modules/Task.jsm", ns);
Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm", ns);
Cu.import("resource://services-common/log4moz.js", ns);
// How many times will we rewrite this code before rolling it up into a
// generic module? See also bug 451283.
const LOGGERS = [
"Metrics",
"Services.HealthReport",
"Services.Metrics",
"Services.BagheeraClient",
"Sqlite.Connection.healthreport",
];
let prefs = new Preferences(BRANCH + "logging.");
@ -118,9 +138,8 @@ HealthReportService.prototype = {
}
}
// The reporter initializes in the background.
this._reporter = new ns.HealthReporter(BRANCH);
this._reporter.registerProvidersFromCategoryManager(JS_PROVIDERS_CATEGORY);
this._reporter.start();
return this._reporter;
},

View File

@ -4,7 +4,6 @@
pref("healthreport.documentServerURI", "https://data.mozilla.com/");
pref("healthreport.documentServerNamespace", "metrics");
pref("healthreport.serviceEnabled", true);
pref("healthreport.logging.consoleEnabled", true);
pref("healthreport.logging.consoleLevel", "Warn");
pref("healthreport.policy.currentDaySubmissionFailureCount", 0);
@ -19,4 +18,7 @@ pref("healthreport.policy.lastDataSubmissionFailureTime", "0");
pref("healthreport.policy.lastDataSubmissionRequestedTime", "0");
pref("healthreport.policy.lastDataSubmissionSuccessfulTime", "0");
pref("healthreport.policy.nextDataSubmissionTime", "0");
pref("healthreport.service.enabled", true);
pref("healthreport.service.loadDelayMsec", 10000);
pref("healthreport.service.providerCategories", "healthreport-js-provider");

View File

@ -8,24 +8,33 @@ this.EXPORTED_SYMBOLS = ["HealthReporter"];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://services-common/async.js");
Cu.import("resource://services-common/bagheeraclient.js");
Cu.import("resource://services-common/log4moz.js");
Cu.import("resource://services-common/observers.js");
Cu.import("resource://services-common/preferences.js");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://gre/modules/commonjs/promise/core.js");
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/services/healthreport/policy.jsm");
Cu.import("resource://gre/modules/services/metrics/collector.jsm");
// Oldest year to allow in date preferences. This module was implemented in
// 2012 and no dates older than that should be encountered.
const OLDEST_ALLOWED_YEAR = 2012;
const DAYS_IN_PAYLOAD = 180;
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
const DEFAULT_DATABASE_NAME = "healthreport.sqlite";
/**
* Coordinates collection and submission of metrics.
* Coordinates collection and submission of health report metrics.
*
* This is the main type for Firefox Health Report. It glues all the
* lower-level components (such as collection and submission) together.
@ -39,23 +48,50 @@ const OLDEST_ALLOWED_YEAR = 2012;
* this type and *the* Firefox Health Report (e.g. the policy). This could
* be abstracted if needed.
*
* IMPLEMENTATION NOTES
* ====================
*
* Initialization and shutdown are somewhat complicated and worth explaining
* in extra detail.
*
* The complexity is driven by the requirements of SQLite connection management.
* Once you have a SQLite connection, it isn't enough to just let the
* application shut down. If there is an open connection or if there are
* outstanding SQL statements come XPCOM shutdown time, Storage will assert.
* On debug builds you will crash. On release builds you will get a shutdown
* hang. This must be avoided!
*
* During initialization, the second we create a SQLite connection (via
* Metrics.Storage) we register observers for application shutdown. The
* "quit-application" notification initiates our shutdown procedure. The
* subsequent "profile-do-change" notification ensures it has completed.
*
* The handler for "profile-do-change" may result in event loop spinning. This
* is because of race conditions between our shutdown code and application
* shutdown.
*
* All of our shutdown routines are async. There is the potential that these
* async functions will not complete before XPCOM shutdown. If they don't
* finish in time, we could get assertions in Storage. Our solution is to
* initiate storage early in the shutdown cycle ("quit-application").
* Hopefully all the async operations have completed by the time we reach
* "profile-do-change." If so, great. If not, we spin the event loop until
* they have completed, avoiding potential race conditions.
*
* @param branch
* (string) The preferences branch to use for state storage. The value
* must end with a period (.).
*/
this.HealthReporter = function HealthReporter(branch) {
function HealthReporter(branch) {
if (!branch.endsWith(".")) {
throw new Error("Branch argument must end with a period (.): " + branch);
throw new Error("Branch must end with a period (.): " + branch);
}
this._log = Log4Moz.repository.getLogger("Services.HealthReport.HealthReporter");
this._log.info("Initializing health reporter instance against " + branch);
this._prefs = new Preferences(branch);
let policyBranch = new Preferences(branch + "policy.");
this._policy = new HealthReportPolicy(policyBranch, this);
this._collector = new MetricsCollector();
if (!this.serverURI) {
throw new Error("No server URI defined. Did you forget to define the pref?");
}
@ -63,9 +99,41 @@ this.HealthReporter = function HealthReporter(branch) {
if (!this.serverNamespace) {
throw new Error("No server namespace defined. Did you forget a pref?");
}
this._dbName = this._prefs.get("dbName") || DEFAULT_DATABASE_NAME;
let policyBranch = new Preferences(branch + "policy.");
this._policy = new HealthReportPolicy(policyBranch, this);
this._storage = null;
this._storageInProgress = false;
this._collector = null;
this._initialized = false;
this._initializeHadError = false;
this._initializedDeferred = Promise.defer();
this._shutdownRequested = false;
this._shutdownInitiated = false;
this._shutdownComplete = false;
this._shutdownCompleteCallback = null;
this._ensureDirectoryExists(this._stateDir)
.then(this._onStateDirCreated.bind(this),
this._onInitError.bind(this));
}
HealthReporter.prototype = {
HealthReporter.prototype = Object.freeze({
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
/**
* Whether the service is fully initialized and running.
*
* If this is false, it is not safe to call most functions.
*/
get initialized() {
return this._initialized;
},
/**
* When we last successfully submitted data to the server.
*
@ -146,48 +214,242 @@ HealthReporter.prototype = {
*
* @return bool
*/
haveRemoteData: function haveRemoteData() {
haveRemoteData: function () {
return !!this.lastSubmitID;
},
/**
* Perform post-construction initialization and start background activity.
*
* If this isn't called, no data upload will occur.
*
* This returns a promise that will be fulfilled when all initialization
* activity is completed. It is not safe for this instance to perform
* additional actions until this promise has been resolved.
*/
start: function start() {
let onExists = function onExists() {
this._policy.startPolling();
this._log.info("HealthReporter started.");
//----------------------------------------------------
// SERVICE CONTROL FUNCTIONS
//
// You shouldn't need to call any of these externally.
//----------------------------------------------------
return Promise.resolve();
}.bind(this);
_onInitError: function (error) {
this._log.error("Error during initialization: " +
CommonUtils.exceptionStr(error));
this._initializeHadError = true;
this._initiateShutdown();
this._initializedDeferred.reject(error);
return this._ensureDirectoryExists(this._stateDir)
.then(onExists);
// FUTURE consider poisoning prototype's functions so calls fail with a
// useful error message.
},
/**
* Stop background functionality.
*/
stop: function stop() {
_onStateDirCreated: function () {
// As soon as we have could storage, we need to register cleanup or
// else bad things happen on shutdown.
Services.obs.addObserver(this, "quit-application", false);
Services.obs.addObserver(this, "profile-before-change", false);
this._storageInProgress = true;
Metrics.Storage(this._dbName).then(this._onStorageCreated.bind(this),
this._onInitError.bind(this));
},
// Called when storage has been opened.
_onStorageCreated: function (storage) {
this._log.info("Storage initialized.");
this._storage = storage;
this._storageInProgress = false;
if (this._shutdownRequested) {
this._initiateShutdown();
return;
}
Task.spawn(this._initializeCollector.bind(this))
.then(this._onCollectorInitialized.bind(this),
this._onInitError.bind(this));
},
_initializeCollector: function () {
if (this._collector) {
throw new Error("Collector has already been initialized.");
}
this._log.info("Initializing collector.");
this._collector = new Metrics.Collector(this._storage);
let catString = this._prefs.get("service.providerCategories") || "";
if (catString.length) {
for (let category of catString.split(",")) {
yield this.registerProvidersFromCategoryManager(category);
}
}
},
_onCollectorInitialized: function () {
if (this._shutdownRequested) {
this._initiateShutdown();
return;
}
this._policy.startPolling();
this._log.info("HealthReporter started.");
this._initialized = true;
Services.obs.addObserver(this, "idle-daily", false);
this._initializedDeferred.resolve(this);
},
// nsIObserver to handle shutdown.
observe: function (subject, topic, data) {
switch (topic) {
case "quit-application":
Services.obs.removeObserver(this, "quit-application");
this._initiateShutdown();
break;
case "profile-before-change":
Services.obs.removeObserver(this, "profile-before-change");
this._waitForShutdown();
break;
case "idle-daily":
this._performDailyMaintenance();
break;
}
},
_initiateShutdown: function () {
// Ensure we only begin the main shutdown sequence once.
if (this._shutdownInitiated) {
this._log.warn("Shutdown has already been initiated. No-op.");
return;
}
this._log.info("Request to shut down.");
this._initialized = false;
this._shutdownRequested = true;
// Safe to call multiple times.
this._policy.stopPolling();
// If storage is in the process of initializing, we need to wait for it
// to finish before continuing. The initialization process will call us
// again once storage has initialized.
if (this._storageInProgress) {
this._log.warn("Storage is in progress of initializing. Waiting to finish.");
return;
}
// Everything from here must only be performed once or else race conditions
// could occur.
this._shutdownInitiated = true;
Services.obs.removeObserver(this, "idle-daily");
// If we have collectors, we need to shut down providers.
if (this._collector) {
let onShutdown = this._onCollectorShutdown.bind(this);
Task.spawn(this._shutdownCollector.bind(this))
.then(onShutdown, onShutdown);
return;
}
this._onCollectorShutdown();
},
_shutdownCollector: function () {
for (let provider of this._collector.providers) {
try {
yield provider.shutdown();
} catch (ex) {
this._log.warn("Error when shutting down provider: " +
CommonUtils.exceptionStr(ex));
}
}
},
_onCollectorShutdown: function () {
this._collector = null;
if (this._storage) {
let onClose = this._onStorageClose.bind(this);
this._storage.close().then(onClose, onClose);
return;
}
this._onStorageClose();
},
_onStorageClose: function (error) {
if (error) {
this._log.warn("Error when closing storage: " +
CommonUtils.exceptionStr(error));
}
this._storage = null;
this._shutdownComplete = true;
if (this._shutdownCompleteCallback) {
this._shutdownCompleteCallback();
}
},
_waitForShutdown: function () {
if (this._shutdownComplete) {
return;
}
this._shutdownCompleteCallback = Async.makeSpinningCallback();
this._shutdownCompleteCallback.wait();
this._shutdownCompleteCallback = null;
},
/**
* Register a `MetricsProvider` with this instance.
* Convenience method to shut down the instance.
*
* This should *not* be called outside of tests.
*/
_shutdown: function () {
this._initiateShutdown();
this._waitForShutdown();
},
/**
* Return a promise that is resolved once the service has been initialized.
*/
onInit: function () {
if (this._initializeHadError) {
throw new Error("Service failed to initialize.");
}
if (this._initialized) {
return Promise.resolve(this);
}
return this._initializedDeferred.promise;
},
_performDailyMaintenance: function () {
this._log.info("Request to perform daily maintenance.");
if (!this._initialized) {
return;
}
let now = new Date();
let cutoff = new Date(now.getTime() - MILLISECONDS_PER_DAY * (DAYS_IN_PAYLOAD - 1));
// The operation is enqueued and put in a transaction by the storage module.
this._storage.pruneDataBefore(cutoff);
},
//--------------------
// Provider Management
//--------------------
/**
* Register a `Metrics.Provider` with this instance.
*
* This needs to be called or no data will be collected. See also
* registerProvidersFromCategoryManager`.
*
* @param provider
* (MetricsProvider) The provider to register for collection.
* (Metrics.Provider) The provider to register for collection.
*/
registerProvider: function registerProvider(provider) {
registerProvider: function (provider) {
return this._collector.registerProvider(provider);
},
@ -198,7 +460,7 @@ HealthReporter.prototype = {
* providers.
*
* Category entries are essentially JS modules and the name of the symbol
* within that module that is a `MetricsProvider` instance.
* within that module that is a `Metrics.Provider` instance.
*
* The category entry name is the name of the JS type for the provider. The
* value is the resource:// URI to import which makes this type available.
@ -213,18 +475,18 @@ HealthReporter.prototype = {
*
* Then to load them:
*
* let reporter = new HealthReporter("healthreport.");
* let reporter = getHealthReporter("healthreport.");
* reporter.registerProvidersFromCategoryManager("healthreport-js-provider");
*
* @param category
* (string) Name of category to query and load from.
*/
registerProvidersFromCategoryManager:
function registerProvidersFromCategoryManager(category) {
registerProvidersFromCategoryManager: function (category) {
this._log.info("Registering providers from category: " + category);
let cm = Cc["@mozilla.org/categorymanager;1"]
.getService(Ci.nsICategoryManager);
let promises = [];
let enumerator = cm.enumerateCategory(category);
while (enumerator.hasMoreElements()) {
let entry = enumerator.getNext()
@ -240,20 +502,26 @@ HealthReporter.prototype = {
Cu.import(uri, ns);
let provider = new ns[entry]();
this.registerProvider(provider);
promises.push(this.registerProvider(provider));
} catch (ex) {
this._log.warn("Error registering provider from category manager: " +
entry + "; " + CommonUtils.exceptionStr(ex));
continue;
}
}
return Task.spawn(function wait() {
for (let promise of promises) {
yield promise;
}
});
},
/**
* Collect all measurements for all registered providers.
*/
collectMeasurements: function collectMeasurements() {
return this._collector.collectConstantMeasurements();
collectMeasurements: function () {
return this._collector.collectConstantData();
},
/**
@ -264,7 +532,7 @@ HealthReporter.prototype = {
* @param reason
* (string) Why data submission is being disabled.
*/
recordPolicyRejection: function recordPolicyRejection(reason) {
recordPolicyRejection: function (reason) {
this._policy.recordUserRejection(reason);
},
@ -276,7 +544,7 @@ HealthReporter.prototype = {
* @param reason
* (string) Why data submission is being enabled.
*/
recordPolicyAcceptance: function recordPolicyAcceptance(reason) {
recordPolicyAcceptance: function (reason) {
this._policy.recordUserAcceptance(reason);
},
@ -306,7 +574,7 @@ HealthReporter.prototype = {
* callers should poll haveRemoteData() to determine when remote data is
* deleted.
*/
requestDeleteRemoteData: function requestDeleteRemoteData(reason) {
requestDeleteRemoteData: function (reason) {
if (!this.lastSubmitID) {
return;
}
@ -314,26 +582,110 @@ HealthReporter.prototype = {
return this._policy.deleteRemoteData(reason);
},
getJSONPayload: function getJSONPayload() {
getJSONPayload: function () {
return Task.spawn(this._getJSONPayload.bind(this, this._now()));
},
_getJSONPayload: function (now) {
let pingDateString = this._formatDate(now);
this._log.info("Producing JSON payload for " + pingDateString);
let o = {
version: 1,
thisPingDate: this._formatDate(this._now()),
providers: {},
thisPingDate: pingDateString,
data: {last: {}, days: {}},
};
let outputDataDays = o.data.days;
// We need to be careful that data in errors does not leak potentially
// private information.
// FUTURE ask Privacy if we can put exception stacks in here.
let errors = [];
let lastPingDate = this.lastPingDate;
if (lastPingDate.getTime() > 0) {
o.lastPingDate = this._formatDate(lastPingDate);
}
for (let [name, provider] of this._collector.collectionResults) {
o.providers[name] = provider;
for (let provider of this._collector.providers) {
let providerName = provider.name;
let providerEntry = {
measurements: {},
};
for (let [measurementKey, measurement] of provider.measurements) {
let name = providerName + "." + measurement.name + "." + measurement.version;
let serializer;
try {
serializer = measurement.serializer(measurement.SERIALIZE_JSON);
} catch (ex) {
this._log.warn("Error obtaining serializer for measurement: " + name +
": " + CommonUtils.exceptionStr(ex));
errors.push("Could not obtain serializer: " + name);
continue;
}
let data;
try {
data = yield this._storage.getMeasurementValues(measurement.id);
} catch (ex) {
this._log.warn("Error obtaining data for measurement: " +
name + ": " + CommonUtils.exceptionStr(ex));
errors.push("Could not obtain data: " + name);
continue;
}
if (data.singular.size) {
try {
o.data.last[name] = serializer.singular(data.singular);
} catch (ex) {
this._log.warn("Error serializing data: " + CommonUtils.exceptionStr(ex));
errors.push("Error serializing singular: " + name);
continue;
}
}
let dataDays = data.days;
for (let i = 0; i < DAYS_IN_PAYLOAD; i++) {
let date = new Date(now.getTime() - i * MILLISECONDS_PER_DAY);
if (!dataDays.hasDay(date)) {
continue;
}
let dateFormatted = this._formatDate(date);
try {
let serialized = serializer.daily(dataDays.getDay(date));
if (!serialized) {
continue;
}
if (!(dateFormatted in outputDataDays)) {
outputDataDays[dateFormatted] = {};
}
outputDataDays[dateFormatted][name] = serialized;
} catch (ex) {
this._log.warn("Error populating data for day: " +
CommonUtils.exceptionStr(ex));
errors.push("Could not serialize day: " + name +
" ( " + dateFormatted + ")");
continue;
}
}
}
}
return JSON.stringify(o);
if (errors.length) {
o.errors = errors;
}
throw new Task.Result(JSON.stringify(o));
},
_onBagheeraResult: function _onBagheeraResult(request, isDelete, result) {
_onBagheeraResult: function (request, isDelete, result) {
this._log.debug("Received Bagheera result.");
let promise = Promise.resolve(null);
@ -362,36 +714,34 @@ HealthReporter.prototype = {
return promise;
},
_onSubmitDataRequestFailure: function _onSubmitDataRequestFailure(error) {
_onSubmitDataRequestFailure: function (error) {
this._log.error("Error processing request to submit data: " +
CommonUtils.exceptionStr(error));
},
_formatDate: function _formatDate(date) {
_formatDate: function (date) {
// Why, oh, why doesn't JS have a strftime() equivalent?
return date.toISOString().substr(0, 10);
},
_uploadData: function _uploadData(request) {
_uploadData: function (request) {
let id = CommonUtils.generateUUID();
this._log.info("Uploading data to server: " + this.serverURI + " " +
this.serverNamespace + ":" + id);
let client = new BagheeraClient(this.serverURI);
let payload = this.getJSONPayload();
return this._saveLastPayload(payload)
.then(client.uploadJSON.bind(client,
this.serverNamespace,
id,
payload,
this.lastSubmitID))
.then(this._onBagheeraResult.bind(this, request, false));
return Task.spawn(function doUpload() {
let payload = yield this.getJSONPayload();
yield this._saveLastPayload(payload);
let result = yield client.uploadJSON(this.serverNamespace, id, payload,
this.lastSubmitID);
yield this._onBagheeraResult(request, false, result);
}.bind(this));
},
_deleteRemoteData: function _deleteRemoteData(request) {
_deleteRemoteData: function (request) {
if (!this.lastSubmitID) {
this._log.info("Received request to delete remote data but no data stored.");
request.onNoDataAvailable();
@ -419,7 +769,7 @@ HealthReporter.prototype = {
return OS.Path.join(profD, "healthreport");
},
_ensureDirectoryExists: function _ensureDirectoryExists(path) {
_ensureDirectoryExists: function (path) {
let deferred = Promise.defer();
OS.File.makeDir(path).then(
@ -443,7 +793,7 @@ HealthReporter.prototype = {
return OS.Path.join(this._stateDir, "lastpayload.json");
},
_saveLastPayload: function _saveLastPayload(payload) {
_saveLastPayload: function (payload) {
let path = this._lastPayloadPath;
let pathTmp = path + ".tmp";
@ -462,7 +812,7 @@ HealthReporter.prototype = {
*
* @return Promise<object>
*/
getLastPayload: function getLoadPayload() {
getLastPayload: function () {
let path = this._lastPayloadPath;
return OS.File.read(path).then(
@ -486,26 +836,24 @@ HealthReporter.prototype = {
// HealthReportPolicy listeners
//-----------------------------
onRequestDataUpload: function onRequestDataSubmission(request) {
onRequestDataUpload: function (request) {
this.collectMeasurements()
.then(this._uploadData.bind(this, request),
this._onSubmitDataRequestFailure.bind(this));
},
onNotifyDataPolicy: function onNotifyDataPolicy(request) {
onNotifyDataPolicy: function (request) {
// This isn't very loosely coupled. We may want to have this call
// registered listeners instead.
Observers.notify("healthreport:notify-data-policy:request", request);
},
onRequestRemoteDelete: function onRequestRemoteDelete(request) {
onRequestRemoteDelete: function (request) {
this._deleteRemoteData(request);
},
//------------------------------------
// End of HealthReportPolicy listeners
//------------------------------------
};
Object.freeze(HealthReporter.prototype);
});

View File

@ -11,24 +11,26 @@ this.EXPORTED_SYMBOLS = [
const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
const DEFAULT_PROFILE_MEASUREMENT_NAME = "org.mozilla.profile";
const DEFAULT_PROFILE_MEASUREMENT_NAME = "age";
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
const REQUIRED_UINT32_TYPE = {type: "TYPE_UINT32"};
Cu.import("resource://gre/modules/commonjs/promise/core.js");
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/osfile.jsm")
Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://services-common/log4moz.js");
Cu.import("resource://services-common/utils.js");
// Profile creation time access.
// This is separate from the provider to simplify testing and enable extraction
// to a shared location in the future.
function ProfileCreationTimeAccessor(profile) {
function ProfileCreationTimeAccessor(profile, log) {
this.profilePath = profile || OS.Constants.Path.profileDir;
if (!this.profilePath) {
throw new Error("No profile directory.");
}
this._log = log || {"debug": function (s) { dump(s + "\n"); }};
}
ProfileCreationTimeAccessor.prototype = {
/**
@ -115,31 +117,47 @@ ProfileCreationTimeAccessor.prototype = {
* and returning its creation timestamp.
*/
getOldestProfileTimestamp: function () {
let self = this;
let oldest = Date.now() + 1000;
let iterator = new OS.File.DirectoryIterator(this.profilePath);
dump("Iterating over profile " + this.profilePath);
self._log.debug("Iterating over profile " + this.profilePath);
if (!iterator) {
throw new Error("Unable to fetch oldest profile entry: no profile iterator.");
}
function onEntry(entry) {
if ("winLastWriteDate" in entry) {
// Under Windows, additional information allow us to sort files immediately
// without having to perform additional I/O.
let timestamp = entry.winCreationDate.getTime();
if (timestamp < oldest) {
oldest = timestamp;
}
return;
}
// Under other OSes, we need to call OS.File.stat.
function onStatSuccess(info) {
let date = info.creationDate;
let timestamp = date.getTime();
dump("CREATION DATE: " + entry.path + " = " + date);
if (timestamp < oldest) {
oldest = timestamp;
// OS.File doesn't seem to be behaving. See Bug 827148.
// Let's do the best we can. This whole function is defensive.
let date;
if ("winBirthDate" in info) {
date = info.winBirthDate;
} else if ("macBirthDate" in info) {
date = info.macBirthDate;
}
if (!date || !date.getTime()) {
// Hack: as of this writing, OS.File will only return file
// creation times of any kind of Mac and Windows, where birthTime
// is defined. That means we're unable to function on Linux.
// Use ctime, fall back to mtime.
// Oh, and info.macBirthDate doesn't work.
self._log.debug("No birth date: using ctime/mtime.");
try {
date = info.creationDate ||
info.lastModificationDate ||
info.unixLastStatusChangeDate;
} catch (ex) {
self._log.debug("Exception fetching creation date: " + ex);
}
}
if (date) {
let timestamp = date.getTime();
self._log.debug("Using date: " + entry.path + " = " + date);
if (timestamp < oldest) {
oldest = timestamp;
}
}
}
return OS.File.stat(entry.path)
@ -165,15 +183,18 @@ dump("Iterating over profile " + this.profilePath);
/**
* Measurements pertaining to the user's profile.
*/
function ProfileMetadataMeasurement(name=DEFAULT_PROFILE_MEASUREMENT_NAME) {
MetricsMeasurement.call(this, name, 1);
function ProfileMetadataMeasurement() {
Metrics.Measurement.call(this);
}
ProfileMetadataMeasurement.prototype = {
__proto__: MetricsMeasurement.prototype,
__proto__: Metrics.Measurement.prototype,
fields: {
name: DEFAULT_PROFILE_MEASUREMENT_NAME,
version: 1,
configureStorage: function () {
// Profile creation date. Number of days since Unix epoch.
"profileCreation": REQUIRED_UINT32_TYPE,
return this.registerStorageField("profileCreation", this.storage.FIELD_LAST_NUMERIC);
},
};
@ -188,41 +209,35 @@ function truncate(msec) {
}
/**
* A MetricsProvider for profile metadata, such as profile creation time.
* A Metrics.Provider for profile metadata, such as profile creation time.
*/
function ProfileMetadataProvider(name="ProfileMetadataProvider") {
MetricsProvider.call(this, name);
function ProfileMetadataProvider() {
Metrics.Provider.call(this);
}
ProfileMetadataProvider.prototype = {
__proto__: MetricsProvider.prototype,
__proto__: Metrics.Provider.prototype,
name: "org.mozilla.profile",
measurementTypes: [ProfileMetadataMeasurement],
getProfileCreationDays: function () {
let accessor = new ProfileCreationTimeAccessor();
let accessor = new ProfileCreationTimeAccessor(null, this._log);
return accessor.created
.then(truncate);
},
collectConstantMeasurements: function () {
let result = this.createResult();
result.expectMeasurement("org.mozilla.profile");
result.populate = this._populateConstants.bind(this);
return result;
},
collectConstantData: function () {
let m = this.getMeasurement(DEFAULT_PROFILE_MEASUREMENT_NAME, 1);
_populateConstants: function (result) {
let name = DEFAULT_PROFILE_MEASUREMENT_NAME;
result.addMeasurement(new ProfileMetadataMeasurement(name));
function onSuccess(days) {
result.setValue(name, "profileCreation", days);
result.finish();
}
function onFailure(ex) {
result.addError(ex);
result.finish();
}
return this.getProfileCreationDays()
.then(onSuccess, onFailure);
return Task.spawn(function collectConstant() {
let createdDays = yield this.getProfileCreationDays();
yield this.enqueueStorageOperation(function storeDays() {
return m.setLastNumeric("profileCreation", createdDays);
});
}.bind(this));
},
};

View File

@ -21,17 +21,14 @@ this.EXPORTED_SYMBOLS = [
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
Cu.import("resource://services-common/preferences.js");
Cu.import("resource://services-common/utils.js");
const REQUIRED_STRING_TYPE = {type: "TYPE_STRING"};
const OPTIONAL_STRING_TYPE = {type: "TYPE_STRING", optional: true};
const REQUIRED_UINT32_TYPE = {type: "TYPE_UINT32"};
XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
"resource://gre/modules/UpdateChannel.jsm");
@ -42,40 +39,54 @@ XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
* pieces thrown in.
*/
function AppInfoMeasurement() {
MetricsMeasurement.call(this, "appinfo", 1);
Metrics.Measurement.call(this);
}
AppInfoMeasurement.prototype = {
__proto__: MetricsMeasurement.prototype,
AppInfoMeasurement.prototype = Object.freeze({
__proto__: Metrics.Measurement.prototype,
fields: {
vendor: REQUIRED_STRING_TYPE,
name: REQUIRED_STRING_TYPE,
id: REQUIRED_STRING_TYPE,
version: REQUIRED_STRING_TYPE,
appBuildID: REQUIRED_STRING_TYPE,
platformVersion: REQUIRED_STRING_TYPE,
platformBuildID: REQUIRED_STRING_TYPE,
os: REQUIRED_STRING_TYPE,
xpcomabi: REQUIRED_STRING_TYPE,
updateChannel: REQUIRED_STRING_TYPE,
distributionID: REQUIRED_STRING_TYPE,
distributionVersion: REQUIRED_STRING_TYPE,
hotfixVersion: REQUIRED_STRING_TYPE,
locale: REQUIRED_STRING_TYPE,
name: "appinfo",
version: 1,
LAST_TEXT_FIELDS: [
"vendor",
"name",
"id",
"version",
"appBuildID",
"platformVersion",
"platformBuildID",
"os",
"xpcomabi",
"updateChannel",
"distributionID",
"distributionVersion",
"hotfixVersion",
"locale",
],
configureStorage: function () {
let self = this;
return Task.spawn(function configureStorage() {
for (let field of self.LAST_TEXT_FIELDS) {
yield self.registerStorageField(field, self.storage.FIELD_LAST_TEXT);
}
});
},
};
Object.freeze(AppInfoMeasurement.prototype);
});
this.AppInfoProvider = function AppInfoProvider() {
MetricsProvider.call(this, "app-info");
Metrics.Provider.call(this);
this._prefs = new Preferences({defaultBranch: null});
}
AppInfoProvider.prototype = {
__proto__: MetricsProvider.prototype,
AppInfoProvider.prototype = Object.freeze({
__proto__: Metrics.Provider.prototype,
name: "org.mozilla.appInfo",
measurementTypes: [AppInfoMeasurement],
appInfoFields: {
// From nsIXULAppInfo.
@ -92,16 +103,15 @@ AppInfoProvider.prototype = {
xpcomabi: "XPCOMABI",
},
collectConstantMeasurements: function collectConstantMeasurements() {
let result = this.createResult();
result.expectMeasurement("appinfo");
result.populate = this._populateConstants.bind(this);
return result;
collectConstantData: function () {
return this.enqueueStorageOperation(function collect() {
return Task.spawn(this._populateConstants.bind(this));
}.bind(this));
},
_populateConstants: function _populateConstants(result) {
result.addMeasurement(new AppInfoMeasurement());
_populateConstants: function () {
let m = this.getMeasurement(AppInfoMeasurement.prototype.name,
AppInfoMeasurement.prototype.version);
let ai;
try {
@ -119,71 +129,71 @@ AppInfoProvider.prototype = {
for (let [k, v] in Iterator(this.appInfoFields)) {
try {
result.setValue("appinfo", k, ai[v]);
yield m.setLastText(k, ai[v]);
} catch (ex) {
this._log.warn("Error obtaining Services.appinfo." + v);
result.addError(ex);
}
}
try {
result.setValue("appinfo", "updateChannel", UpdateChannel.get());
yield m.setLastText("updateChannel", UpdateChannel.get());
} catch (ex) {
this._log.warn("Could not obtain update channel: " +
CommonUtils.exceptionStr(ex));
result.addError(ex);
}
result.setValue("appinfo", "distributionID", this._prefs.get("distribution.id", ""));
result.setValue("appinfo", "distributionVersion", this._prefs.get("distribution.version", ""));
result.setValue("appinfo", "hotfixVersion", this._prefs.get("extensions.hotfix.lastVersion", ""));
yield m.setLastText("distributionID", this._prefs.get("distribution.id", ""));
yield m.setLastText("distributionVersion", this._prefs.get("distribution.version", ""));
yield m.setLastText("hotfixVersion", this._prefs.get("extensions.hotfix.lastVersion", ""));
try {
let locale = Cc["@mozilla.org/chrome/chrome-registry;1"]
.getService(Ci.nsIXULChromeRegistry)
.getSelectedLocale("global");
result.setValue("appinfo", "locale", locale);
yield m.setLastText("locale", locale);
} catch (ex) {
this._log.warn("Could not obtain application locale: " +
CommonUtils.exceptionStr(ex));
result.addError(ex);
}
result.finish();
},
};
Object.freeze(AppInfoProvider.prototype);
});
function SysInfoMeasurement() {
MetricsMeasurement.call(this, "sysinfo", 1);
Metrics.Measurement.call(this);
}
SysInfoMeasurement.prototype = {
__proto__: MetricsMeasurement.prototype,
SysInfoMeasurement.prototype = Object.freeze({
__proto__: Metrics.Measurement.prototype,
fields: {
cpuCount: REQUIRED_UINT32_TYPE,
memoryMB: REQUIRED_UINT32_TYPE,
manufacturer: OPTIONAL_STRING_TYPE,
device: OPTIONAL_STRING_TYPE,
hardware: OPTIONAL_STRING_TYPE,
name: OPTIONAL_STRING_TYPE,
version: OPTIONAL_STRING_TYPE,
architecture: OPTIONAL_STRING_TYPE,
name: "sysinfo",
version: 1,
configureStorage: function () {
return Task.spawn(function configureStorage() {
yield this.registerStorageField("cpuCount", this.storage.FIELD_LAST_NUMERIC);
yield this.registerStorageField("memoryMB", this.storage.FIELD_LAST_NUMERIC);
yield this.registerStorageField("manufacturer", this.storage.FIELD_LAST_TEXT);
yield this.registerStorageField("device", this.storage.FIELD_LAST_TEXT);
yield this.registerStorageField("hardware", this.storage.FIELD_LAST_TEXT);
yield this.registerStorageField("name", this.storage.FIELD_LAST_TEXT);
yield this.registerStorageField("version", this.storage.FIELD_LAST_TEXT);
yield this.registerStorageField("architecture", this.storage.FIELD_LAST_TEXT);
}.bind(this));
},
},
Object.freeze(SysInfoMeasurement.prototype);
});
this.SysInfoProvider = function SysInfoProvider() {
MetricsProvider.call(this, "sys-info");
Metrics.Provider.call(this);
};
SysInfoProvider.prototype = {
__proto__: MetricsProvider.prototype,
SysInfoProvider.prototype = Object.freeze({
__proto__: Metrics.Provider.prototype,
name: "org.mozilla.sysinfo",
measurementTypes: [SysInfoMeasurement],
sysInfoFields: {
cpucount: "cpuCount",
@ -196,19 +206,15 @@ SysInfoProvider.prototype = {
arch: "architecture",
},
INT_FIELDS: new Set("cpucount", "memsize"),
collectConstantMeasurements: function collectConstantMeasurements() {
let result = this.createResult();
result.expectMeasurement("sysinfo");
result.populate = this._populateConstants.bind(this);
return result;
collectConstantData: function () {
return this.enqueueStorageOperation(function collection() {
return Task.spawn(this._populateConstants.bind(this));
}.bind(this));
},
_populateConstants: function _populateConstants(result) {
result.addMeasurement(new SysInfoMeasurement());
_populateConstants: function () {
let m = this.getMeasurement(SysInfoMeasurement.prototype.name,
SysInfoMeasurement.prototype.version);
let si = Cc["@mozilla.org/system-info;1"]
.getService(Ci.nsIPropertyBag2);
@ -221,16 +227,16 @@ SysInfoProvider.prototype = {
}
let value = si.getProperty(k);
let method = "setLastText";
if (this.INT_FIELDS.has(k)) {
if (["cpucount", "memsize"].indexOf(k) != -1) {
let converted = parseInt(value, 10);
if (Number.isNaN(converted)) {
result.addError(new Error("Value is not an integer: " + k + "=" +
value));
continue;
}
value = converted;
method = "setLastNumeric";
}
// Round memory to mebibytes.
@ -238,17 +244,12 @@ SysInfoProvider.prototype = {
value = Math.round(value / 1048576);
}
result.setValue("sysinfo", v, value);
yield m[method](v, value);
} catch (ex) {
this._log.warn("Error obtaining system info field: " + k + " " +
CommonUtils.exceptionStr(ex));
result.addError(ex);
}
}
result.finish();
},
};
Object.freeze(SysInfoProvider.prototype);
});

View File

@ -10,6 +10,8 @@ Cu.import("resource://services-common/preferences.js");
Cu.import("resource://gre/modules/commonjs/promise/core.js");
Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm");
Cu.import("resource://gre/modules/services/healthreport/policy.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://testing-common/services-common/bagheeraserver.js");
Cu.import("resource://testing-common/services/metrics/mocks.jsm");
@ -17,6 +19,7 @@ Cu.import("resource://testing-common/services/metrics/mocks.jsm");
const SERVER_HOSTNAME = "localhost";
const SERVER_PORT = 8080;
const SERVER_URI = "http://" + SERVER_HOSTNAME + ":" + SERVER_PORT;
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
function defineNow(policy, now) {
@ -34,28 +37,39 @@ function getReporter(name, uri=SERVER_URI) {
let prefs = new Preferences(branch);
prefs.set("documentServerURI", uri);
prefs.set("dbName", name);
return new HealthReporter(branch);
let reporter = new HealthReporter(branch);
return reporter.onInit();
}
function getReporterAndServer(name, namespace="test") {
let reporter = getReporter(name, SERVER_URI);
reporter.serverNamespace = namespace;
return Task.spawn(function get() {
let reporter = yield getReporter(name, SERVER_URI);
reporter.serverNamespace = namespace;
let server = new BagheeraServer(SERVER_URI);
server.createNamespace(namespace);
let server = new BagheeraServer(SERVER_URI);
server.createNamespace(namespace);
server.start(SERVER_PORT);
server.start(SERVER_PORT);
return [reporter, server];
throw new Task.Result([reporter, server]);
});
}
function shutdownServer(server) {
let deferred = Promise.defer();
server.stop(deferred.resolve.bind(deferred));
return deferred.promise;
}
function run_test() {
run_next_test();
}
add_test(function test_constructor() {
let reporter = getReporter("constructor");
add_task(function test_constructor() {
let reporter = yield getReporter("constructor");
do_check_eq(reporter.lastPingDate.getTime(), 0);
do_check_null(reporter.lastSubmitID);
@ -70,16 +84,16 @@ add_test(function test_constructor() {
new HealthReporter("foo.bar");
} catch (ex) {
failed = true;
do_check_true(ex.message.startsWith("Branch argument must end"));
do_check_true(ex.message.startsWith("Branch must end"));
} finally {
do_check_true(failed);
failed = false;
}
run_next_test();
reporter._shutdown();
});
add_test(function test_register_providers_from_category_manager() {
add_task(function test_register_providers_from_category_manager() {
const category = "healthreporter-js-modules";
let cm = Cc["@mozilla.org/categorymanager;1"]
@ -88,113 +102,140 @@ add_test(function test_register_providers_from_category_manager() {
"resource://testing-common/services/metrics/mocks.jsm",
false, true);
let reporter = getReporter("category_manager");
do_check_eq(reporter._collector._providers.length, 0);
reporter.registerProvidersFromCategoryManager(category);
do_check_eq(reporter._collector._providers.length, 1);
let reporter = yield getReporter("category_manager");
do_check_eq(reporter._collector._providers.size, 0);
yield reporter.registerProvidersFromCategoryManager(category);
do_check_eq(reporter._collector._providers.size, 1);
run_next_test();
reporter._shutdown();
});
add_test(function test_start() {
let reporter = getReporter("start");
reporter.start().then(function onStarted() {
reporter.stop();
run_next_test();
});
});
add_test(function test_json_payload_simple() {
let reporter = getReporter("json_payload_simple");
add_task(function test_json_payload_simple() {
let reporter = yield getReporter("json_payload_simple");
let now = new Date();
let payload = reporter.getJSONPayload();
let payload = yield reporter.getJSONPayload();
let original = JSON.parse(payload);
do_check_eq(original.version, 1);
do_check_eq(original.thisPingDate, reporter._formatDate(now));
do_check_eq(Object.keys(original.providers).length, 0);
do_check_eq(Object.keys(original.data.last).length, 0);
do_check_eq(Object.keys(original.data.days).length, 0);
reporter.lastPingDate = new Date(now.getTime() - 24 * 60 * 60 * 1000 - 10);
original = JSON.parse(reporter.getJSONPayload());
original = JSON.parse(yield reporter.getJSONPayload());
do_check_eq(original.lastPingDate, reporter._formatDate(reporter.lastPingDate));
// This could fail if we cross UTC day boundaries at the exact instance the
// test is executed. Let's tempt fate.
do_check_eq(original.thisPingDate, reporter._formatDate(now));
run_next_test();
reporter._shutdown();
});
add_test(function test_json_payload_dummy_provider() {
let reporter = getReporter("json_payload_dummy_provider");
add_task(function test_json_payload_dummy_provider() {
let reporter = yield getReporter("json_payload_dummy_provider");
reporter.registerProvider(new DummyProvider());
reporter.collectMeasurements().then(function onResult() {
let o = JSON.parse(reporter.getJSONPayload());
yield reporter.registerProvider(new DummyProvider());
yield reporter.collectMeasurements();
let payload = yield reporter.getJSONPayload();
print(payload);
let o = JSON.parse(payload);
do_check_eq(Object.keys(o.providers).length, 1);
do_check_true("DummyProvider" in o.providers);
do_check_true("measurements" in o.providers.DummyProvider);
do_check_true("DummyMeasurement" in o.providers.DummyProvider.measurements);
do_check_eq(Object.keys(o.data.last).length, 1);
do_check_true("DummyProvider.DummyMeasurement.1" in o.data.last);
run_next_test();
});
reporter._shutdown();
});
add_test(function test_notify_policy_observers() {
let reporter = getReporter("notify_policy_observers");
add_task(function test_json_payload_multiple_days() {
let reporter = yield getReporter("json_payload_multiple_days");
let provider = new DummyProvider();
yield reporter.registerProvider(provider);
Observers.add("healthreport:notify-data-policy:request",
function onObserver(subject, data) {
Observers.remove("healthreport:notify-data-policy:request", onObserver);
let now = new Date();
let m = provider.getMeasurement("DummyMeasurement", 1);
for (let i = 0; i < 200; i++) {
let date = new Date(now.getTime() - i * MILLISECONDS_PER_DAY);
yield m.incrementDailyCounter("daily-counter", date);
yield m.addDailyDiscreteNumeric("daily-discrete-numeric", i, date);
yield m.addDailyDiscreteNumeric("daily-discrete-numeric", i + 100, date);
yield m.addDailyDiscreteText("daily-discrete-text", "" + i, date);
yield m.addDailyDiscreteText("daily-discrete-text", "" + (i + 50), date);
yield m.setDailyLastNumeric("daily-last-numeric", date.getTime(), date);
}
do_check_true("foo" in subject);
let payload = yield reporter.getJSONPayload();
print(payload);
let o = JSON.parse(payload);
run_next_test();
});
do_check_eq(Object.keys(o.data.days).length, 180);
let today = reporter._formatDate(now);
do_check_true(today in o.data.days);
reporter.onNotifyDataPolicy({foo: "bar"});
reporter._shutdown();
});
add_test(function test_data_submission_transport_failure() {
let reporter = getReporter("data_submission_transport_failure");
add_task(function test_idle_daily() {
let reporter = yield getReporter("idle_daily");
let provider = new DummyProvider();
yield reporter.registerProvider(provider);
let now = new Date();
let m = provider.getMeasurement("DummyMeasurement", 1);
for (let i = 0; i < 200; i++) {
let date = new Date(now.getTime() - i * MILLISECONDS_PER_DAY);
yield m.incrementDailyCounter("daily-counter", date);
}
let values = yield m.getValues();
do_check_eq(values.days.size, 200);
Services.obs.notifyObservers(null, "idle-daily", null);
let values = yield m.getValues();
do_check_eq(values.days.size, 180);
reporter._shutdown();
});
add_task(function test_data_submission_transport_failure() {
let reporter = yield getReporter("data_submission_transport_failure");
reporter.serverURI = "http://localhost:8080/";
reporter.serverNamespace = "test00";
let deferred = Promise.defer();
deferred.promise.then(function onResult(request) {
do_check_eq(request.state, request.SUBMISSION_FAILURE_SOFT);
run_next_test();
});
let request = new DataSubmissionRequest(deferred, new Date(Date.now + 30000));
reporter.onRequestDataUpload(request);
yield deferred.promise;
do_check_eq(request.state, request.SUBMISSION_FAILURE_SOFT);
reporter._shutdown();
});
add_test(function test_data_submission_success() {
let [reporter, server] = getReporterAndServer("data_submission_success");
add_task(function test_data_submission_success() {
let [reporter, server] = yield getReporterAndServer("data_submission_success");
do_check_eq(reporter.lastPingDate.getTime(), 0);
do_check_false(reporter.haveRemoteData());
let deferred = Promise.defer();
deferred.promise.then(function onResult(request) {
do_check_eq(request.state, request.SUBMISSION_SUCCESS);
do_check_neq(reporter.lastPingDate.getTime(), 0);
do_check_true(reporter.haveRemoteData());
server.stop(run_next_test);
});
let request = new DataSubmissionRequest(deferred, new Date());
reporter.onRequestDataUpload(request);
yield deferred.promise;
do_check_eq(request.state, request.SUBMISSION_SUCCESS);
do_check_true(reporter.lastPingDate.getTime() > 0);
do_check_true(reporter.haveRemoteData());
reporter._shutdown();
yield shutdownServer(server);
});
add_test(function test_recurring_daily_pings() {
let [reporter, server] = getReporterAndServer("recurring_daily_pings");
add_task(function test_recurring_daily_pings() {
let [reporter, server] = yield getReporterAndServer("recurring_daily_pings");
reporter.registerProvider(new DummyProvider());
let policy = reporter._policy;
@ -204,55 +245,52 @@ add_test(function test_recurring_daily_pings() {
defineNow(policy, policy.nextDataSubmissionDate);
let promise = policy.checkStateAndTrigger();
do_check_neq(promise, null);
yield promise;
promise.then(function onUploadComplete() {
let lastID = reporter.lastSubmitID;
let lastID = reporter.lastSubmitID;
do_check_neq(lastID, null);
do_check_true(server.hasDocument(reporter.serverNamespace, lastID));
do_check_neq(lastID, null);
do_check_true(server.hasDocument(reporter.serverNamespace, lastID));
// Skip forward to next scheduled submission time.
defineNow(policy, policy.nextDataSubmissionDate);
promise = policy.checkStateAndTrigger();
do_check_neq(promise, null);
yield promise;
do_check_neq(reporter.lastSubmitID, lastID);
do_check_true(server.hasDocument(reporter.serverNamespace, reporter.lastSubmitID));
do_check_false(server.hasDocument(reporter.serverNamespace, lastID));
// Skip forward to next scheduled submission time.
defineNow(policy, policy.nextDataSubmissionDate);
let promise = policy.checkStateAndTrigger();
do_check_neq(promise, null);
promise.then(function onSecondUploadCOmplete() {
do_check_neq(reporter.lastSubmitID, lastID);
do_check_true(server.hasDocument(reporter.serverNamespace, reporter.lastSubmitID));
do_check_false(server.hasDocument(reporter.serverNamespace, lastID));
server.stop(run_next_test);
});
});
reporter._shutdown();
yield shutdownServer(server);
});
add_test(function test_request_remote_data_deletion() {
let [reporter, server] = getReporterAndServer("request_remote_data_deletion");
add_task(function test_request_remote_data_deletion() {
let [reporter, server] = yield getReporterAndServer("request_remote_data_deletion");
let policy = reporter._policy;
defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
policy.recordUserAcceptance();
defineNow(policy, policy.nextDataSubmissionDate);
policy.checkStateAndTrigger().then(function onUploadComplete() {
let id = reporter.lastSubmitID;
do_check_neq(id, null);
do_check_true(server.hasDocument(reporter.serverNamespace, id));
yield policy.checkStateAndTrigger();
let id = reporter.lastSubmitID;
do_check_neq(id, null);
do_check_true(server.hasDocument(reporter.serverNamespace, id));
defineNow(policy, policy._futureDate(10 * 1000));
defineNow(policy, policy._futureDate(10 * 1000));
let promise = reporter.requestDeleteRemoteData();
do_check_neq(promise, null);
promise.then(function onDeleteComplete() {
do_check_null(reporter.lastSubmitID);
do_check_false(reporter.haveRemoteData());
do_check_false(server.hasDocument(reporter.serverNamespace, id));
let promise = reporter.requestDeleteRemoteData();
do_check_neq(promise, null);
yield promise;
do_check_null(reporter.lastSubmitID);
do_check_false(reporter.haveRemoteData());
do_check_false(server.hasDocument(reporter.serverNamespace, id));
server.stop(run_next_test);
});
});
reporter._shutdown();
yield shutdownServer(server);
});
add_test(function test_policy_accept_reject() {
let [reporter, server] = getReporterAndServer("policy_accept_reject");
add_task(function test_policy_accept_reject() {
let [reporter, server] = yield getReporterAndServer("policy_accept_reject");
do_check_false(reporter.dataSubmissionPolicyAccepted);
do_check_false(reporter.willUploadData);
@ -265,21 +303,22 @@ add_test(function test_policy_accept_reject() {
do_check_false(reporter.dataSubmissionPolicyAccepted);
do_check_false(reporter.willUploadData);
server.stop(run_next_test);
reporter._shutdown();
yield shutdownServer(server);
});
add_test(function test_upload_save_payload() {
let [reporter, server] = getReporterAndServer("upload_save_payload");
add_task(function test_upload_save_payload() {
let [reporter, server] = yield getReporterAndServer("upload_save_payload");
let deferred = Promise.defer();
let request = new DataSubmissionRequest(deferred, new Date(), false);
reporter._uploadData(request).then(function onUpload() {
reporter.getLastPayload().then(function onJSON(json) {
do_check_true("thisPingDate" in json);
server.stop(run_next_test);
});
});
yield reporter._uploadData(request);
let json = yield reporter.getLastPayload();
do_check_true("thisPingDate" in json);
reporter._shutdown();
yield shutdownServer(server);
});

View File

@ -13,12 +13,14 @@ let profile_creation_lower = Date.now() - MILLISECONDS_PER_DAY;
do_get_profile();
Cu.import("resource://gre/modules/commonjs/promise/core.js");
Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/services/healthreport/profile.jsm");
Cu.import("resource://gre/modules/Task.jsm");
function MockProfileMetadataProvider(name="MockProfileMetadataProvider") {
ProfileMetadataProvider.call(this, name);
this.name = name;
ProfileMetadataProvider.call(this);
}
MockProfileMetadataProvider.prototype = {
__proto__: ProfileMetadataProvider.prototype,
@ -33,15 +35,6 @@ function run_test() {
run_next_test();
}
/**
* Treat the provided function as a generator of promises,
* suitable for use with Task.spawn. Success runs next test;
* failure throws.
*/
function testTask(promiseFunction) {
Task.spawn(promiseFunction).then(run_next_test, do_throw);
}
/**
* Ensure that OS.File works in our environment.
* This test can go once there are xpcshell tests for OS.File.
@ -84,22 +77,17 @@ add_test(function test_time_accessor_no_file() {
});
});
add_test(function test_time_accessor_named_file() {
add_task(function test_time_accessor_named_file() {
let acc = getAccessor();
testTask(function () {
// There should be no file yet.
yield acc.writeTimes({created: 12345}, "test.json");
yield acc.readTimes("test.json")
.then(function onSuccess(json) {
print("Read: " + JSON.stringify(json));
do_check_eq(12345, json.created);
run_next_test();
});
});
// There should be no file yet.
yield acc.writeTimes({created: 12345}, "test.json");
let json = yield acc.readTimes("test.json")
print("Read: " + JSON.stringify(json));
do_check_eq(12345, json.created);
});
add_test(function test_time_accessor_creates_file() {
add_task(function test_time_accessor_creates_file() {
let lower = profile_creation_lower;
// Ensure that provided contents are merged, and existing
@ -109,42 +97,32 @@ add_test(function test_time_accessor_creates_file() {
let existing = {abc: "123", easy: "abc"};
let expected;
testTask(function () {
yield acc.computeAndPersistTimes(existing, "test2.json")
.then(function onSuccess(created) {
let upper = Date.now() + 1000;
print(lower + " < " + created + " <= " + upper);
do_check_true(lower < created);
do_check_true(upper >= created);
expected = created;
});
yield acc.readTimes("test2.json")
.then(function onSuccess(json) {
print("Read: " + JSON.stringify(json));
do_check_eq("123", json.abc);
do_check_eq("abc", json.easy);
do_check_eq(expected, json.created);
});
});
let created = yield acc.computeAndPersistTimes(existing, "test2.json")
let upper = Date.now() + 1000;
print(lower + " < " + created + " <= " + upper);
do_check_true(lower < created);
do_check_true(upper >= created);
expected = created;
let json = yield acc.readTimes("test2.json")
print("Read: " + JSON.stringify(json));
do_check_eq("123", json.abc);
do_check_eq("abc", json.easy);
do_check_eq(expected, json.created);
});
add_test(function test_time_accessor_all() {
add_task(function test_time_accessor_all() {
let lower = profile_creation_lower;
let acc = getAccessor();
let expected;
testTask(function () {
yield acc.created
.then(function onSuccess(created) {
let upper = Date.now() + 1000;
do_check_true(lower < created);
do_check_true(upper >= created);
expected = created;
});
yield acc.created
.then(function onSuccess(again) {
do_check_eq(expected, again);
});
});
let created = yield acc.created
let upper = Date.now() + 1000;
do_check_true(lower < created);
do_check_true(upper >= created);
expected = created;
let again = yield acc.created
do_check_eq(expected, again);
});
add_test(function test_constructor() {
@ -171,43 +149,48 @@ add_test(function test_profile_files() {
// A generic test helper. We use this with both real
// and mock providers in these tests.
function test_collect_constant(provider, valueTest) {
let result = provider.collectConstantMeasurements();
do_check_true(result instanceof MetricsCollectionResult);
function test_collect_constant(provider) {
return Task.spawn(function () {
yield provider.collectConstantData();
result.onFinished(function onFinished() {
do_check_eq(result.expectedMeasurements.size, 1);
do_check_true(result.expectedMeasurements.has("org.mozilla.profile"));
let m = result.measurements.get("org.mozilla.profile");
do_check_true(!!m);
valueTest(m.getValue("profileCreation"));
let m = provider.getMeasurement("age", 1);
do_check_neq(m, null);
let values = yield m.getValues();
do_check_eq(values.singular.size, 1);
do_check_true(values.singular.has("profileCreation"));
run_next_test();
throw new Task.Result(values.singular.get("profileCreation")[1]);
});
result.populate(result);
}
add_test(function test_collect_constant_mock() {
add_task(function test_collect_constant_mock() {
let storage = yield Metrics.Storage("collect_constant_mock");
let provider = new MockProfileMetadataProvider();
function valueTest(v) {
do_check_eq(v, 1234);
}
test_collect_constant(provider, valueTest);
yield provider.init(storage);
let v = yield test_collect_constant(provider);
do_check_eq(v, 1234);
yield storage.close();
});
add_test(function test_collect_constant_real() {
add_task(function test_collect_constant_real() {
let provider = new ProfileMetadataProvider();
function valueTest(v) {
let ms = v * MILLISECONDS_PER_DAY;
let lower = profile_creation_lower;
let upper = Date.now() + 1000;
print("Day: " + v);
print("msec: " + ms);
print("Lower: " + lower);
print("Upper: " + upper);
do_check_true(lower <= ms);
do_check_true(upper >= ms);
}
test_collect_constant(provider, valueTest);
let storage = yield Metrics.Storage("collect_constant_real");
yield provider.init(storage);
let v = yield test_collect_constant(provider);
let ms = v * MILLISECONDS_PER_DAY;
let lower = profile_creation_lower;
let upper = Date.now() + 1000;
print("Day: " + v);
print("msec: " + ms);
print("Lower: " + lower);
print("Upper: " + upper);
do_check_true(lower <= ms);
do_check_true(upper >= ms);
yield storage.close();
});

View File

@ -5,9 +5,9 @@
const {interfaces: Ci, results: Cr, utils: Cu} = Components;
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
function run_test() {
run_next_test();
@ -19,32 +19,28 @@ add_test(function test_constructor() {
run_next_test();
});
add_test(function test_collect_smoketest() {
add_task(function test_collect_smoketest() {
let storage = yield Metrics.Storage("collect_smoketest");
let provider = new AppInfoProvider();
yield provider.init(storage);
let result = provider.collectConstantMeasurements();
do_check_true(result instanceof MetricsCollectionResult);
yield provider.collectConstantData();
result.onFinished(function onFinished() {
do_check_eq(result.expectedMeasurements.size, 1);
do_check_true(result.expectedMeasurements.has("appinfo"));
do_check_eq(result.measurements.size, 1);
do_check_true(result.measurements.has("appinfo"));
do_check_eq(result.errors.length, 0);
let m = provider.getMeasurement("appinfo", 1);
let data = yield storage.getMeasurementValues(m.id);
let serializer = m.serializer(m.SERIALIZE_JSON);
let d = serializer.singular(data.singular);
let ai = result.measurements.get("appinfo");
do_check_eq(ai.getValue("vendor"), "Mozilla");
do_check_eq(ai.getValue("name"), "xpcshell");
do_check_eq(ai.getValue("id"), "xpcshell@tests.mozilla.org");
do_check_eq(ai.getValue("version"), "1");
do_check_eq(ai.getValue("appBuildID"), "20121107");
do_check_eq(ai.getValue("platformVersion"), "p-ver");
do_check_eq(ai.getValue("platformBuildID"), "20121106");
do_check_eq(ai.getValue("os"), "XPCShell");
do_check_eq(ai.getValue("xpcomabi"), "noarch-spidermonkey");
do_check_eq(d.vendor, "Mozilla");
do_check_eq(d.name, "xpcshell");
do_check_eq(d.id, "xpcshell@tests.mozilla.org");
do_check_eq(d.version, "1");
do_check_eq(d.appBuildID, "20121107");
do_check_eq(d.platformVersion, "p-ver");
do_check_eq(d.platformBuildID, "20121106");
do_check_eq(d.os, "XPCShell");
do_check_eq(d.xpcomabi, "noarch-spidermonkey");
run_next_test();
});
result.populate(result);
yield storage.close();
});

View File

@ -5,9 +5,9 @@
const {interfaces: Ci, results: Cr, utils: Cu} = Components;
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
function run_test() {
@ -20,26 +20,21 @@ add_test(function test_constructor() {
run_next_test();
});
add_test(function test_collect_smoketest() {
add_task(function test_collect_smoketest() {
let storage = yield Metrics.Storage("collect_smoketest");
let provider = new SysInfoProvider();
yield provider.init(storage);
let result = provider.collectConstantMeasurements();
do_check_true(result instanceof MetricsCollectionResult);
yield provider.collectConstantData();
result.onFinished(function onFinished() {
do_check_eq(result.expectedMeasurements.size, 1);
do_check_true(result.expectedMeasurements.has("sysinfo"));
do_check_eq(result.measurements.size, 1);
do_check_true(result.measurements.has("sysinfo"));
do_check_eq(result.errors.length, 0);
let m = provider.getMeasurement("sysinfo", 1);
let data = yield storage.getMeasurementValues(m.id);
let serializer = m.serializer(m.SERIALIZE_JSON);
let d = serializer.singular(data.singular);
let si = result.measurements.get("sysinfo");
do_check_true(si.getValue("cpuCount") > 0);
do_check_neq(si.getValue("name"), null);
do_check_true(d.cpuCount > 0);
do_check_neq(d.name, null);
run_next_test();
});
result.populate(result);
yield storage.close();
});