فهرست منبع

[tests] Compilation server vs defineModule/defineType (#11159)

* [tests] server tests - basic support for @:variant

* [tests] add server test for compilation server safe type building

* [tests] server tests - error handling for @:variant

* [tests] add debug info

* [tests] temp change of build params

* [tests] add more debug data

* [tests] better debug data on failures

* [tests] reenable other server tests
Rudy Ges 1 سال پیش
والد
کامیت
f8d1caf769

+ 149 - 0
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<String>, ?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);
+	}
+}

+ 97 - 28
tests/server/src/utils/macro/TestBuilder.macro.hx

@@ -2,11 +2,15 @@ package utils.macro;
 
 
 import haxe.macro.Expr;
 import haxe.macro.Expr;
 import haxe.macro.Context;
 import haxe.macro.Context;
+import haxe.macro.Type;
 
 
 using StringTools;
 using StringTools;
 
 
 class TestBuilder {
 class TestBuilder {
 	static public function build(fields:Array<Field>):Array<Field> {
 	static public function build(fields:Array<Field>):Array<Field> {
+		var removedFields = [];
+		var newFields = [];
+
 		for (field in fields) {
 		for (field in fields) {
 			if (!field.name.startsWith("test")) {
 			if (!field.name.startsWith("test")) {
 				continue;
 				continue;
@@ -16,40 +20,105 @@ class TestBuilder {
 					// Async is already manually handled, nothing to do
 					// Async is already manually handled, nothing to do
 
 
 				case FFun(f):
 				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 _:
 				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<Expr>) {
 	static function transformHaxeCalls(asyncName:String, el:Array<Expr>) {

+ 6 - 0
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<Bar>;

+ 7 - 0
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<Bar>;
+typedef BB = Foo<Baz>;

+ 2 - 0
tests/server/test/templates/csSafeTypeBuilding/Foo.hx

@@ -0,0 +1,2 @@
+#if !macro @:genericBuild(Macro.buildFoo()) #end
+class Foo<T> {}

+ 63 - 0
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<String, Bool>();
+
+	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 "";
+		}
+	}
+}

+ 9 - 0
tests/server/test/templates/csSafeTypeBuilding/Main.hx

@@ -0,0 +1,9 @@
+// Create a dependency to Bar
+import Bar;
+
+typedef A = Foo<Main>;
+
+#if !macro @:build(Macro.logBuild()) #end
+class Main {
+	static function main() Sys.println("[runtime] Hello from Main");
+}