Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[tests] Compilation server vs defineModule/defineType #11159

Merged
merged 8 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions tests/server/src/cases/CsSafeTypeBuilding.hx
Original file line number Diff line number Diff line change
@@ -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);
}
}
125 changes: 97 additions & 28 deletions tests/server/src/utils/macro/TestBuilder.macro.hx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Field>):Array<Field> {
var removedFields = [];
var newFields = [];

for (field in fields) {
if (!field.name.startsWith("test")) {
continue;
Expand All @@ -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<Expr>) {
Expand Down
6 changes: 6 additions & 0 deletions tests/server/test/templates/csSafeTypeBuilding/Bar.hx
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions tests/server/test/templates/csSafeTypeBuilding/Baz.hx
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions tests/server/test/templates/csSafeTypeBuilding/Foo.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#if !macro @:genericBuild(Macro.buildFoo()) #end
class Foo<T> {}
63 changes: 63 additions & 0 deletions tests/server/test/templates/csSafeTypeBuilding/Macro.macro.hx
Original file line number Diff line number Diff line change
@@ -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 changes: 9 additions & 0 deletions tests/server/test/templates/csSafeTypeBuilding/Main.hx
Original file line number Diff line number Diff line change
@@ -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");
}