Created
August 10, 2026 03:28
-
-
Save player-03/1f6d2d2c8811d70ab470d070defa8046 to your computer and use it in GitHub Desktop.
UTest error message generator
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
| import haxe.macro.Expr; | |
| import haxe.macro.Printer; | |
| import haxe.PosInfos; | |
| #if !macro | |
| import utest.Assert as UAssert; | |
| import utest.Assertation; | |
| #end | |
| using haxe.macro.Context; | |
| using haxe.macro.ComplexTypeTools; | |
| /** | |
| * A drop-in replacement for `utest.Assert` that generates more detailed error | |
| * messages for `isTrue()` and `isFalse()`. | |
| */ | |
| class Assert { | |
| /** | |
| * @param message If null, defaults to the source code for `cond` followed | |
| * by the runtime values. For instance, `Assert.isTrue(x == y)` would print | |
| * something like "Failure: x == y. Values: 5 == 4". | |
| */ | |
| public static macro function isTrue(condition:ExprOf<Bool>, ?message:ExprOf<String>, ?pos:ExprOf<PosInfos>):Expr { | |
| return switch(message) { | |
| case null, macro null: | |
| addMessage("isTrue", condition, pos); | |
| default: | |
| if(pos.expr.match(EConst(CIdent("null"))) | |
| && message.typeof().unify((macro:haxe.PosInfos).toType())) { | |
| return addMessage("isTrue", condition, message); | |
| } | |
| buildAssertion("isTrue", condition, message, pos); | |
| }; | |
| } | |
| /** | |
| * @param message If null, defaults to the source code for `cond` followed | |
| * by the runtime values. For instance, `Assert.isFalse(x == y)` would print | |
| * something like "Failure: x == y should be false. Values: 4 == 4". | |
| */ | |
| public static macro function isFalse(condition:ExprOf<Bool>, ?message:ExprOf<String>, ?pos:ExprOf<PosInfos>):Expr { | |
| return switch(message) { | |
| case null, macro null: | |
| addMessage("isFalse", condition, pos); | |
| default: | |
| if(pos.expr.match(EConst(CIdent("null"))) | |
| && message.typeof().unify((macro:haxe.PosInfos).toType())) { | |
| return addMessage("isTrue", condition, message); | |
| } | |
| buildAssertion("isFalse", condition, message, pos); | |
| }; | |
| } | |
| #if !macro | |
| public var results(get, set):List<Assertation>; | |
| private inline function get_results():List<Assertation> { | |
| return UAssert.results; | |
| } | |
| private inline function set_results(value:List<Assertation>):List<Assertation> { | |
| return UAssert.results = value; | |
| } | |
| public static inline function isNull<T>(value:Null<T>, ?msg:String, ?pos:PosInfos):Bool { | |
| return UAssert.isNull(value, msg, pos); | |
| } | |
| public static inline function notNull(value:Null<Any>, ?msg:String, ?pos:PosInfos):Bool { | |
| return UAssert.notNull(value, msg, pos); | |
| } | |
| public static inline function isOfType(value:Null<Any>, type:Any, ?msg:String, ?pos:PosInfos):Bool { | |
| return UAssert.isOfType(value, type, msg, pos); | |
| } | |
| public static inline function notEquals<T>(expected:Null<T>, value:Null<T>, ?msg:String, ?pos:PosInfos):Bool { | |
| return UAssert.notEquals(expected, value, msg, pos); | |
| } | |
| public static inline function equals<T>(expected:Null<T>, value:Null<T>, ?msg:String, ?pos:PosInfos):Bool { | |
| return UAssert.equals(expected, value, msg, pos); | |
| } | |
| public static inline function match(pattern:EReg, value:String, ?msg:String, ?pos:PosInfos):Bool { | |
| return UAssert.match(pattern, value, msg, pos); | |
| } | |
| public static inline function floatEquals(expected:Float, value:Float, ?approx:Float, ?msg:String, ?pos:PosInfos):Bool{ | |
| return UAssert.floatEquals(expected, value, approx, msg, pos); | |
| } | |
| public static inline function same(expected:Null<Any>, value:Null<Any>, ?recursive:Bool, ?msg:String, ?approx:Float, ?pos:PosInfos):Bool { | |
| return UAssert.same(expected, value, recursive, msg, approx, pos); | |
| } | |
| public static inline function similar(expected:Null<Any>, value:Null<Any>, recursive:Bool = true, ?msg:String, approx:Float = 1e-5, ?pos:PosInfos):Bool { | |
| return UAssert.similar(expected, value, recursive, msg, approx, pos); | |
| } | |
| public static inline function exception<T>(method:() -> Void, ?type:Class<T>, ?condition:(e:T)->Bool, ?msgNotThrown:String, ?msgWrongType:String, ?msgWrongCondition:String, ?pos:PosInfos):Bool { | |
| return UAssert.exception(method, type, condition, msgNotThrown, msgWrongType, msgWrongCondition, pos); | |
| } | |
| public static inline function allows<T>(possibilities:Array<T>, value:T, ?msg:String, ?pos:PosInfos):Bool { | |
| return UAssert.allows(possibilities, value, msg, pos); | |
| } | |
| public static inline function contains<T>(match:T, values:Array<T>, ?msg:String, ?pos:PosInfos):Bool { | |
| return UAssert.contains(match, values, msg, pos); | |
| } | |
| public static inline function notContains<T>(match:T, values:Array<T>, ?msg:String, ?pos:PosInfos):Bool { | |
| return UAssert.notContains(match, values, msg, pos); | |
| } | |
| public static inline function stringContains(match:String, value:Null<String>, ?msg:String, ?pos:PosInfos):Bool { | |
| return UAssert.stringContains(match, value, msg, pos); | |
| } | |
| public static inline function stringSequence(sequence:Array<String>, value:Null<String>, ?msg:String, ?pos:PosInfos):Bool { | |
| return UAssert.stringSequence(sequence, value, pos); | |
| } | |
| public static inline function pass(msg = "pass expected", ?pos:PosInfos):Bool { | |
| return UAssert.pass(msg, pos); | |
| } | |
| public static inline function fail(msg = "failure expected", ?pos:PosInfos):Bool { | |
| return UAssert.fail(msg, pos); | |
| } | |
| public static inline function warn(msg:String):Void { | |
| UAssert.warn(msg); | |
| } | |
| #else | |
| private static final printer:Printer = new Printer(); | |
| private static function addMessage(assertion:String, condition:Expr, pos:Expr):Expr { | |
| return switch(condition.expr) { | |
| case EBinop(op = OpEq | OpNotEq | OpGt | OpGte | OpLt | OpLte, left, right): | |
| buildBinopAssertion(assertion, condition, op, left, right, pos); | |
| case ECall(func, args): | |
| buildCallAssertion(assertion, condition, func, args, pos); | |
| case EUnop(OpNot, false, condition): | |
| addMessage(assertion == "isTrue" ? "isFalse" : "isTrue", condition, pos); | |
| case EParenthesis(condition): | |
| addMessage(assertion, condition, pos); | |
| default: | |
| buildAssertion(assertion, condition, macro null, pos); | |
| }; | |
| } | |
| private static function buildAssertion(assertion:String, condition:Expr, message:Expr, pos:Expr):Expr { | |
| return switch(pos) { | |
| case null, macro null: | |
| macro @:pos(Context.currentPos()) utest.Assert.$assertion($condition, $message); | |
| default: | |
| macro @:pos(Context.currentPos()) utest.Assert.$assertion($condition, $message, $pos); | |
| }; | |
| } | |
| private static function buildCallAssertion(assertion:String, condition:Expr, func:Expr, args:Array<Expr>, pos:Expr):Expr { | |
| final vars:Array<Var> = []; | |
| final funcValue:Expr = 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 }; | |
| //Display the value of `_utest_this` and the name of `func`. So | |
| //if `condition` is `[a, b].contains(c)`, `funcValue` would be | |
| //`Std.string([a, b]) + ".contains"`. | |
| macro Std.string(_utest_this) + $v{ "." + funcName }; | |
| default: | |
| macro $v{ printer.printExpr(func) }; | |
| }; | |
| final argValues:Array<Expr> = []; | |
| final args:Array<Expr> = [for(i => arg in args) { | |
| if(isCapitalized(arg)) { | |
| argValues.push(macro $v{ printer.printExpr(arg) }); | |
| arg; | |
| } else { | |
| final varName = "_utest_arg_" + i; | |
| vars.push({ name: varName, expr: arg }); | |
| argValues.push(macro Std.string($i{ varName })); | |
| macro $i{ varName }; | |
| } | |
| }]; | |
| final variableDeclarations:Expr = { expr: EVars(vars), pos: condition.pos }; | |
| final shouldBeFalse:String = assertion == "isFalse" ? " should be false" : ""; | |
| return macro @:pos(condition.pos) { | |
| $variableDeclarations; | |
| final _utest_msg = "Failed: " + $v{ printer.printExpr(condition) + shouldBeFalse } + ". " | |
| + "Values: " + $funcValue + "(" + $a{ argValues }.join(", ") + ")"; | |
| ${ buildAssertion(assertion, macro @:pos(func.pos) $func($a{ args }), macro _utest_msg, pos) }; | |
| }; | |
| } | |
| private static function isCapitalized(expr:Expr) { | |
| return switch(expr.expr) { | |
| case EConst(CIdent(ident)), EField(_, ident): | |
| ~/^[A-Z]/.match(ident); | |
| default: | |
| false; | |
| } | |
| } | |
| private static function buildBinopAssertion(assertion:String, condition:Expr, op:Binop, left:Expr, right:Expr, pos:Expr):Expr { | |
| final conditionString:String = printer.printExpr(condition) | |
| + (assertion == "isFalse" ? " should be false" : ""); | |
| final condition:Expr = { | |
| expr: EBinop(op, macro @:pos(left.pos) _utest_left, macro @:pos(right.pos) _utest_right), | |
| pos: condition.pos | |
| }; | |
| return macro @:pos(condition.pos) { | |
| final _utest_left = $left; | |
| final _utest_right = $right; | |
| final _utest_msg:String = "Failed: " + $v{ conditionString } + ". " | |
| + "Values: " + _utest_left + " " + $v{ printer.printBinop(op) } + " " + _utest_right; | |
| ${ buildAssertion(assertion, condition, macro @:pos(condition.pos) _utest_msg, pos) }; | |
| }; | |
| } | |
| #end | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment