Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate better default messages for isTrue(). #117

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ Asserts successfully when the condition is true.

`cond` The condition to test

`msg` An optional error message. If not passed a default one will be used
`msg` An optional error message. If not passed [a default one will be used](#reducing-message-detail).

`pos` Code position where the Assert call has been executed. Don't fill it
unless you know what you are doing.
Expand All @@ -284,7 +284,7 @@ Asserts successfully when the condition is false.

`cond` The condition to test

`msg` An optional error message. If not passed a default one will be used
`msg` An optional error message. If not passed [a default one will be used](#reducing-message-detail).

`pos` Code position where the Assert call has been executed. Don't fill it
unless you know what you are doing.
Expand Down Expand Up @@ -502,3 +502,27 @@ Creates a warning message.
`msg` A mandatory message that justifies the warning.

`pos` Code position where the Assert call has been executed. Don't fill it

## Increasing message detail

Normally, when `Assert.isTrue` and `Assert.isFalse` fail, they only print "expected false" or "expected true", making it hard to diagnose the issue. To make diagnosis easier, utest offers the `-D UTEST_DETAILED_MESSAGES` option. Setting this makes the default messages more informative, while leaving custom messages untouched.

```haxe
function testSum() {
var x = 3;
Assert.isTrue(2 + 2 == x + x); // Failed: 2 + 2 == x + x. Values: 4 == 6
Assert.isTrue(2 + 3 >= x + x); // Failed: 2 + 3 >= x + x. Values: 5 >= 6
Assert.isFalse(2 + 4 <= x + x); // Failed: 2 + 4 <= x + x should be false. Values: 6 <= 6
Assert.isTrue(2 + 3 >= x + x, "my custom message"); // my custom message

var array = [1, 2, 4, 8];
Assert.isTrue(array.contains(x)); // Failed: array.contains(x). Values: [1,2,4,8].contains(3)
Assert.isFalse(array.contains(2), "didn't expect 2"); // didn't expect 2

// Currently, only binary operators and function calls are supported.
// Other expressions such as array access will fall back to the default.
var bools = [true, false];
Assert.isFalse(bools[0]); // expected false
Assert.isTrue(bools[1]); // expected true
}
```
82 changes: 76 additions & 6 deletions src/utest/utils/TestBuilder.hx
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ class TestBuilder {
case _:
error('Wrong arguments count. The only supported argument is utest.Async for asynchronous tests.', field.pos);
}
if(field.name.indexOf(TEST_PREFIX) == 0 && fn.expr != null && Context.defined("UTEST_DETAILED_MESSAGES")) {
fn.expr = prepareTest(fn.expr);
}
//specification test
if(field.name.indexOf(SPEC_PREFIX) == 0 && fn.expr != null) {
fn.expr = prepareSpec(fn.expr);
Expand Down Expand Up @@ -283,6 +286,71 @@ class TestBuilder {
return ancestorHasInitializeUtest(superClass);
}

static function prepareTest(expr:Expr) {
return switch(expr.expr) {
case ECall({expr: EField({expr: EConst(CIdent("Assert")) | EField(_, "Assert")}, assertion = "isTrue" | "isFalse")}, [condition]):
switch(condition.expr) {
case EBinop(op = OpEq | OpNotEq | OpGt | OpGte | OpLt | OpLte, left, right):
parseSpecBinop(condition, op, left, right, assertion);
case EUnop(op = OpNot, prefix, subj):
parseSpecUnop(condition, op, prefix, subj, assertion);
case ECall(func, args):
parseTestCall(condition, func, args, assertion);
default:
expr;
}
case _:
ExprTools.map(expr, prepareTest);
}
}

static function parseTestCall(expr:Expr, func:Expr, args:Array<Expr>, ?assertion:String = "isTrue"):Expr {
var funcStr = ExprTools.toString(func);
var funcValue = macro $v{funcStr};
var argsStr:String = [for(arg in args) ExprTools.toString(arg)].join(", ");

var vars:Array<Var> = [];
switch(func.expr) {
case EField(thisValue, funcName) if(!isCapitalized(thisValue)):
vars.push({name: "_utest_this", expr: thisValue});
func = {expr: EField(macro _utest_this, funcName), pos: func.pos};
funcValue = macro _utest_this + "." + $v{funcName};
default:
}

var varValues:Array<Expr> = [];
var argValues:Array<Expr> = [];
for(i => arg in args) {
if(isCapitalized(arg)) {
varValues.push(arg);
argValues.push(macro $v{ExprTools.toString(arg)});
} else {
var varName = "_utest_arg_" + i;
vars.push({name: varName, expr: arg});
varValues.push(macro $i{varName});
argValues.push(macro Std.string($i{varName}));
}
}
var varsExpr = {expr: EVars(vars), pos: expr.pos};

var expected = assertion == "isFalse" ? " should be false" : "";
return macro @:pos(expr.pos) {
$varsExpr;
var _utest_msg = "Failed: " + $v{funcStr} + "(" + $v{argsStr} + ")" + $v{expected} + ". "
+ "Values: " + $funcValue + "(" + $a{argValues}.join(", ") + ")";
utest.Assert.$assertion($func($a{varValues}), _utest_msg);
}
}

static function isCapitalized(expr:Expr) {
return switch(expr.expr) {
case EConst(CIdent(ident)), EField(_, ident):
~/^[A-Z]/.match(ident);
default:
false;
}
}

static function prepareSpec(expr:Expr) {
return switch(expr.expr) {
case EBinop(op, left, right):
Expand All @@ -294,7 +362,7 @@ class TestBuilder {
}
}

static function parseSpecBinop(expr:Expr, op:Binop, left:Expr, right:Expr):Expr {
static function parseSpecBinop(expr:Expr, op:Binop, left:Expr, right:Expr, ?assertion:String = "isTrue"):Expr {
switch op {
case OpEq | OpNotEq | OpGt | OpGte | OpLt | OpLte:
var leftStr = ExprTools.toString(left);
Expand All @@ -304,19 +372,20 @@ class TestBuilder {
expr:EBinop(op, macro @:pos(left.pos) _utest_left, macro @:pos(right.pos) _utest_right),
pos:expr.pos
}
var expected = assertion == "isFalse" ? " should be false" : "";
return macro @:pos(expr.pos) {
var _utest_left = $left;
var _utest_right = $right;
var _utest_msg = "Failed: " + $v{leftStr} + " " + $v{opStr} + " " + $v{rightStr} + ". "
var _utest_msg = "Failed: " + $v{leftStr} + " " + $v{opStr} + " " + $v{rightStr} + $v{expected} + ". "
+ "Values: " + _utest_left + " " + $v{opStr} + " " + _utest_right;
utest.Assert.isTrue($binop, _utest_msg);
utest.Assert.$assertion($binop, _utest_msg);
}
case _:
return ExprTools.map(expr, prepareSpec);
}
}

static function parseSpecUnop(expr:Expr, op:Unop, prefix:Bool, subj:Expr):Expr {
static function parseSpecUnop(expr:Expr, op:Unop, prefix:Bool, subj:Expr, ?assertion:String = "isTrue"):Expr {
switch op {
case OpNot if(!prefix):
var subjStr = ExprTools.toString(subj);
Expand All @@ -325,11 +394,12 @@ class TestBuilder {
expr: EUnop(op, prefix, macro @:pos(subj.pos) _utest_subj),
pos: expr.pos
}
var expected = assertion == "isFalse" ? " should be false" : "";
return macro @:pos(expr.pos) {
var _utest_subj = $subj;
var _utest_msg = "Failed: " + $v{opStr} + $v{subjStr} + ". "
var _utest_msg = "Failed: " + $v{opStr} + $v{subjStr} + $v{expected} + ". "
+ "Values: " + $v{opStr} + _utest_subj;
utest.Assert.isTrue($unop, _utest_msg);
utest.Assert.$assertion($unop, _utest_msg);
}
case _:
return ExprTools.map(expr, prepareSpec);
Expand Down
Loading