Continued from phase 1; please read it first for background.
Overview
This phase focuses on assertions. Here's where things get ugly. Catch uses macro expansions to decompose assertion expressions. Since we don't have that luxury in JavaScript without adding a build-time requirement, we have to settle for Function.prototype.toString
and eval
. For example, take this simple assertion:
var x = 2;
REQUIRE(x == 3);
This assertion will fail, and when it does, it needs to be able to provide information about why it failed. It needs to tell us something like this:
FAILED:
REQUIRE(x == 3)
, with expansion2 == 3
.
In order to do that, we need to transform the REQUIRE(x == 3)
invocation into something like this (this is a slightly simplified example of what the current implementation would produce):
REQUIRE(x == 3, "x == 3", x + " == " + 3)
In other words, the REQUIRE
function has a signature like the following, and invocations in test cases are transformed to match it.
function REQUIRE(result, expression, expansion) { ... }
Restrictions
Fortunately, tests are meant to be run in a fairly controlled environment, and the issues that come from transforming a function to a string, doing some simple regex replacement, and transforming back to a function again can be avoided by following these simple rules:
Test cases may only rely on values in the global scope. In other words, if the test cases themselves are not in the global scope, they should not reference anything in their local scope.
Only one assertion is allowed per line, and only one line per assertion. Multiple assertions must not be placed on a line, and assertions must not span multiple lines.
Assertion expressions should be kept simple. In particular, they must not include string literals containing anything that looks remotely like a comparison or logical operator.
The first rule cannot be avoided, as far as I can tell. The second two could be avoided by doing some proper parsing instead of regex replacement, but I'm not sure it's worth the code bloat -- those rules seem reasonable, and sensible workarounds exist.
Source
The code from phase 1 has only been modified slightly. I'll probably post a final question with the complete source after all phases are complete.
Right now, only the REQUIRE
and CHECK
assertion macros are present. So far, I don't see a need for REQUIRE_FALSE
and CHECK_FALSE
in this port. I may add a "phase 2.1" for the exception handling macros if it's worth a separate post, otherwise I'll just leave it for the final question.
Here's the new stuff from this phase:
function Assertion(passed, expression, expansion, type) {
this.passed = passed;
this.expression = expression;
this.expansion = expansion;
this.type = type;
}
function assert(passed, expression, expansion, type) {
var outer = context[0],
assertion = new Assertion(passed, expression, expansion, type);
outer.assertions.push(assertion);
return assertion;
}
/** Require.
Test an expression and abort the test case if it fails.
*/
global['REQUIRE'] = function(result, expression, expansion, type) {
var assertion = assert(result, expression, expansion, type);
if (!result) {
// Not doing anything special with the exception yet,
// so just throw the assertion for now.
throw assertion;
}
};
/** Check.
Test an expression and continue execution of the test case.
*/
global['CHECK'] = function(result, expression, expansion, type) {
assert(result, expression, expansion, type);
};
//
// Preprocessing (rewrite test case functions)
//
var macroPattern = /(REQUIRE|CHECK)\s*\((.*)\)/g,
operatorPattern = /\s*([=!<>]{2,3}|\|\||&&)\s*/g;
function quoteOperators(string) {
return ')+"' + string + '"+(';
}
function decompose(assertion) {
return '(' + assertion.replace(operatorPattern, quoteOperators) + ')';
}
function expand(string, functionName, assertion) {
var expression = JSON.stringify(assertion),
expansion = decompose(assertion),
args = [assertion, expression, expansion, JSON.stringify(functionName)];
return functionName + '(' + args + ')';
}
function rewriteTestCase(body) {
return ('(' + body + ')').replace(macroPattern, expand);
}
The TEST_CASE
function has been modified to call body = eval(rewriteTestCase(body))
, and error handling has been added to Section.prototype.run
to catch thrown assertions.
Edit: in response to konjin's answer, renamed assertion
to expression
in a few places.
function decompose(expression) {
return '(' + expression.replace(operatorPattern, quoteOperator) + ')';
}
function expand(string, functionName, expression) {
var quotedExpression = JSON.stringify(expression),
expansion = decompose(expression),
type = JSON.stringify(functionName),
args = [expression, quotedExpression, expansion, type];
return functionName + '(' + args + ')';
}