Last active
December 2, 2021 16:31
-
-
Save haxiomic/34a17c90ffa254cc03f0ccefb91a6e02 to your computer and use it in GitHub Desktop.
Super simple unit testing powered by haxe macros
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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