diff --git a/tests/server/src/cases/CsSafeTypeBuilding.hx b/tests/server/src/cases/CsSafeTypeBuilding.hx new file mode 100644 index 00000000000..f8e15f96642 --- /dev/null +++ b/tests/server/src/cases/CsSafeTypeBuilding.hx @@ -0,0 +1,149 @@ +package cases; + +import haxe.display.Display; +import haxe.display.FsPath; +import haxe.display.Server; +import utest.Assert; + +using StringTools; +using Lambda; + +class CsSafeTypeBuilding extends TestCase { + var originalContent:String; + + override public function setup(async:utest.Async) { + super.setup(async); + + originalContent = ""; + vfs.putContent("Bar.hx", getTemplate("csSafeTypeBuilding/Bar.hx")); + vfs.putContent("Baz.hx", getTemplate("csSafeTypeBuilding/Baz.hx")); + vfs.putContent("Foo.hx", getTemplate("csSafeTypeBuilding/Foo.hx")); + vfs.putContent("Macro.macro.hx", getTemplate("csSafeTypeBuilding/Macro.macro.hx")); + vfs.putContent("Main.hx", getTemplate("csSafeTypeBuilding/Main.hx")); + } + + #if debug + var failed:Bool; + function _assertHasPrint(s:String, ?pos:haxe.PosInfos) { + if (!assertHasPrint(s)) { + failed = true; + haxe.Log.trace("Fail: doesn't contain \"" + s + "\"", pos); + } + } + #end + + function assertResult(target:String) { + #if debug + failed = false; + var assertHasPrint = _assertHasPrint; + #end + assertSuccess(); + + // Make sure all types are generated + assertHasPrint("[runtime] Hello from Bar"); + assertHasPrint("[runtime] Hello from Baz"); + assertHasPrint("[runtime] Hello from Foo__Bar__Bar"); + assertHasPrint("[runtime] Hello from Foo__Baz__Baz"); + assertHasPrint("[runtime] Hello from Foo__Main__Main"); + assertHasPrint("[runtime] Hello from Main"); + + #if debug + if (failed) messages.filter(m -> StringTools.startsWith(m, "Haxe print: ")).iter(m -> trace(m)); + #end + + // Disabled this check because types move around a bit so we get false negatives + // Kept for debugging purposes + if (false && target == "js") { + var content = sys.io.File.getContent(haxe.io.Path.join([testDir, "out.js"])); + Assert.isTrue(content == originalContent); + + // Needs https://github.com/kLabz/hxdiff for displaying diff + // if (content != originalContent) { + // final a = new diff.FileData(haxe.io.Bytes.ofString(originalContent), "expected", Date.now()); + // final b = new diff.FileData(haxe.io.Bytes.ofString(content), "actual", Date.now()); + // var ctx:diff.Context = { + // file1: a, + // file2: b, + // context: 10 + // } + + // final script = diff.Analyze.diff2Files(ctx); + // var diff = diff.Printer.printUnidiff(ctx, script); + // Sys.println(diff); + // } + } + } + + function assertBuilt(modules:Array, ?macroInvalidated:Bool = false) { + #if debug trace('Invalidated ${modules.join(",")} (macro invalidated: ${macroInvalidated ? "true" : "false"})'); #end + #if debug var assertHasPrint = _assertHasPrint; #end + + for (m in modules) { + assertHasPrint('Building $m.'); + + var t = 'Foo__${m}__${m}'; + if (!macroInvalidated) assertHasPrint('[$m] Previously generated type for $t has been discarded.'); + assertHasPrint('[$m] Generating type for $t.'); + + if (m == "Baz") { + assertHasPrint('[$m] Reusing previously generated type for Foo__Bar__Bar.'); + } + } + } + + @:variant("JsDefineModule", true, "js") + @:variant("JsDefineType", false, "js") + @:variant("InterpDefineModule", true, "interp") + @:variant("InterpDefineType", false, "interp") + function test(defineModule:Bool, target:String) { + var targetArgs = switch target { + case "js": ["-js", "out.js", "-lib", "hxnodejs", "-cmd", "node out.js"]; + case "interp": ["--interp"]; + case _: []; + } + + var args = ["-main", "Main", "Baz"]; + if (defineModule) args = args.concat(["-D", "config.defineModule"]); + args = args.concat(targetArgs); + + runHaxe(args); + if (target == "js") originalContent = sys.io.File.getContent(haxe.io.Path.join([testDir, "out.js"])); + assertBuilt(["Main", "Bar", "Baz"], true); + assertResult(target); + + #if debug trace("Rerun without invalidate"); #end + runHaxe(args); + assertResult(target); + + runHaxeJson([], ServerMethods.Invalidate, {file: new FsPath("Baz.hx")}); + runHaxe(args); + assertBuilt(["Baz"]); + assertResult(target); + + runHaxeJson([], ServerMethods.Invalidate, {file: new FsPath("Main.hx")}); + runHaxe(args); + assertBuilt(["Main"]); + assertResult(target); + + runHaxeJson([], ServerMethods.Invalidate, {file: new FsPath("Bar.hx")}); + runHaxeJson([], ServerMethods.Invalidate, {file: new FsPath("Main.hx")}); + runHaxe(args); + assertBuilt(["Main", "Bar"]); + assertResult(target); + + runHaxeJson([], ServerMethods.Invalidate, {file: new FsPath("Bar.hx")}); + runHaxe(args); + assertBuilt(["Main", "Bar", "Baz"]); + assertResult(target); + + runHaxeJson([], ServerMethods.Invalidate, {file: new FsPath("Foo.hx")}); + runHaxe(args); + assertBuilt(["Main", "Bar", "Baz"]); + assertResult(target); + + runHaxeJson([], ServerMethods.Invalidate, {file: new FsPath("Macro.macro.hx")}); + runHaxe(args); + assertBuilt(["Main", "Bar", "Baz"], true); + assertResult(target); + } +} diff --git a/tests/server/src/utils/macro/TestBuilder.macro.hx b/tests/server/src/utils/macro/TestBuilder.macro.hx index 1cfab11dea2..d28724d2179 100644 --- a/tests/server/src/utils/macro/TestBuilder.macro.hx +++ b/tests/server/src/utils/macro/TestBuilder.macro.hx @@ -2,11 +2,15 @@ package utils.macro; import haxe.macro.Expr; import haxe.macro.Context; +import haxe.macro.Type; using StringTools; class TestBuilder { static public function build(fields:Array):Array { + var removedFields = []; + var newFields = []; + for (field in fields) { if (!field.name.startsWith("test")) { continue; @@ -16,40 +20,105 @@ class TestBuilder { // Async is already manually handled, nothing to do case FFun(f): - var asyncName = switch f.args { - case []: - var name = "async"; - f.args.push({ - name: name, - type: macro:utest.Async - }); - name; - case [arg]: - if (arg.name == "_") { - arg.name = "async"; - arg.type = macro:utest.Async; + var variants = field.meta.filter(m -> m.name == ":variant"); + if (variants.length == 0) { + makeAsyncTest(f, field.pos); + } else { + // TODO: support functions that define their own async arg (not named `_` or `async`) + var args = f.args.copy(); + f.args = []; + makeAsyncTest(f, field.pos); + + // Ignore original field; generate variants instead + removedFields.push(field); + + for (variant in variants) { + if (variant.params.length == 0) { + Context.error('Unexpected amount of variant parameters.', variant.pos); } - arg.name; - case _: - Context.fatalError('Unexpected amount of test arguments', field.pos); - ""; - } - switch (f.expr.expr) { - case EBlock(el): - var posInfos = Context.getPosInfos(f.expr.pos); - var pos = Context.makePosition({min: posInfos.max, max: posInfos.max, file: posInfos.file}); - el.push(macro @:pos(pos) $i{asyncName}.done()); - f.expr = macro { - $i{asyncName}.setTimeout(20000); - ${transformHaxeCalls(asyncName, el)}; + + var nameParam = variant.params.shift(); + var name:String = try haxe.macro.ExprTools.getValue(nameParam) catch(e) { + Context.error('Variant first parameter should be a String (variant name)', nameParam.pos); + }; + + var inits = [for (arg in args) { + var name = arg.name; + var ct = arg.type; + + if (variant.params.length == 0) { + Context.error('Unexpected amount of variant parameters.', variant.pos); + } + + var param = variant.params.shift(); + macro @:pos(param.pos) var $name:$ct = (($name:$ct) -> $i{name})(${param}); + }]; + + if (variant.params.length > 0) { + Context.error('Unexpected amount of variant parameters.', variant.params[0].pos); } - case _: - Context.error("Block expression expected", f.expr.pos); + + switch (f.expr.expr) { + case EBlock(b): + var ff = { + ret: f.ret, + params: f.params, + expr: {pos: variant.pos, expr: EBlock(inits.concat(b))}, + args: [{name: "async", type: macro:utest.Async}] + }; + + newFields.push({ + pos: variant.pos, + name: field.name + name, + meta: field.meta.filter(m -> m.name != ":variant"), + kind: FFun(ff), + doc: field.doc, + access : field.access + }); + + case _: + } + } } case _: } } - return fields; + + for (f in removedFields) fields.remove(f); + return fields.concat(newFields); + } + + static function makeAsyncTest(f:Function, fpos:Position) { + var asyncName = switch f.args { + case []: + var name = "async"; + f.args.push({ + name: name, + type: macro:utest.Async + }); + name; + case [arg]: + if (arg.name == "_") { + arg.name = "async"; + arg.type = macro:utest.Async; + } + arg.name; + case _: + Context.fatalError('Unexpected amount of test arguments', fpos); + ""; + } + switch (f.expr.expr) { + case EBlock(el): + var posInfos = Context.getPosInfos(f.expr.pos); + var pos = Context.makePosition({min: posInfos.max, max: posInfos.max, file: posInfos.file}); + el.push(macro @:pos(pos) $i{asyncName}.done()); + f.expr = macro { + $i{asyncName}.setTimeout(20000); + ${transformHaxeCalls(asyncName, el)}; + } + case _: + Context.error("Block expression expected", f.expr.pos); + } } static function transformHaxeCalls(asyncName:String, el:Array) { diff --git a/tests/server/test/templates/csSafeTypeBuilding/Bar.hx b/tests/server/test/templates/csSafeTypeBuilding/Bar.hx new file mode 100644 index 00000000000..19434fb20b5 --- /dev/null +++ b/tests/server/test/templates/csSafeTypeBuilding/Bar.hx @@ -0,0 +1,6 @@ +#if !macro @:build(Macro.logBuild()) #end +class Bar { + static function __init__() Sys.println("[runtime] Hello from Bar"); +} + +typedef B = Foo; diff --git a/tests/server/test/templates/csSafeTypeBuilding/Baz.hx b/tests/server/test/templates/csSafeTypeBuilding/Baz.hx new file mode 100644 index 00000000000..574b0c41fa7 --- /dev/null +++ b/tests/server/test/templates/csSafeTypeBuilding/Baz.hx @@ -0,0 +1,7 @@ +#if !macro @:build(Macro.logBuild()) #end +class Baz { + static function __init__() Sys.println("[runtime] Hello from Baz"); +} + +typedef AA = Foo; +typedef BB = Foo; diff --git a/tests/server/test/templates/csSafeTypeBuilding/Foo.hx b/tests/server/test/templates/csSafeTypeBuilding/Foo.hx new file mode 100644 index 00000000000..64cac29e64b --- /dev/null +++ b/tests/server/test/templates/csSafeTypeBuilding/Foo.hx @@ -0,0 +1,2 @@ +#if !macro @:genericBuild(Macro.buildFoo()) #end +class Foo {} diff --git a/tests/server/test/templates/csSafeTypeBuilding/Macro.macro.hx b/tests/server/test/templates/csSafeTypeBuilding/Macro.macro.hx new file mode 100644 index 00000000000..b8a607ab6e1 --- /dev/null +++ b/tests/server/test/templates/csSafeTypeBuilding/Macro.macro.hx @@ -0,0 +1,63 @@ +import haxe.macro.Context; +import haxe.macro.Expr; +import haxe.macro.Type; +import haxe.macro.TypeTools; + +class Macro { + public static function logBuild() { + Sys.println('Building ${Context.getLocalClass().toString()}.'); + return null; + } + + @:persistent static var generated = new Map(); + + static function isAlive(ct:ComplexType, pos:Position):Bool { + // Null check is just there to make it a one liner + // Basically returning true if no exception is caught + return try Context.resolveType(ct, pos) != null catch(e) false; + } + + public static function buildFoo() { + var from = '[${Context.getLocalModule()}] '; + var print = s -> Sys.println(from + s); + + switch (Context.getLocalType()) { + case TInst(_, [target]): + var pos = Context.currentPos(); + var bt = TypeTools.toBaseType(target); + var key = ["Foo", bt.module, bt.name].join("__"); + var ct = TPath({pack: [], name: key}); + + if (generated.exists(key)) { + if (isAlive(ct, pos)) { + print('Reusing previously generated type for $key.'); + return ct; + } + + print('Previously generated type for $key has been discarded.'); + } + + var genDef = macro class $key { + static function __init__() Sys.println("[runtime] Hello from " + $v{key}); + }; + + // Not really needed but nicer + // genDef.pos = pos; + + // Not needed unless dce full + // genDef.meta.push({name: ":keep", params: [], pos: pos}); + + print('Generating type for $key.'); + #if config.defineModule + Context.defineModule(key, [genDef]); + #else + Context.defineType(genDef, bt.module); + #end + + generated.set(key, true); + return ct; + + case _: throw ""; + } + } +} diff --git a/tests/server/test/templates/csSafeTypeBuilding/Main.hx b/tests/server/test/templates/csSafeTypeBuilding/Main.hx new file mode 100644 index 00000000000..108036891c1 --- /dev/null +++ b/tests/server/test/templates/csSafeTypeBuilding/Main.hx @@ -0,0 +1,9 @@ +// Create a dependency to Bar +import Bar; + +typedef A = Foo
; + +#if !macro @:build(Macro.logBuild()) #end +class Main { + static function main() Sys.println("[runtime] Hello from Main"); +}