mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-02 20:20:00 +00:00
Bug 807842 - FHR provider: profile metadata. r=gps
This commit is contained in:
parent
f7a60afb5d
commit
f001aa4e10
@ -3,6 +3,7 @@
|
||||
|
||||
Cu.import("resource://gre/modules/FileUtils.jsm");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://gre/modules/osfile.jsm")
|
||||
|
||||
function run_test() {
|
||||
initTestLogging();
|
||||
@ -108,3 +109,33 @@ add_test(function test_load_logging() {
|
||||
run_next_test();
|
||||
}));
|
||||
});
|
||||
|
||||
add_test(function test_writeJSON_readJSON() {
|
||||
_("Round-trip some JSON through the promise-based JSON writer.");
|
||||
|
||||
let contents = {
|
||||
"a": 12345.67,
|
||||
"b": {
|
||||
"c": "héllö",
|
||||
},
|
||||
"d": undefined,
|
||||
"e": null,
|
||||
};
|
||||
|
||||
function checkJSON(json) {
|
||||
do_check_eq(contents.a, json.a);
|
||||
do_check_eq(contents.b.c, json.b.c);
|
||||
do_check_eq(contents.d, json.d);
|
||||
do_check_eq(contents.e, json.e);
|
||||
run_next_test();
|
||||
};
|
||||
|
||||
function doRead() {
|
||||
CommonUtils.readJSON(path)
|
||||
.then(checkJSON, do_throw);
|
||||
}
|
||||
|
||||
let path = OS.Path.join(OS.Constants.Path.profileDir, "bar.json");
|
||||
CommonUtils.writeJSON(contents, path)
|
||||
.then(doRead, do_throw);
|
||||
});
|
||||
|
@ -10,6 +10,7 @@ Cu.import("resource://gre/modules/FileUtils.jsm");
|
||||
Cu.import("resource://gre/modules/NetUtil.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/osfile.jsm")
|
||||
Cu.import("resource://services-common/log4moz.js");
|
||||
|
||||
this.CommonUtils = {
|
||||
@ -329,6 +330,33 @@ this.CommonUtils = {
|
||||
return over ? atob(b64.substr(0, len - over)) : atob(b64);
|
||||
},
|
||||
|
||||
/**
|
||||
* Parses a JSON file from disk using OS.File and promises.
|
||||
*
|
||||
* @param path the file to read. Will be passed to `OS.File.read()`.
|
||||
* @return a promise that resolves to the JSON contents of the named file.
|
||||
*/
|
||||
readJSON: function(path) {
|
||||
let decoder = new TextDecoder();
|
||||
let promise = OS.File.read(path);
|
||||
return promise.then(function onSuccess(array) {
|
||||
return JSON.parse(decoder.decode(array));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Write a JSON object to the named file using OS.File and promises.
|
||||
*
|
||||
* @param contents a JS object. Will be serialized.
|
||||
* @param path the path of the file to write.
|
||||
* @return a promise, as produced by OS.File.writeAtomic.
|
||||
*/
|
||||
writeJSON: function(contents, path) {
|
||||
let encoder = new TextEncoder();
|
||||
let array = encoder.encode(JSON.stringify(contents));
|
||||
return OS.File.writeAtomic(path, array, {tmpPath: path + ".tmp"});
|
||||
},
|
||||
|
||||
/**
|
||||
* Load a JSON file from disk in the profile directory.
|
||||
*
|
||||
|
@ -11,4 +11,5 @@ category app-startup HealthReportService service,@mozilla.org/healthreport/servi
|
||||
|
||||
category healthreport-js-provider AppInfoProvider resource://gre/modules/services/healthreport/providers.jsm
|
||||
category healthreport-js-provider SysInfoProvider resource://gre/modules/services/healthreport/providers.jsm
|
||||
category healthreport-js-provider ProfileMetadataProvider resource://gre/modules/services/healthreport/profile.jsm
|
||||
|
||||
|
@ -12,6 +12,7 @@ include $(DEPTH)/config/autoconf.mk
|
||||
modules := \
|
||||
healthreporter.jsm \
|
||||
policy.jsm \
|
||||
profile.jsm \
|
||||
providers.jsm \
|
||||
$(NULL)
|
||||
|
||||
|
228
services/healthreport/profile.jsm
Normal file
228
services/healthreport/profile.jsm
Normal file
@ -0,0 +1,228 @@
|
||||
/* 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 = [
|
||||
"ProfileCreationTimeAccessor",
|
||||
"ProfileMetadataProvider",
|
||||
];
|
||||
|
||||
const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
|
||||
|
||||
const DEFAULT_PROFILE_MEASUREMENT_NAME = "org.mozilla.profile";
|
||||
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/osfile.jsm")
|
||||
Cu.import("resource://gre/modules/services/metrics/dataprovider.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) {
|
||||
this.profilePath = profile || OS.Constants.Path.profileDir;
|
||||
if (!this.profilePath) {
|
||||
throw new Error("No profile directory.");
|
||||
}
|
||||
}
|
||||
ProfileCreationTimeAccessor.prototype = {
|
||||
/**
|
||||
* There are three ways we can get our creation time:
|
||||
*
|
||||
* 1. From our own saved value (to avoid redundant work).
|
||||
* 2. From the on-disk JSON file.
|
||||
* 3. By calculating it from the filesystem.
|
||||
*
|
||||
* If we have to calculate, we write out the file; if we have
|
||||
* to touch the file, we persist in-memory.
|
||||
*
|
||||
* @return a promise that resolves to the profile's creation time.
|
||||
*/
|
||||
get created() {
|
||||
if (this._created) {
|
||||
return Promise.resolve(this._created);
|
||||
}
|
||||
|
||||
function onSuccess(times) {
|
||||
if (times && times.created) {
|
||||
return this._created = times.created;
|
||||
}
|
||||
return onFailure.call(this, null, times);
|
||||
}
|
||||
|
||||
function onFailure(err, times) {
|
||||
return this.computeAndPersistTimes(times)
|
||||
.then(function onSuccess(created) {
|
||||
return this._created = created;
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
return this.readTimes()
|
||||
.then(onSuccess.bind(this),
|
||||
onFailure.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Explicitly make `file`, a filename, a full path
|
||||
* relative to our profile path.
|
||||
*/
|
||||
getPath: function (file) {
|
||||
return OS.Path.join(this.profilePath, file);
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a promise which resolves to the JSON contents
|
||||
* of the time file in this accessor's profile.
|
||||
*/
|
||||
readTimes: function (file="times.json") {
|
||||
return CommonUtils.readJSON(this.getPath(file));
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a promise representing the writing of `contents`
|
||||
* to `file` in the specified profile.
|
||||
*/
|
||||
writeTimes: function (contents, file="times.json") {
|
||||
return CommonUtils.writeJSON(contents, this.getPath(file));
|
||||
},
|
||||
|
||||
/**
|
||||
* Merge existing contents with a 'created' field, writing them
|
||||
* to the specified file. Promise, naturally.
|
||||
*/
|
||||
computeAndPersistTimes: function (existingContents, file="times.json") {
|
||||
let path = this.getPath(file);
|
||||
function onOldest(oldest) {
|
||||
let contents = existingContents || {};
|
||||
contents.created = oldest;
|
||||
return this.writeTimes(contents, path)
|
||||
.then(function onSuccess() {
|
||||
return oldest;
|
||||
});
|
||||
}
|
||||
|
||||
return this.getOldestProfileTimestamp()
|
||||
.then(onOldest.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Traverse the contents of the profile directory, finding the oldest file
|
||||
* and returning its creation timestamp.
|
||||
*/
|
||||
getOldestProfileTimestamp: function () {
|
||||
let oldest = Date.now() + 1000;
|
||||
let iterator = new OS.File.DirectoryIterator(this.profilePath);
|
||||
dump("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;
|
||||
}
|
||||
}
|
||||
return OS.File.stat(entry.path)
|
||||
.then(onStatSuccess);
|
||||
}
|
||||
|
||||
let promise = iterator.forEach(onEntry);
|
||||
|
||||
function onSuccess() {
|
||||
iterator.close();
|
||||
return oldest;
|
||||
}
|
||||
|
||||
function onFailure(reason) {
|
||||
iterator.close();
|
||||
throw new Error("Unable to fetch oldest profile entry: " + reason);
|
||||
}
|
||||
|
||||
return promise.then(onSuccess, onFailure);
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Measurements pertaining to the user's profile.
|
||||
*/
|
||||
function ProfileMetadataMeasurement(name=DEFAULT_PROFILE_MEASUREMENT_NAME) {
|
||||
MetricsMeasurement.call(this, name, 1);
|
||||
}
|
||||
ProfileMetadataMeasurement.prototype = {
|
||||
__proto__: MetricsMeasurement.prototype,
|
||||
|
||||
fields: {
|
||||
// Profile creation date. Number of days since Unix epoch.
|
||||
"profileCreation": REQUIRED_UINT32_TYPE,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Turn a millisecond timestamp into a day timestamp.
|
||||
*
|
||||
* @param msec a number of milliseconds since epoch.
|
||||
* @return the number of whole days denoted by the input.
|
||||
*/
|
||||
function truncate(msec) {
|
||||
return Math.floor(msec / MILLISECONDS_PER_DAY);
|
||||
}
|
||||
|
||||
/**
|
||||
* A MetricsProvider for profile metadata, such as profile creation time.
|
||||
*/
|
||||
function ProfileMetadataProvider(name="ProfileMetadataProvider") {
|
||||
MetricsProvider.call(this, name);
|
||||
}
|
||||
ProfileMetadataProvider.prototype = {
|
||||
__proto__: MetricsProvider.prototype,
|
||||
|
||||
getProfileCreationDays: function () {
|
||||
let accessor = new ProfileCreationTimeAccessor();
|
||||
|
||||
return accessor.created
|
||||
.then(truncate);
|
||||
},
|
||||
|
||||
collectConstantMeasurements: function () {
|
||||
let result = this.createResult();
|
||||
result.expectMeasurement("org.mozilla.profile");
|
||||
result.populate = this._populateConstants.bind(this);
|
||||
return result;
|
||||
},
|
||||
|
||||
_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);
|
||||
},
|
||||
};
|
||||
|
@ -6,6 +6,7 @@
|
||||
const modules = [
|
||||
"healthreporter.jsm",
|
||||
"policy.jsm",
|
||||
"profile.jsm",
|
||||
"providers.jsm",
|
||||
];
|
||||
|
||||
|
213
services/healthreport/tests/xpcshell/test_profile.js
Normal file
213
services/healthreport/tests/xpcshell/test_profile.js
Normal file
@ -0,0 +1,213 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
|
||||
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
// Create profile directory before use.
|
||||
// It can be no older than a day ago….
|
||||
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/services/healthreport/profile.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
function MockProfileMetadataProvider(name="MockProfileMetadataProvider") {
|
||||
ProfileMetadataProvider.call(this, name);
|
||||
}
|
||||
MockProfileMetadataProvider.prototype = {
|
||||
__proto__: ProfileMetadataProvider.prototype,
|
||||
|
||||
getProfileCreationDays: function getProfileCreationDays() {
|
||||
return Promise.resolve(1234);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
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.
|
||||
*/
|
||||
add_test(function use_os_file() {
|
||||
Cu.import("resource://gre/modules/osfile.jsm")
|
||||
|
||||
// Ensure that we get constants, too.
|
||||
do_check_neq(OS.Constants.Path.profileDir, null);
|
||||
|
||||
let iterator = new OS.File.DirectoryIterator(".");
|
||||
iterator.forEach(function onEntry(entry) {
|
||||
print("Got " + entry.path);
|
||||
}).then(function onSuccess() {
|
||||
iterator.close();
|
||||
print("Done.");
|
||||
run_next_test();
|
||||
}, function onFail() {
|
||||
iterator.close();
|
||||
do_throw("Iterating over current directory failed.");
|
||||
});
|
||||
});
|
||||
|
||||
function getAccessor() {
|
||||
let acc = new ProfileCreationTimeAccessor();
|
||||
print("Profile is " + acc.profilePath);
|
||||
return acc;
|
||||
}
|
||||
|
||||
add_test(function test_time_accessor_no_file() {
|
||||
let acc = getAccessor();
|
||||
|
||||
// There should be no file yet.
|
||||
acc.readTimes()
|
||||
.then(function onSuccess(json) {
|
||||
do_throw("File existed!");
|
||||
},
|
||||
function onFailure() {
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
|
||||
add_test(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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_time_accessor_creates_file() {
|
||||
let lower = profile_creation_lower;
|
||||
|
||||
// Ensure that provided contents are merged, and existing
|
||||
// files can be overwritten. These two things occur if we
|
||||
// read and then decide that we have to write.
|
||||
let acc = getAccessor();
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
add_test(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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_constructor() {
|
||||
let provider = new ProfileMetadataProvider("named");
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_profile_files() {
|
||||
let provider = new ProfileMetadataProvider();
|
||||
|
||||
function onSuccess(answer) {
|
||||
let now = Date.now() / MILLISECONDS_PER_DAY;
|
||||
print("Got " + answer + ", versus now = " + now);
|
||||
do_check_true(answer < now);
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
function onFailure(ex) {
|
||||
do_throw("Directory iteration failed: " + ex);
|
||||
}
|
||||
|
||||
provider.getProfileCreationDays().then(onSuccess, onFailure);
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
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"));
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
result.populate(result);
|
||||
}
|
||||
|
||||
add_test(function test_collect_constant_mock() {
|
||||
let provider = new MockProfileMetadataProvider();
|
||||
function valueTest(v) {
|
||||
do_check_eq(v, 1234);
|
||||
}
|
||||
test_collect_constant(provider, valueTest);
|
||||
});
|
||||
|
||||
add_test(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);
|
||||
});
|
@ -3,6 +3,7 @@ head = head.js
|
||||
tail =
|
||||
|
||||
[test_load_modules.js]
|
||||
[test_profile.js]
|
||||
[test_policy.js]
|
||||
[test_healthreporter.js]
|
||||
[test_provider_appinfo.js]
|
||||
|
Loading…
x
Reference in New Issue
Block a user