No bug - Update shield-recipe-client from GitHub r=Gijs

--HG--
extra : rebase_source : 4c9ffe3a55b5682215aa4e94dc35637c07798725
This commit is contained in:
Mythmon 2017-01-27 10:22:44 -08:00
Родитель 29d8cca0b9
Коммит e7ac97534f
30 изменённых файлов: 562 добавлений и 192 удалений

Просмотреть файл

@ -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]