Skip to content

Instantly share code, notes, and snippets.

@haxiomic
Last active December 2, 2021 16:31
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save haxiomic/34a17c90ffa254cc03f0ccefb91a6e02 to your computer and use it in GitHub Desktop.
Save haxiomic/34a17c90ffa254cc03f0ccefb91a6e02 to your computer and use it in GitHub Desktop.
Super simple unit testing powered by haxe macros
#if macro
import haxe.macro.Context;
import haxe.macro.PositionTools;
import haxe.macro.Expr;
import haxe.macro.ComplexTypeTools;
#end
/**
Pass in a boolean expression, if test fails (expression evaluates to false) the expression itself will be printed with the line number and failure reason
The second argument is information to display if the test fails
Call `testsComplete()` to display report
```haxe
var a = 1;
var b = 2;
test(a == b, "math works as expected"); // prints: test failed, a == b "math works as expected " because 1 != 2
test(a != b, "universe is broken"); // passes
if (!testsComplete()) {
Sys.exit(1); // some tests failed so exit with error code
}
```
**/
macro function test(expr: ExprOf<Bool>, ?details: ExprOf<String>) {
var pos = Context.currentPos();
var isBoolExpr = Context.unify(Context.typeof(expr), ComplexTypeTools.toType(macro :Bool));
if (!isBoolExpr) {
Context.fatalError('Test expression should be a Bool expression', pos);
}
var p = new haxe.macro.Printer('\t');
var exprString = p.printExpr(expr);
var posInfo = PositionTools.toLocation(pos);
final inverseBooleanBinop = [
// ==
OpEq => OpNotEq,
// !=
OpNotEq => OpEq,
// >
OpGt => OpLte,
// >=
OpGte => OpLt,
// <
OpLt => OpGte,
// <=
OpLte => OpGt,
];
var binop = getFinalBinop(expr.expr);
var inverseOp = binop != null ? inverseBooleanBinop[binop.op] : null;
var valuesPrint = if (binop != null && inverseOp != null) {
macro @:privateAccess UnitTestFramework.println(
'\nBecause: \n\n\t' + $e{binop.e1} + ' ' + $v{p.printBinop(inverseOp)} + ' ' + $e{binop.e2} + '\n'
);
} else {
macro null;
}
return macro @:privateAccess if (!${expr}) {
UnitTestFramework.testFailed();
var detail: Null<Any> = ${details};
UnitTestFramework.println(
'Test evaluated to false (' + $v{posInfo.file} + ':' + $v{posInfo.range.start.line} + ')\n\n' +
$v{exprString.split('\n').map(l -> '\t' + l).join('\n')} +
(detail != null ? '\n\n\t"' + detail + '"' : '')
);
$valuesPrint;
} else {
UnitTestFramework.testPassed();
};
}
/**
Call before any tests to track execution time in the report
**/
function testsStart() {
tStart_s = haxe.Timer.stamp();
}
/**
Prints report and returns true if all tests passed
Call `testsStart()` before any tests to track test execution time
**/
function testsComplete(): Bool {
var dt_ms: Null<Float> = if (tStart_s != null) {
var v = (haxe.Timer.stamp() - tStart_s) * 1000;
Math.round(v * 10000) / 10000;
} else null;
var testsTotal = testsPassed + testsFailed;
if (testsTotal == 0) {
println('[${getTargetName()}] No tests were run');
return false;
}
if (testsFailed == 0) {
println('[${getTargetName()}] All tests passed ($testsPassed/$testsTotal)' + (dt_ms != null ? 'in $dt_ms ms' : ''));
return true;
} else {
println('[${getTargetName()}] $testsFailed tests failed ($testsPassed/$testsTotal passed)' + (dt_ms != null ? 'in $dt_ms ms' : ''));
return false;
}
}
var testsPassed = 0;
var testsFailed = 0;
var tStart_s: Null<Float> = null;
private function testPassed() {
testsPassed++;
}
private function testFailed() {
testsFailed++;
}
private function println(str) {
#if sys
Sys.println(str);
#elseif js
js.Browser.console.log(str);
#else
trace(str);
#end
}
private macro function getTargetName() {
return macro $v{Context.definedValue('target.name')};
}
#if macro
private function getFinalBinop(expr) {
return switch expr {
case EBinop(op, e1, e2): { op: op, e1: e1, e2: e2 };
// case EBlock(exprs): getFinalBinop(exprs[exprs.length - 1].expr);
default: null;
}
}
#end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment