зеркало из https://github.com/mozilla/gecko-dev.git
No bug - Update shield-recipe-client from GitHub r=Gijs
--HG-- extra : rebase_source : 4c9ffe3a55b5682215aa4e94dc35637c07798725
This commit is contained in:
Родитель
29d8cca0b9
Коммит
e7ac97534f
|
@ -6,6 +6,7 @@
|
|||
const {utils: Cu} = Components;
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
|
||||
const REASONS = {
|
||||
APP_STARTUP: 1, // The application is starting up.
|
||||
|
@ -24,9 +25,12 @@ const DEFAULT_PREFS = {
|
|||
dev_mode: false,
|
||||
enabled: true,
|
||||
startup_delay_seconds: 300,
|
||||
"logging.level": Log.Level.Warn,
|
||||
user_id: "",
|
||||
};
|
||||
const PREF_DEV_MODE = "extensions.shield-recipe-client.dev_mode";
|
||||
const PREF_SELF_SUPPORT_ENABLED = "browser.selfsupport.enabled";
|
||||
const PREF_LOGGING_LEVEL = PREF_BRANCH + "logging.level";
|
||||
|
||||
let shouldRun = true;
|
||||
|
||||
|
@ -48,11 +52,18 @@ this.startup = function() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Setup logging and listen for changes to logging prefs
|
||||
Cu.import("resource://shield-recipe-client/lib/LogManager.jsm");
|
||||
LogManager.configure(Services.prefs.getIntPref(PREF_LOGGING_LEVEL));
|
||||
Preferences.observe(PREF_LOGGING_LEVEL, LogManager.configure);
|
||||
|
||||
Cu.import("resource://shield-recipe-client/lib/RecipeRunner.jsm");
|
||||
RecipeRunner.init();
|
||||
};
|
||||
|
||||
this.shutdown = function(data, reason) {
|
||||
Preferences.ignore(PREF_LOGGING_LEVEL, LogManager.configure);
|
||||
|
||||
Cu.import("resource://shield-recipe-client/lib/CleanupManager.jsm");
|
||||
|
||||
CleanupManager.cleanup();
|
||||
|
@ -66,6 +77,7 @@ this.shutdown = function(data, reason) {
|
|||
"lib/CleanupManager.jsm",
|
||||
"lib/EnvExpressions.jsm",
|
||||
"lib/Heartbeat.jsm",
|
||||
"lib/LogManager.jsm",
|
||||
"lib/NormandyApi.jsm",
|
||||
"lib/NormandyDriver.jsm",
|
||||
"lib/RecipeRunner.jsm",
|
||||
|
|
|
@ -6,4 +6,5 @@
|
|||
% resource shield-recipe-client %content/
|
||||
content/lib/ (./lib/*)
|
||||
content/data/ (./data/*)
|
||||
content/test/ (./test/*)
|
||||
content/node_modules/jexl/ (./node_modules/jexl/*)
|
||||
|
|
|
@ -4,15 +4,19 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/TelemetryArchive.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://shield-recipe-client/lib/Sampling.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
|
||||
const {generateUUID} = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["EnvExpressions"];
|
||||
|
||||
const prefs = Services.prefs.getBranch("extensions.shield-recipe-client.");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "nodeRequire", () => {
|
||||
const {Loader, Require} = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
|
||||
const loader = new Loader({
|
||||
|
@ -29,37 +33,55 @@ XPCOMUtils.defineLazyGetter(this, "jexl", () => {
|
|||
jexl.addTransforms({
|
||||
date: dateString => new Date(dateString),
|
||||
stableSample: Sampling.stableSample,
|
||||
bucketSample: Sampling.bucketSample,
|
||||
});
|
||||
return jexl;
|
||||
});
|
||||
|
||||
const getLatestTelemetry = Task.async(function *() {
|
||||
const pings = yield TelemetryArchive.promiseArchivedPingList();
|
||||
this.EnvExpressions = {
|
||||
getLatestTelemetry: Task.async(function *() {
|
||||
const pings = yield TelemetryArchive.promiseArchivedPingList();
|
||||
|
||||
// get most recent ping per type
|
||||
const mostRecentPings = {};
|
||||
for (const ping of pings) {
|
||||
if (ping.type in mostRecentPings) {
|
||||
if (mostRecentPings[ping.type].timeStampCreated < ping.timeStampCreated) {
|
||||
// get most recent ping per type
|
||||
const mostRecentPings = {};
|
||||
for (const ping of pings) {
|
||||
if (ping.type in mostRecentPings) {
|
||||
if (mostRecentPings[ping.type].timeStampCreated < ping.timeStampCreated) {
|
||||
mostRecentPings[ping.type] = ping;
|
||||
}
|
||||
} else {
|
||||
mostRecentPings[ping.type] = ping;
|
||||
}
|
||||
} else {
|
||||
mostRecentPings[ping.type] = ping;
|
||||
}
|
||||
}
|
||||
|
||||
const telemetry = {};
|
||||
for (const key in mostRecentPings) {
|
||||
const ping = mostRecentPings[key];
|
||||
telemetry[ping.type] = yield TelemetryArchive.promiseArchivedPingById(ping.id);
|
||||
}
|
||||
return telemetry;
|
||||
});
|
||||
const telemetry = {};
|
||||
for (const key in mostRecentPings) {
|
||||
const ping = mostRecentPings[key];
|
||||
telemetry[ping.type] = yield TelemetryArchive.promiseArchivedPingById(ping.id);
|
||||
}
|
||||
return telemetry;
|
||||
}),
|
||||
|
||||
getUserId() {
|
||||
let id = prefs.getCharPref("user_id");
|
||||
if (id === "") {
|
||||
// generateUUID adds leading and trailing "{" and "}". strip them off.
|
||||
id = generateUUID().toString().slice(1, -1);
|
||||
prefs.setCharPref("user_id", id);
|
||||
}
|
||||
return id;
|
||||
},
|
||||
|
||||
this.EnvExpressions = {
|
||||
eval(expr, extraContext = {}) {
|
||||
const context = Object.assign({telemetry: getLatestTelemetry()}, extraContext);
|
||||
// First clone the extra context
|
||||
const context = Object.assign({}, extraContext);
|
||||
// jexl handles promises, so it is fine to include them in this data.
|
||||
context.telemetry = EnvExpressions.getLatestTelemetry();
|
||||
context.normandy = context.normandy || {};
|
||||
context.normandy.userId = EnvExpressions.getUserId();
|
||||
|
||||
const onelineExpr = expr.replace(/[\t\n\r]/g, " ");
|
||||
|
||||
return jexl.eval(onelineExpr, context);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -9,14 +9,14 @@ const {utils: Cu} = Components;
|
|||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
Cu.import("resource://gre/modules/TelemetryController.jsm");
|
||||
Cu.import("resource://gre/modules/Timer.jsm"); /* globals setTimeout, clearTimeout */
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://shield-recipe-client/lib/CleanupManager.jsm");
|
||||
Cu.import("resource://shield-recipe-client/lib/LogManager.jsm");
|
||||
|
||||
Cu.importGlobalProperties(["URL"]); /* globals URL */
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["Heartbeat"];
|
||||
|
||||
const log = Log.repository.getLogger("shield-recipe-client");
|
||||
const log = LogManager.getLogger("heartbeat");
|
||||
const PREF_SURVEY_DURATION = "browser.uitour.surveyDuration";
|
||||
const NOTIFICATION_TIME = 3000;
|
||||
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/* 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";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["LogManager"];
|
||||
|
||||
const ROOT_LOGGER_NAME = "extensions.shield-recipe-client"
|
||||
let rootLogger = null;
|
||||
|
||||
this.LogManager = {
|
||||
/**
|
||||
* Configure the root logger for the Recipe Client. Must be called at
|
||||
* least once before using any loggers created via getLogger.
|
||||
* @param {Number} loggingLevel
|
||||
* Logging level to use as defined in Log.jsm
|
||||
*/
|
||||
configure(loggingLevel) {
|
||||
if (!rootLogger) {
|
||||
rootLogger = Log.repository.getLogger(ROOT_LOGGER_NAME);
|
||||
rootLogger.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
|
||||
}
|
||||
rootLogger.level = loggingLevel;
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtain a named logger with the recipe client logger as its parent.
|
||||
* @param {String} name
|
||||
* Name of the logger to obtain.
|
||||
* @return {Logger}
|
||||
*/
|
||||
getLogger(name) {
|
||||
return Log.repository.getLogger(`${ROOT_LOGGER_NAME}.${name}`);
|
||||
},
|
||||
};
|
|
@ -9,12 +9,12 @@ const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
|
|||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/CanonicalJSON.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://shield-recipe-client/lib/LogManager.jsm");
|
||||
Cu.importGlobalProperties(["fetch"]); /* globals fetch */
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["NormandyApi"];
|
||||
|
||||
const log = Log.repository.getLogger("extensions.shield-recipe-client");
|
||||
const log = LogManager.getLogger("normandy-api");
|
||||
const prefs = Services.prefs.getBranch("extensions.shield-recipe-client.");
|
||||
|
||||
this.NormandyApi = {
|
||||
|
|
|
@ -12,16 +12,17 @@ Cu.import("resource://gre/modules/Preferences.jsm");
|
|||
Cu.import("resource:///modules/ShellService.jsm");
|
||||
Cu.import("resource://gre/modules/AddonManager.jsm");
|
||||
Cu.import("resource://gre/modules/Timer.jsm"); /* globals setTimeout, clearTimeout */
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://shield-recipe-client/lib/LogManager.jsm");
|
||||
Cu.import("resource://shield-recipe-client/lib/Storage.jsm");
|
||||
Cu.import("resource://shield-recipe-client/lib/Heartbeat.jsm");
|
||||
Cu.import("resource://shield-recipe-client/lib/EnvExpressions.jsm");
|
||||
|
||||
const {generateUUID} = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["NormandyDriver"];
|
||||
|
||||
const log = Log.repository.getLogger("extensions.shield-recipe-client");
|
||||
const actionLog = Log.repository.getLogger("extensions.shield-recipe-client.actions");
|
||||
const log = LogManager.getLogger("normandy-driver");
|
||||
const actionLog = LogManager.getLogger("normandy-driver.actions");
|
||||
|
||||
this.NormandyDriver = function(sandboxManager, extraContext = {}) {
|
||||
if (!sandboxManager) {
|
||||
|
@ -38,6 +39,10 @@ this.NormandyDriver = function(sandboxManager, extraContext = {}) {
|
|||
.getSelectedLocale("browser");
|
||||
},
|
||||
|
||||
get userId() {
|
||||
return EnvExpressions.getUserId();
|
||||
},
|
||||
|
||||
log(message, level = "debug") {
|
||||
const levels = ["debug", "info", "warn", "error"];
|
||||
if (levels.indexOf(level) === -1) {
|
||||
|
|
|
@ -8,7 +8,7 @@ const {utils: Cu} = Components;
|
|||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Timer.jsm"); /* globals setTimeout */
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://shield-recipe-client/lib/LogManager.jsm");
|
||||
Cu.import("resource://shield-recipe-client/lib/NormandyDriver.jsm");
|
||||
Cu.import("resource://shield-recipe-client/lib/EnvExpressions.jsm");
|
||||
Cu.import("resource://shield-recipe-client/lib/NormandyApi.jsm");
|
||||
|
@ -17,7 +17,7 @@ Cu.importGlobalProperties(["fetch"]); /* globals fetch */
|
|||
|
||||
this.EXPORTED_SYMBOLS = ["RecipeRunner"];
|
||||
|
||||
const log = Log.repository.getLogger("extensions.shield-recipe-client");
|
||||
const log = LogManager.getLogger("recipe-runner");
|
||||
const prefs = Services.prefs.getBranch("extensions.shield-recipe-client.");
|
||||
|
||||
this.RecipeRunner = {
|
||||
|
@ -72,7 +72,7 @@ this.RecipeRunner = {
|
|||
try {
|
||||
extraContext = yield this.getExtraContext();
|
||||
} catch (e) {
|
||||
log.warning(`Couldn't get extra filter context: ${e}`);
|
||||
log.warn(`Couldn't get extra filter context: ${e}`);
|
||||
extraContext = {};
|
||||
}
|
||||
|
||||
|
@ -151,24 +151,34 @@ this.RecipeRunner = {
|
|||
let a = new Action(sandboxedDriver, sandboxedRecipe);
|
||||
a.execute()
|
||||
.then(actionFinished)
|
||||
.catch(err => sandboxedDriver.log(err, 'error'));
|
||||
.catch(actionFailed);
|
||||
};
|
||||
|
||||
window.registerAction = registerAction;
|
||||
window.setTimeout = sandboxedDriver.setTimeout;
|
||||
window.clearTimeout = sandboxedDriver.clearTimeout;
|
||||
this.window = this;
|
||||
this.registerAction = registerAction;
|
||||
this.setTimeout = sandboxedDriver.setTimeout;
|
||||
this.clearTimeout = sandboxedDriver.clearTimeout;
|
||||
`;
|
||||
|
||||
const driver = new NormandyDriver(sandboxManager, extraContext);
|
||||
sandbox.sandboxedDriver = Cu.cloneInto(driver, sandbox, {cloneFunctions: true});
|
||||
sandbox.sandboxedRecipe = Cu.cloneInto(recipe, sandbox);
|
||||
|
||||
// Results are cloned so that they don't become inaccessible when
|
||||
// the sandbox they came from is nuked when the hold is removed.
|
||||
sandbox.actionFinished = result => {
|
||||
const clonedResult = Cu.cloneInto(result, {});
|
||||
sandboxManager.removeHold("recipeExecution");
|
||||
resolve(result);
|
||||
resolve(clonedResult);
|
||||
};
|
||||
sandbox.actionFailed = err => {
|
||||
Cu.reportError(err);
|
||||
|
||||
// Error objects can't be cloned, so we just copy the message
|
||||
// (which doesn't need to be cloned) to be somewhat useful.
|
||||
const message = err.message;
|
||||
sandboxManager.removeHold("recipeExecution");
|
||||
reject(err);
|
||||
reject(new Error(message));
|
||||
};
|
||||
|
||||
sandboxManager.addHold("recipeExecution");
|
||||
|
|
|
@ -5,77 +5,129 @@
|
|||
"use strict";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.importGlobalProperties(["crypto", "TextEncoder"]);
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["Sampling"];
|
||||
|
||||
const log = Log.repository.getLogger("extensions.shield-recipe-client");
|
||||
|
||||
/**
|
||||
* Map from the range [0, 1] to [0, max(sha256)].
|
||||
* @param {number} frac A float from 0.0 to 1.0.
|
||||
* @return {string} A 48 bit number represented in hex, padded to 12 characters.
|
||||
*/
|
||||
function fractionToKey(frac) {
|
||||
const hashBits = 48;
|
||||
const hashLength = hashBits / 4;
|
||||
|
||||
if (frac < 0 || frac > 1) {
|
||||
throw new Error(`frac must be between 0 and 1 inclusive (got ${frac})`);
|
||||
}
|
||||
|
||||
const mult = Math.pow(2, hashBits) - 1;
|
||||
const inDecimal = Math.floor(frac * mult);
|
||||
let hexDigits = inDecimal.toString(16);
|
||||
if (hexDigits.length < hashLength) {
|
||||
// Left pad with zeroes
|
||||
// If N zeroes are needed, generate an array of nulls N+1 elements long,
|
||||
// and inserts zeroes between each null.
|
||||
hexDigits = Array(hashLength - hexDigits.length + 1).join("0") + hexDigits;
|
||||
}
|
||||
|
||||
// Saturate at 2**48 - 1
|
||||
if (hexDigits.length > hashLength) {
|
||||
hexDigits = Array(hashLength + 1).join("f");
|
||||
}
|
||||
|
||||
return hexDigits;
|
||||
}
|
||||
|
||||
function bufferToHex(buffer) {
|
||||
const hexCodes = [];
|
||||
const view = new DataView(buffer);
|
||||
for (let i = 0; i < view.byteLength; i += 4) {
|
||||
// Using getUint32 reduces the number of iterations needed (we process 4 bytes each time)
|
||||
const value = view.getUint32(i);
|
||||
// toString(16) will give the hex representation of the number without padding
|
||||
hexCodes.push(value.toString(16).padStart(8, "0"));
|
||||
}
|
||||
|
||||
// Join all the hex strings into one
|
||||
return hexCodes.join("");
|
||||
}
|
||||
const hashBits = 48;
|
||||
const hashLength = hashBits / 4; // each hexadecimal digit represents 4 bits
|
||||
const hashMultiplier = Math.pow(2, hashBits) - 1;
|
||||
|
||||
this.Sampling = {
|
||||
stableSample(input, rate) {
|
||||
const hasher = crypto.subtle;
|
||||
/**
|
||||
* Map from the range [0, 1] to [0, 2^48].
|
||||
* @param {number} frac A float from 0.0 to 1.0.
|
||||
* @return {string} A 48 bit number represented in hex, padded to 12 characters.
|
||||
*/
|
||||
fractionToKey(frac) {
|
||||
if (frac < 0 || frac > 1) {
|
||||
throw new Error(`frac must be between 0 and 1 inclusive (got ${frac})`);
|
||||
}
|
||||
|
||||
return hasher.digest("SHA-256", new TextEncoder("utf-8").encode(JSON.stringify(input)))
|
||||
.then(hash => {
|
||||
// truncate hash to 12 characters (2^48)
|
||||
const inputHash = bufferToHex(hash).slice(0, 12);
|
||||
const samplePoint = fractionToKey(rate);
|
||||
|
||||
if (samplePoint.length !== 12 || inputHash.length !== 12) {
|
||||
throw new Error("Unexpected hash length");
|
||||
}
|
||||
|
||||
return inputHash < samplePoint;
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
log.error(`Error: ${error}`);
|
||||
});
|
||||
return Math.floor(frac * hashMultiplier).toString(16).padStart(hashLength, "0");
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} buffer Data to convert
|
||||
* @returns {String} `buffer`'s content, converted to a hexadecimal string.
|
||||
*/
|
||||
bufferToHex(buffer) {
|
||||
const hexCodes = [];
|
||||
const view = new DataView(buffer);
|
||||
for (let i = 0; i < view.byteLength; i += 4) {
|
||||
// Using getUint32 reduces the number of iterations needed (we process 4 bytes each time)
|
||||
const value = view.getUint32(i);
|
||||
// toString(16) will give the hex representation of the number without padding
|
||||
hexCodes.push(value.toString(16).padStart(8, "0"));
|
||||
}
|
||||
|
||||
// Join all the hex strings into one
|
||||
return hexCodes.join("");
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if an input hash is contained in a bucket range.
|
||||
*
|
||||
* isHashInBucket(fractionToKey(0.5), 3, 6, 10) -> returns true
|
||||
*
|
||||
* minBucket
|
||||
* | hash
|
||||
* v v
|
||||
* [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
* ^
|
||||
* maxBucket
|
||||
*
|
||||
* @param inputHash {String}
|
||||
* @param minBucket {int} The lower boundary, inclusive, of the range to check.
|
||||
* @param maxBucket {int} The upper boundary, exclusive, of the range to check.
|
||||
* @param bucketCount {int} The total number of buckets. Should be greater than
|
||||
* or equal to maxBucket.
|
||||
*/
|
||||
isHashInBucket(inputHash, minBucket, maxBucket, bucketCount) {
|
||||
const minHash = Sampling.fractionToKey(minBucket / bucketCount);
|
||||
const maxHash = Sampling.fractionToKey(maxBucket / bucketCount);
|
||||
return (minHash <= inputHash) && (inputHash < maxHash);
|
||||
},
|
||||
|
||||
/**
|
||||
* @promise A hash of `data`, truncated to the 12 most significant characters.
|
||||
*/
|
||||
truncatedHash: Task.async(function* (data) {
|
||||
const hasher = crypto.subtle;
|
||||
const input = new TextEncoder("utf-8").encode(JSON.stringify(data));
|
||||
const hash = yield hasher.digest("SHA-256", input);
|
||||
// truncate hash to 12 characters (2^48), because the full hash is larger
|
||||
// than JS can meaningfully represent as a number.
|
||||
return Sampling.bufferToHex(hash).slice(0, 12);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Sample by splitting the input into two buckets, one with a size (rate) and
|
||||
* another with a size (1.0 - rate), and then check if the input's hash falls
|
||||
* into the first bucket.
|
||||
*
|
||||
* @param {object} input Input to hash to determine the sample.
|
||||
* @param {Number} rate Number between 0.0 and 1.0 to sample at. A value of
|
||||
* 0.25 returns true 25% of the time.
|
||||
* @promises {boolean} True if the input is in the sample.
|
||||
*/
|
||||
stableSample: Task.async(function* (input, rate) {
|
||||
const inputHash = yield Sampling.truncatedHash(input);
|
||||
const samplePoint = Sampling.fractionToKey(rate);
|
||||
|
||||
return inputHash < samplePoint;
|
||||
}),
|
||||
|
||||
/**
|
||||
* Sample by splitting the input space into a series of buckets, and checking
|
||||
* if the given input is in a range of buckets.
|
||||
*
|
||||
* The range to check is defined by a start point and length, and can wrap
|
||||
* around the input space. For example, if there are 100 buckets, and we ask to
|
||||
* check 50 buckets starting from bucket 70, then buckets 70-99 and 0-19 will
|
||||
* be checked.
|
||||
*
|
||||
* @param {object} input Input to hash to determine the matching bucket.
|
||||
* @param {integer} start Index of the bucket to start checking.
|
||||
* @param {integer} count Number of buckets to check.
|
||||
* @param {integer} total Total number of buckets to group inputs into.
|
||||
* @promises {boolean} True if the given input is within the range of buckets
|
||||
* we're checking. */
|
||||
bucketSample: Task.async(function* (input, start, count, total) {
|
||||
const inputHash = yield Sampling.truncatedHash(input);
|
||||
const wrappedStart = start % total;
|
||||
const end = wrappedStart + count;
|
||||
|
||||
// If the range we're testing wraps, we have to check two ranges: from start
|
||||
// to max, and from min to end.
|
||||
if (end > total) {
|
||||
return (
|
||||
Sampling.isHashInBucket(inputHash, 0, end % total, total)
|
||||
|| Sampling.isHashInBucket(inputHash, wrappedStart, total, total)
|
||||
);
|
||||
}
|
||||
|
||||
return Sampling.isHashInBucket(inputHash, wrappedStart, end, total);
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -56,8 +56,6 @@ function makeSandbox() {
|
|||
wantGlobalProperties: ["URL", "URLSearchParams"],
|
||||
});
|
||||
|
||||
sandbox.window = Cu.cloneInto({}, sandbox);
|
||||
|
||||
const url = "resource://shield-recipe-client/data/EventEmitter.js";
|
||||
Services.scriptloader.loadSubScript(url, sandbox);
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
const {utils: Cu} = Components;
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://shield-recipe-client/lib/LogManager.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "JSONFile", "resource://gre/modules/JSONFile.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
|
||||
|
@ -14,7 +14,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm
|
|||
|
||||
this.EXPORTED_SYMBOLS = ["Storage"];
|
||||
|
||||
const log = Log.repository.getLogger("extensions.shield-recipe-client");
|
||||
const log = LogManager.getLogger("storage");
|
||||
let storePromise;
|
||||
|
||||
function loadStorage() {
|
||||
|
@ -131,4 +131,18 @@ this.Storage = {
|
|||
cloneFunctions: true,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear ALL storage data and save to the disk.
|
||||
*/
|
||||
clearAllStorage() {
|
||||
return loadStorage()
|
||||
.then(store => {
|
||||
store.data = {};
|
||||
store.saveSoon();
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -15,8 +15,8 @@ FINAL_TARGET_PP_FILES.features['shield-recipe-client@mozilla.org'] += [
|
|||
'install.rdf.in'
|
||||
]
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += [
|
||||
'test/browser.ini',
|
||||
]
|
||||
BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
|
|
|
@ -9,9 +9,11 @@ module.exports = {
|
|||
isnot: false,
|
||||
ok: false,
|
||||
SpecialPowers: false,
|
||||
SimpleTest: false,
|
||||
},
|
||||
rules: {
|
||||
"spaced-comment": 2,
|
||||
"space-before-function-paren": 2,
|
||||
"require-yield": 0
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
/* 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";
|
||||
|
||||
/* eslint-disable no-console */
|
||||
this.EXPORTED_SYMBOLS = ["TestUtils"];
|
||||
|
||||
this.TestUtils = {
|
||||
promiseTest(test) {
|
||||
return function(assert, done) {
|
||||
test(assert)
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
assert.ok(false, err);
|
||||
})
|
||||
.then(() => done());
|
||||
};
|
||||
},
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
const {utils: Cu} = Components;
|
||||
Cu.import("resource://shield-recipe-client/lib/SandboxManager.jsm", this);
|
||||
Cu.import("resource://shield-recipe-client/lib/NormandyDriver.jsm", this);
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["Utils"];
|
||||
|
||||
this.Utils = {
|
||||
UUID_REGEX: /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/,
|
||||
|
||||
withSandboxManager(Assert, testGenerator) {
|
||||
return function* inner() {
|
||||
const sandboxManager = new SandboxManager();
|
||||
sandboxManager.addHold("test running");
|
||||
|
||||
yield testGenerator(sandboxManager);
|
||||
|
||||
sandboxManager.removeHold("test running");
|
||||
yield sandboxManager.isNuked()
|
||||
.then(() => Assert.ok(true, "sandbox is nuked"))
|
||||
.catch(e => Assert.ok(false, "sandbox is nuked", e));
|
||||
};
|
||||
},
|
||||
|
||||
withDriver(Assert, testGenerator) {
|
||||
return Utils.withSandboxManager(Assert, function* inner(sandboxManager) {
|
||||
const driver = new NormandyDriver(sandboxManager);
|
||||
yield testGenerator(driver);
|
||||
});
|
||||
},
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
[browser_driver_uuids.js]
|
||||
[browser_env_expressions.js]
|
||||
[browser_NormandyDriver.js]
|
||||
[browser_EnvExpressions.js]
|
||||
[browser_EventEmitter.js]
|
||||
[browser_Storage.js]
|
||||
[browser_Heartbeat.js]
|
||||
|
@ -7,3 +7,4 @@
|
|||
support-files =
|
||||
test_server.sjs
|
||||
[browser_RecipeRunner.js]
|
||||
[browser_LogManager.js]
|
|
@ -1,11 +1,12 @@
|
|||
"use strict";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
Cu.import("resource://gre/modules/TelemetryController.jsm", this);
|
||||
Cu.import("resource://gre/modules/Task.jsm", this);
|
||||
|
||||
Cu.import("resource://shield-recipe-client/lib/EnvExpressions.jsm", this);
|
||||
Cu.import("resource://gre/modules/Log.jsm", this);
|
||||
Cu.import("resource://shield-recipe-client/test/browser/Utils.jsm", this);
|
||||
|
||||
add_task(function* () {
|
||||
// setup
|
||||
|
@ -53,4 +54,32 @@ add_task(function* () {
|
|||
// Test stable sample returns true for matching samples
|
||||
val = yield EnvExpressions.eval('["test"]|stableSample(0)');
|
||||
is(val, false, "Stable sample returns false for 0% sample");
|
||||
|
||||
// Test stable sample for known samples
|
||||
val = yield EnvExpressions.eval('["test-1"]|stableSample(0.5)');
|
||||
is(val, true, "Stable sample returns true for a known sample");
|
||||
val = yield EnvExpressions.eval('["test-4"]|stableSample(0.5)');
|
||||
is(val, false, "Stable sample returns false for a known sample");
|
||||
|
||||
// Test bucket sample for known samples
|
||||
val = yield EnvExpressions.eval('["test-1"]|bucketSample(0, 5, 10)');
|
||||
is(val, true, "Bucket sample returns true for a known sample");
|
||||
val = yield EnvExpressions.eval('["test-4"]|bucketSample(0, 5, 10)');
|
||||
is(val, false, "Bucket sample returns false for a known sample");
|
||||
|
||||
// Test that userId is available
|
||||
val = yield EnvExpressions.eval("normandy.userId");
|
||||
ok(Utils.UUID_REGEX.test(val), "userId available");
|
||||
|
||||
// test that it pulls from the right preference
|
||||
yield SpecialPowers.pushPrefEnv({set: [["extensions.shield-recipe-client.user_id", "fake id"]]});
|
||||
val = yield EnvExpressions.eval("normandy.userId");
|
||||
Assert.equal(val, "fake id", "userId is pulled from preferences");
|
||||
|
||||
// test that it merges context correctly, `userId` comes from the default context, and
|
||||
// `injectedValue` comes from us. Expect both to be on the final `normandy` object.
|
||||
val = yield EnvExpressions.eval(
|
||||
"[normandy.userId, normandy.injectedValue]",
|
||||
{normandy: {injectedValue: "injected"}});
|
||||
Assert.deepEqual(val, ["fake id", "injected"], "context is correctly merged");
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
"use strict";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Log.jsm", this);
|
||||
Cu.import("resource://shield-recipe-client/lib/LogManager.jsm", this);
|
||||
|
||||
add_task(function*() {
|
||||
// Ensure that configuring the logger affects all generated loggers.
|
||||
const firstLogger = LogManager.getLogger("first");
|
||||
LogManager.configure(5);
|
||||
const secondLogger = LogManager.getLogger("second");
|
||||
is(firstLogger.level, 5, "First logger level inherited from root logger.");
|
||||
is(secondLogger.level, 5, "Second logger level inherited from root logger.");
|
||||
|
||||
// Ensure that our loggers have at least one appender.
|
||||
LogManager.configure(Log.Level.Warn);
|
||||
const logger = LogManager.getLogger("test");
|
||||
ok(logger.appenders.length > 0, true, "Loggers have at least one appender.");
|
||||
|
||||
// Ensure our loggers log to the console.
|
||||
yield new Promise(resolve => {
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
SimpleTest.monitorConsole(resolve, [{message: /legend has it/}]);
|
||||
logger.warn("legend has it");
|
||||
SimpleTest.endMonitorConsole();
|
||||
});
|
||||
});
|
|
@ -10,12 +10,12 @@ add_task(function* () {
|
|||
[
|
||||
"extensions.shield-recipe-client.api_url",
|
||||
"http://mochi.test:8888/browser/browser/extensions/shield-recipe-client/test",
|
||||
]
|
||||
]
|
||||
})
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
// Test that NormandyApi can fetch from the test server.
|
||||
const response = yield NormandyApi.get("test_server.sjs");
|
||||
const response = yield NormandyApi.get("browser/test_server.sjs");
|
||||
const data = yield response.json();
|
||||
Assert.deepEqual(data, {test: "data"}, "NormandyApi returned incorrect server data.");
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
"use strict";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
Cu.import("resource://shield-recipe-client/test/browser/Utils.jsm", this);
|
||||
Cu.import("resource://gre/modules/Console.jsm", this);
|
||||
|
||||
add_task(Utils.withDriver(Assert, function* uuids(driver) {
|
||||
// Test that it is a UUID
|
||||
const uuid1 = driver.uuid();
|
||||
ok(Utils.UUID_REGEX.test(uuid1), "valid uuid format");
|
||||
|
||||
// Test that UUIDs are different each time
|
||||
const uuid2 = driver.uuid();
|
||||
isnot(uuid1, uuid2, "uuids are unique");
|
||||
}));
|
||||
|
||||
add_task(Utils.withDriver(Assert, function* userId(driver) {
|
||||
// Test that userId is a UUID
|
||||
ok(Utils.UUID_REGEX.test(driver.userId), "userId is a uuid");
|
||||
}));
|
|
@ -0,0 +1,87 @@
|
|||
"use strict";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
Cu.import("resource://shield-recipe-client/lib/RecipeRunner.jsm", this);
|
||||
|
||||
add_task(function* execute() {
|
||||
// Test that RecipeRunner can execute a basic recipe/action and return
|
||||
// the result of execute.
|
||||
const recipe = {
|
||||
foo: "bar",
|
||||
};
|
||||
const actionScript = `
|
||||
class TestAction {
|
||||
constructor(driver, recipe) {
|
||||
this.recipe = recipe;
|
||||
}
|
||||
|
||||
execute() {
|
||||
return new Promise(resolve => {
|
||||
resolve({foo: this.recipe.foo});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerAction('test-action', TestAction);
|
||||
`;
|
||||
|
||||
const result = yield RecipeRunner.executeAction(recipe, {}, actionScript);
|
||||
is(result.foo, "bar", "Recipe executed correctly");
|
||||
});
|
||||
|
||||
add_task(function* error() {
|
||||
// Test that RecipeRunner rejects with error messages from within the
|
||||
// sandbox.
|
||||
const actionScript = `
|
||||
class TestAction {
|
||||
execute() {
|
||||
return new Promise((resolve, reject) => {
|
||||
reject(new Error("ERROR MESSAGE"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerAction('test-action', TestAction);
|
||||
`;
|
||||
|
||||
let gotException = false;
|
||||
try {
|
||||
yield RecipeRunner.executeAction({}, {}, actionScript);
|
||||
} catch (err) {
|
||||
gotException = true;
|
||||
is(err.message, "ERROR MESSAGE", "RecipeRunner throws errors from the sandbox correctly.");
|
||||
}
|
||||
ok(gotException, "RecipeRunner threw an error from the sandbox.");
|
||||
});
|
||||
|
||||
add_task(function* globalObject() {
|
||||
// Test that window is an alias for the global object, and that it
|
||||
// has some expected functions available on it.
|
||||
const actionScript = `
|
||||
window.setOnWindow = "set";
|
||||
this.setOnGlobal = "set";
|
||||
|
||||
class TestAction {
|
||||
execute() {
|
||||
return new Promise(resolve => {
|
||||
resolve({
|
||||
setOnWindow: setOnWindow,
|
||||
setOnGlobal: window.setOnGlobal,
|
||||
setTimeoutExists: setTimeout !== undefined,
|
||||
clearTimeoutExists: clearTimeout !== undefined,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerAction('test-action', TestAction);
|
||||
`;
|
||||
|
||||
const result = yield RecipeRunner.executeAction({}, {}, actionScript);
|
||||
Assert.deepEqual(result, {
|
||||
setOnWindow: "set",
|
||||
setOnGlobal: "set",
|
||||
setTimeoutExists: true,
|
||||
clearTimeoutExists: true,
|
||||
}, "sandbox.window is the global object and has expected functions.");
|
||||
});
|
|
@ -34,4 +34,14 @@ add_task(function* () {
|
|||
const complex = {a: 1, b: [2, 3], c: {d: 4}};
|
||||
yield store1.setItem("complex", complex);
|
||||
Assert.deepEqual(yield store1.getItem("complex"), complex);
|
||||
|
||||
// Check that clearing the storage removes data from multiple
|
||||
// prefixes.
|
||||
yield store1.setItem("removeTest", 1);
|
||||
yield store2.setItem("removeTest", 2);
|
||||
Assert.equal(yield store1.getItem("removeTest"), 1);
|
||||
Assert.equal(yield store2.getItem("removeTest"), 2);
|
||||
yield Storage.clearAllStorage();
|
||||
Assert.equal(yield store1.getItem("removeTest"), null);
|
||||
Assert.equal(yield store2.getItem("removeTest"), null);
|
||||
});
|
|
@ -1,29 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
Cu.import("resource://shield-recipe-client/lib/RecipeRunner.jsm", this);
|
||||
|
||||
add_task(function*() {
|
||||
// Test that RecipeRunner can execute a basic recipe/action.
|
||||
const recipe = {
|
||||
foo: "bar",
|
||||
};
|
||||
const actionScript = `
|
||||
class TestAction {
|
||||
constructor(driver, recipe) {
|
||||
this.recipe = recipe;
|
||||
}
|
||||
|
||||
execute() {
|
||||
return new Promise(resolve => {
|
||||
resolve(this.recipe.foo);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerAction('test-action', TestAction);
|
||||
`;
|
||||
|
||||
const result = yield RecipeRunner.executeAction(recipe, {}, actionScript);
|
||||
is(result, "bar", "Recipe executed correctly");
|
||||
});
|
|
@ -1,26 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
Cu.import("resource://shield-recipe-client/lib/NormandyDriver.jsm", this);
|
||||
Cu.import("resource://shield-recipe-client/lib/SandboxManager.jsm", this);
|
||||
|
||||
add_task(function* () {
|
||||
const sandboxManager = new SandboxManager();
|
||||
sandboxManager.addHold("test running");
|
||||
let driver = new NormandyDriver(sandboxManager);
|
||||
|
||||
// Test that UUID look about right
|
||||
const uuid1 = driver.uuid();
|
||||
ok(/^[a-f0-9-]{36}$/.test(uuid1), "valid uuid format");
|
||||
|
||||
// Test that UUIDs are different each time
|
||||
const uuid2 = driver.uuid();
|
||||
isnot(uuid1, uuid2, "uuids are unique");
|
||||
|
||||
driver = null;
|
||||
sandboxManager.removeHold("test running");
|
||||
|
||||
yield sandboxManager.isNuked()
|
||||
.then(() => ok(true, "sandbox is nuked"))
|
||||
.catch(e => ok(false, "sandbox is nuked", e));
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
"use strict";
|
||||
// Cu is defined in xpc_head.js
|
||||
/* globals Cu, equal */
|
||||
|
||||
Cu.import("resource://gre/modules/Task.jsm", this);
|
||||
Cu.import("resource://shield-recipe-client/lib/Sampling.jsm", this);
|
||||
|
||||
add_task(function* testStableSample() {
|
||||
// Absolute samples
|
||||
equal(yield Sampling.stableSample("test", 1), true, "stableSample returns true for 100% sample");
|
||||
equal(yield Sampling.stableSample("test", 0), false, "stableSample returns false for 0% sample");
|
||||
|
||||
// Known samples. The numbers are nonces to make the tests pass
|
||||
equal(yield Sampling.stableSample("test-0", 0.5), true, "stableSample returns true for known matching sample");
|
||||
equal(yield Sampling.stableSample("test-1", 0.5), false, "stableSample returns false for known non-matching sample");
|
||||
});
|
||||
|
||||
add_task(function* testBucketSample() {
|
||||
// Absolute samples
|
||||
equal(yield Sampling.bucketSample("test", 0, 10, 10), true, "bucketSample returns true for 100% sample");
|
||||
equal(yield Sampling.bucketSample("test", 0, 0, 10), false, "bucketSample returns false for 0% sample");
|
||||
|
||||
// Known samples. The numbers are nonces to make the tests pass
|
||||
equal(yield Sampling.bucketSample("test-0", 0, 5, 10), true, "bucketSample returns true for known matching sample");
|
||||
equal(yield Sampling.bucketSample("test-1", 0, 5, 10), false, "bucketSample returns false for known non-matching sample");
|
||||
});
|
||||
|
||||
add_task(function* testFractionToKey() {
|
||||
// Test that results are always 12 character hexadecimal strings.
|
||||
const expected_regex = /[0-9a-f]{12}/;
|
||||
const count = 100;
|
||||
let successes = 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const p = Sampling.fractionToKey(Math.random());
|
||||
if (expected_regex.test(p)) {
|
||||
successes++;
|
||||
}
|
||||
}
|
||||
equal(successes, count, "fractionToKey makes keys the right length");
|
||||
});
|
||||
|
||||
add_task(function* testTruncatedHash() {
|
||||
const expected_regex = /[0-9a-f]{12}/;
|
||||
const count = 100;
|
||||
let successes = 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const h = yield Sampling.truncatedHash(Math.random());
|
||||
if (expected_regex.test(h)) {
|
||||
successes++;
|
||||
}
|
||||
}
|
||||
equal(successes, count, "truncatedHash makes hashes the right length");
|
||||
});
|
||||
|
||||
add_task(function* testBufferToHex() {
|
||||
const data = new ArrayBuffer(4);
|
||||
const view = new DataView(data);
|
||||
view.setUint8(0, 0xff);
|
||||
view.setUint8(1, 0x7f);
|
||||
view.setUint8(2, 0x3f);
|
||||
view.setUint8(3, 0x1f);
|
||||
equal(Sampling.bufferToHex(data), "ff7f3f1f");
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
"use strict";
|
||||
|
||||
const {interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
// Load our bootstrap extension manifest so we can access our chrome/resource URIs.
|
||||
// Cargo culted from formautofill system add-on
|
||||
const EXTENSION_ID = "shield-recipe-client@mozilla.org";
|
||||
let extensionDir = Services.dirsvc.get("GreD", Ci.nsIFile);
|
||||
extensionDir.append("browser");
|
||||
extensionDir.append("features");
|
||||
extensionDir.append(EXTENSION_ID);
|
||||
// If the unpacked extension doesn't exist, use the packed version.
|
||||
if (!extensionDir.exists()) {
|
||||
extensionDir = extensionDir.parent;
|
||||
extensionDir.append(EXTENSION_ID + ".xpi");
|
||||
}
|
||||
Components.manager.addBootstrappedManifestLocation(extensionDir);
|
|
@ -0,0 +1,4 @@
|
|||
[DEFAULT]
|
||||
head = xpc_head.js
|
||||
|
||||
[test_Sampling.js]
|
Загрузка…
Ссылка в новой задаче