Skip to content

Commit

Permalink
[tests] Compilation server vs defineModule/defineType (#11159)
Browse files Browse the repository at this point in the history
* [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
  • Loading branch information
kLabz authored Jan 3, 2024
1 parent c9e9459 commit f8d1caf
Show file tree
Hide file tree
Showing 7 changed files with 333 additions and 28 deletions.
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");
}

0 comments on commit f8d1caf

Please sign in to comment.