Bug 1014482: make Assert.jsm methods globally available and deprecate XPCShell-test's custom assert methods. This changes do_check_matches() semantics significantly. r=gps

This commit is contained in:
Mike de Boer 2014-05-30 16:26:42 +02:00
parent 661f45cb5e
commit 8d2305e33c
2 changed files with 67 additions and 212 deletions

View File

@ -187,7 +187,7 @@ proto.report = function(failed, actual, expected, message, operator) {
throw err;
}
} else {
this._reporter(failed ? err : null, message, err.stack);
this._reporter(failed ? err : null, err.message, err.stack);
}
};

View File

@ -19,8 +19,23 @@ var _cleanupFunctions = [];
var _pendingTimers = [];
var _profileInitialized = false;
// Register the testing-common resource protocol early, to have access to its
// modules.
_register_modules_protocol_handler();
let _Promise = Components.utils.import("resource://gre/modules/Promise.jsm", this).Promise;
// Support a common assertion library, Assert.jsm.
let AssertCls = Components.utils.import("resource://testing-common/Assert.jsm", null).Assert;
// Pass a custom report function for xpcshell-test style reporting.
let Assert = new AssertCls(function(err, message, stack) {
if (err) {
do_report_result(false, err.message, err.stack);
} else {
do_report_result(true, message, stack);
}
});
let _log = function (action, params) {
if (typeof _XPCSHELL_PROCESS != "undefined") {
params.process = _XPCSHELL_PROCESS;
@ -313,34 +328,48 @@ function do_get_idle() {
// Map resource://test/ to current working directory and
// resource://testing-common/ to the shared test modules directory.
function _register_protocol_handlers() {
let (ios = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService)) {
let protocolHandler =
ios.getProtocolHandler("resource")
.QueryInterface(Components.interfaces.nsIResProtocolHandler);
let curDirURI = ios.newFileURI(do_get_cwd());
protocolHandler.setSubstitution("test", curDirURI);
let ios = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
let protocolHandler =
ios.getProtocolHandler("resource")
.QueryInterface(Components.interfaces.nsIResProtocolHandler);
if (this._TESTING_MODULES_DIR) {
let modulesFile = Components.classes["@mozilla.org/file/local;1"].
createInstance(Components.interfaces.nsILocalFile);
modulesFile.initWithPath(_TESTING_MODULES_DIR);
let curDirURI = ios.newFileURI(do_get_cwd());
protocolHandler.setSubstitution("test", curDirURI);
if (!modulesFile.exists()) {
throw new Error("Specified modules directory does not exist: " +
_TESTING_MODULES_DIR);
}
_register_modules_protocol_handler();
}
if (!modulesFile.isDirectory()) {
throw new Error("Specified modules directory is not a directory: " +
_TESTING_MODULES_DIR);
}
let modulesURI = ios.newFileURI(modulesFile);
protocolHandler.setSubstitution("testing-common", modulesURI);
}
function _register_modules_protocol_handler() {
if (!this._TESTING_MODULES_DIR) {
throw new Error("Please define a path where the testing modules can be " +
"found in a variable called '_TESTING_MODULES_DIR' before " +
"head.js is included.");
}
let ios = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
let protocolHandler =
ios.getProtocolHandler("resource")
.QueryInterface(Components.interfaces.nsIResProtocolHandler);
let modulesFile = Components.classes["@mozilla.org/file/local;1"].
createInstance(Components.interfaces.nsILocalFile);
modulesFile.initWithPath(_TESTING_MODULES_DIR);
if (!modulesFile.exists()) {
throw new Error("Specified modules directory does not exist: " +
_TESTING_MODULES_DIR);
}
if (!modulesFile.isDirectory()) {
throw new Error("Specified modules directory is not a directory: " +
_TESTING_MODULES_DIR);
}
let modulesURI = ios.newFileURI(modulesFile);
protocolHandler.setSubstitution("testing-common", modulesURI);
}
function _execute_test() {
@ -363,23 +392,11 @@ function _execute_test() {
// _TEST_FILE is dynamically defined by <runxpcshelltests.py>.
_load_files(_TEST_FILE);
// Support a common assertion library, Assert.jsm.
let Assert = Components.utils.import("resource://testing-common/Assert.jsm", null).Assert;
// Pass a custom report function for xpcshell-test style reporting.
let assertImpl = new Assert(function(err, message, stack) {
if (err) {
do_report_result(false, err.message, err.stack);
} else {
do_report_result(true, message, stack);
}
});
// Allow Assert.jsm methods to be tacked to the current scope.
this.export_assertions = function() {
for (let func in assertImpl) {
this[func] = assertImpl[func].bind(assertImpl);
}
};
this.Assert = assertImpl;
// Tack Assert.jsm methods to the current scope.
this.Assert = Assert;
for (let func in Assert) {
this[func] = Assert[func].bind(Assert);
}
try {
do_test_pending("MAIN run_test");
@ -687,12 +704,7 @@ function do_note_exception(ex, text) {
}
function _do_check_neq(left, right, stack, todo) {
if (!stack)
stack = Components.stack.caller;
var text = _wrap_with_quotes_if_necessary(left) + " != " +
_wrap_with_quotes_if_necessary(right);
do_report_result(left != right, text, stack, todo);
Assert.notEqual(left, right);
}
function do_check_neq(left, right, stack) {
@ -747,10 +759,7 @@ function _do_check_eq(left, right, stack, todo) {
}
function do_check_eq(left, right, stack) {
if (!stack)
stack = Components.stack.caller;
_do_check_eq(left, right, stack, false);
Assert.equal(left, right);
}
function todo_check_eq(left, right, stack) {
@ -761,10 +770,7 @@ function todo_check_eq(left, right, stack) {
}
function do_check_true(condition, stack) {
if (!stack)
stack = Components.stack.caller;
do_check_eq(condition, true, stack);
Assert.ok(condition);
}
function todo_check_true(condition, stack) {
@ -775,10 +781,7 @@ function todo_check_true(condition, stack) {
}
function do_check_false(condition, stack) {
if (!stack)
stack = Components.stack.caller;
do_check_eq(condition, false, stack);
Assert.ok(!condition, stack);
}
function todo_check_false(condition, stack) {
@ -788,163 +791,15 @@ function todo_check_false(condition, stack) {
todo_check_eq(condition, false, stack);
}
function do_check_null(condition, stack=Components.stack.caller) {
do_check_eq(condition, null, stack);
function do_check_null(condition, stack) {
Assert.equal(condition, null);
}
function todo_check_null(condition, stack=Components.stack.caller) {
todo_check_eq(condition, null, stack);
}
/**
* Check that |value| matches |pattern|.
*
* A |value| matches a pattern |pattern| if any one of the following is true:
*
* - |value| and |pattern| are both objects; |pattern|'s enumerable
* properties' values are valid patterns; and for each enumerable
* property |p| of |pattern|, plus 'length' if present at all, |value|
* has a property |p| whose value matches |pattern.p|. Note that if |j|
* has other properties not present in |p|, |j| may still match |p|.
*
* - |value| and |pattern| are equal string, numeric, or boolean literals
*
* - |pattern| is |undefined| (this is a wildcard pattern)
*
* - typeof |pattern| == "function", and |pattern(value)| is true.
*
* For example:
*
* do_check_matches({x:1}, {x:1}) // pass
* do_check_matches({x:1}, {}) // fail: all pattern props required
* do_check_matches({x:1}, {x:2}) // fail: values must match
* do_check_matches({x:1}, {x:1, y:2}) // pass: extra props tolerated
*
* // Property order is irrelevant.
* do_check_matches({x:"foo", y:"bar"}, {y:"bar", x:"foo"}) // pass
*
* do_check_matches({x:undefined}, {x:1}) // pass: 'undefined' is wildcard
* do_check_matches({x:undefined}, {x:2})
* do_check_matches({x:undefined}, {y:2}) // fail: 'x' must still be there
*
* // Patterns nest.
* do_check_matches({a:1, b:{c:2,d:undefined}}, {a:1, b:{c:2,d:3}})
*
* // 'length' property counts, even if non-enumerable.
* do_check_matches([3,4,5], [3,4,5]) // pass
* do_check_matches([3,4,5], [3,5,5]) // fail; value doesn't match
* do_check_matches([3,4,5], [3,4,5,6]) // fail; length doesn't match
*
* // functions in patterns get applied.
* do_check_matches({foo:function (v) v.length == 2}, {foo:"hi"}) // pass
* do_check_matches({foo:function (v) v.length == 2}, {bar:"hi"}) // fail
* do_check_matches({foo:function (v) v.length == 2}, {foo:"hello"}) // fail
*
* // We don't check constructors, prototypes, or classes. However, if
* // pattern has a 'length' property, we require values to match that as
* // well, even if 'length' is non-enumerable in the pattern. So arrays
* // are useful as patterns.
* do_check_matches({0:0, 1:1, length:2}, [0,1]) // pass
* do_check_matches({0:1}, [1,2]) // pass
* do_check_matches([0], {0:0, length:1}) // pass
*
* Notes:
*
* The 'length' hack gives us reasonably intuitive handling of arrays.
*
* This is not a tight pattern-matcher; it's only good for checking data
* from well-behaved sources. For example:
* - By default, we don't mind values having extra properties.
* - We don't check for proxies or getters.
* - We don't check the prototype chain.
* However, if you know the values are, say, JSON, which is pretty
* well-behaved, and if you want to tolerate additional properties
* appearing on the JSON for backward-compatibility, then do_check_matches
* is ideal. If you do want to be more careful, you can use function
* patterns to implement more stringent checks.
*/
function do_check_matches(pattern, value, stack=Components.stack.caller, todo=false) {
var matcher = pattern_matcher(pattern);
var text = "VALUE: " + uneval(value) + "\nPATTERN: " + uneval(pattern) + "\n";
var diagnosis = []
if (matcher(value, diagnosis)) {
do_report_result(true, "value matches pattern:\n" + text, stack, todo);
} else {
text = ("value doesn't match pattern:\n" +
text +
"DIAGNOSIS: " +
format_pattern_match_failure(diagnosis[0]) + "\n");
do_report_result(false, text, stack, todo);
}
}
function todo_check_matches(pattern, value, stack=Components.stack.caller) {
do_check_matches(pattern, value, stack, true);
}
// Return a pattern-matching function of one argument, |value|, that
// returns true if |value| matches |pattern|.
//
// If the pattern doesn't match, and the pattern-matching function was
// passed its optional |diagnosis| argument, the pattern-matching function
// sets |diagnosis|'s '0' property to a JSON-ish description of the portion
// of the pattern that didn't match, which can be formatted legibly by
// format_pattern_match_failure.
function pattern_matcher(pattern) {
function explain(diagnosis, reason) {
if (diagnosis) {
diagnosis[0] = reason;
}
return false;
}
if (typeof pattern == "function") {
return pattern;
} else if (typeof pattern == "object" && pattern) {
var matchers = [[p, pattern_matcher(pattern[p])] for (p in pattern)];
// Kludge: include 'length', if not enumerable. (If it is enumerable,
// we picked it up in the array comprehension, above.
var ld = Object.getOwnPropertyDescriptor(pattern, 'length');
if (ld && !ld.enumerable) {
matchers.push(['length', pattern_matcher(pattern.length)])
}
return function (value, diagnosis) {
if (!(value && typeof value == "object")) {
return explain(diagnosis, "value not object");
}
for (let [p, m] of matchers) {
var element_diagnosis = [];
if (!(p in value && m(value[p], element_diagnosis))) {
return explain(diagnosis, { property:p,
diagnosis:element_diagnosis[0] });
}
}
return true;
};
} else if (pattern === undefined) {
return function(value) { return true; };
} else {
return function (value, diagnosis) {
if (value !== pattern) {
return explain(diagnosis, "pattern " + uneval(pattern) + " not === to value " + uneval(value));
}
return true;
};
}
}
// Format an explanation for a pattern match failure, as stored in the
// second argument to a matching function.
function format_pattern_match_failure(diagnosis, indent="") {
var a;
if (!diagnosis) {
a = "Matcher did not explain reason for mismatch.";
} else if (typeof diagnosis == "string") {
a = diagnosis;
} else if (diagnosis.property) {
a = "Property " + uneval(diagnosis.property) + " of object didn't match:\n";
a += format_pattern_match_failure(diagnosis.diagnosis, indent + " ");
}
return indent + a;
function do_check_matches(pattern, value) {
Assert.deepEqual(pattern, value);
}
// Check that |func| throws an nsIException that has