diff --git a/.gitignore b/.gitignore index 7eb0449beb9..660c9854e7c 100644 --- a/.gitignore +++ b/.gitignore @@ -87,3 +87,4 @@ tests/misc/projects/Issue4070/cpp/ /tests/misc/eventLoop/dump /tests/misc/eventLoop/eventLoop.py /tests/misc/eventLoop/php +tests/display/.vscode/ diff --git a/tests/RunCi.hx b/tests/RunCi.hx index 22c078c9202..6204b68e4b4 100644 --- a/tests/RunCi.hx +++ b/tests/RunCi.hx @@ -597,6 +597,7 @@ class RunCi { static var sysDir(default, never) = cwd + "sys/"; static var optDir(default, never) = cwd + "optimization/"; static var miscDir(default, never) = cwd + "misc/"; + static var displayDir(default, never) = cwd + "display/"; static var gitInfo(get, null):{repo:String, branch:String, commit:String, timestamp:Float, date:String}; static function get_gitInfo() return if (gitInfo != null) gitInfo else gitInfo = { repo: switch (ci) { @@ -915,6 +916,9 @@ class RunCi { case Macro: runCommand("haxe", ["compile-macro.hxml"].concat(args)); + changeDirectory(displayDir); + runCommand("haxe", ["build.hxml"]); + changeDirectory(miscDir); getCsDependencies(); getPythonDependencies(); diff --git a/tests/display/build.hxml b/tests/display/build.hxml new file mode 100644 index 00000000000..75e729791bd --- /dev/null +++ b/tests/display/build.hxml @@ -0,0 +1,4 @@ +-cp src +-main Main +--interp +-D use-rtti-doc \ No newline at end of file diff --git a/tests/display/src/DisplayTestCase.hx b/tests/display/src/DisplayTestCase.hx new file mode 100644 index 00000000000..4f5444a2ca0 --- /dev/null +++ b/tests/display/src/DisplayTestCase.hx @@ -0,0 +1,62 @@ +@:autoBuild(Macro.buildTestCase()) +class DisplayTestCase { + var ctx:DisplayTestContext; + var methods:ArrayVoid>; + var numTests:Int; + var numFailures:Int; + var testName:String; + + // api + inline function pos(name) return ctx.pos(name); + inline function field(pos) return ctx.field(pos); + inline function toplevel(pos) return ctx.toplevel(pos); + inline function type(pos) return ctx.type(pos); + inline function position(pos) return ctx.position(pos); + inline function usage(pos) return ctx.usage(pos); + inline function range(pos1, pos2) return ctx.range(pos1, pos2); + + function assert(v:Bool) if (!v) throw "assertion failed"; + + function eq(expected:String, actual:String, ?pos:haxe.PosInfos) { + numTests++; + if (expected != actual) { + numFailures++; + report("Assertion failed", pos); + report("Expected: " + expected, pos); + report("Actual: " + actual, pos); + } + } + + function arrayEq(expected:Array, actual:Array, ?pos:haxe.PosInfos) { + numTests++; + var leftover = expected.copy(); + for (actual in actual) { + if (!leftover.remove(actual)) { + numFailures++; + report("Result not part of expected Array:", pos); + report(actual, pos); + } + } + for (leftover in leftover) { + numFailures++; + report("Expected result was not part of actual Array:", pos); + report(leftover, pos); + return; + } + } + + function report(message, pos:haxe.PosInfos) { + haxe.Log.trace(message, pos); + } + + public function run() { + for (method in methods) { + method(); + } + return { + testName: testName, + numTests: numTests, + numFailures: numFailures + } + } +} \ No newline at end of file diff --git a/tests/display/src/DisplayTestContext.hx b/tests/display/src/DisplayTestContext.hx new file mode 100644 index 00000000000..f2a889a4da7 --- /dev/null +++ b/tests/display/src/DisplayTestContext.hx @@ -0,0 +1,118 @@ +using StringTools; + +class HaxeInvocationException { + + public var message:String; + public var fieldName:String; + public var arguments:Array; + public var source:String; + + public function new(message:String, fieldName:String, arguments:Array, source:String) { + this.message = message; + this.fieldName = fieldName; + this.arguments = arguments; + this.source = source; + } +} + +class DisplayTestContext { + var source:File; + var markers:Map; + var fieldName:String; + + public function new(path:String, fieldName:String, source:String, markers:Map) { + this.fieldName = fieldName; + this.source = new File(path, source); + this.markers = markers; + } + + public function pos(id:Int):Int { + var r = markers[id]; + if (r == null) throw "No such marker: " + id; + return r; + } + + public function range(pos1:Int, pos2:Int) { + return normalizePath(source.formatPosition(pos(pos1), pos(pos2))); + } + + public function field(pos:Int):String { + return callHaxe('$pos'); + } + + public function toplevel(pos:Int):String { + return callHaxe('$pos'); + } + + public function type(pos:Int):String { + return extractType(callHaxe('$pos@type')); + } + + public function positions(pos:Int):Array { + return extractPositions(callHaxe('$pos@position')); + } + + public function position(pos:Int):String { + return positions(pos)[0]; + } + + public function usage(pos:Int):Array { + return extractPositions(callHaxe('$pos@usage')); + } + + function callHaxe(displayPart:String):String { + var args = [ + "-cp", "src", + "-D", "display-stdin", + "--display", + source.path + "@" + displayPart, + ]; + var stdin = source.content; + var proc = new sys.io.Process("haxe", args); + proc.stdin.writeString(stdin); + proc.stdin.close(); + var stdout = proc.stdout.readAll(); + var stderr = proc.stderr.readAll(); + var exit = proc.exitCode(); + var success = exit == 0; + var s = stderr.toString(); + if (!success || s == "") { + throw new HaxeInvocationException(s, fieldName, args, stdin); + } + return s; + } + + static function extractType(result:String) { + var xml = Xml.parse(result); + xml = xml.firstElement(); + if (xml.nodeName != "type") { + return null; + } + return StringTools.trim(xml.firstChild().nodeValue); + } + + static function extractPositions(result:String) { + var xml = Xml.parse(result); + xml = xml.firstElement(); + if (xml.nodeName != "list") { + return null; + } + var ret = []; + for (xml in xml.elementsNamed("pos")) { + ret.push(normalizePath(xml.firstChild().nodeValue.trim())); + } + return ret; + } + + static function normalizePath(p:String):String { + if (!haxe.io.Path.isAbsolute(p)) { + p = Sys.getCwd() + p; + } + if (Sys.systemName() == "Windows") { + // on windows, haxe returns lowercase paths with backslashes + p = p.replace("/", "\\"); + p = p.toLowerCase(); + } + return p; + } +} diff --git a/tests/display/src/File.hx b/tests/display/src/File.hx new file mode 100644 index 00000000000..676284d0ea2 --- /dev/null +++ b/tests/display/src/File.hx @@ -0,0 +1,62 @@ +class File { + public var content(default,null):String; + public var path(default,null):String; + var lines:Array; + + public function new(path:String, content:String) { + this.path = path; + this.content = content; + initLines(); + } + + function initLines() { + lines = []; + // составляем массив позиций начала строк + var s = 0, p = 0; + while (p < content.length) { + inline function nextChar() return StringTools.fastCodeAt(content, p++); + inline function line() { lines.push(s); s = p; }; + switch (nextChar()) { + case "\n".code: + line(); + case "\r".code: + p++; + line(); + } + } + } + + function findLine(pos:Int):{line:Int, pos:Int} { + function loop(min, max) { + var mid = (min + max) >> 1; + var start = lines[mid]; + return + if (mid == min) + {line: mid, pos: pos - start}; + else if (start > pos) + loop(min, mid); + else + loop(mid, max); + } + return loop(0, lines.length); + } + + public function formatPosition(min:Int, max:Int):String { + var start = findLine(min); + var end = findLine(max); + var pos = + if (start.line == end.line) { + if (start.pos == end.pos) + 'character ${start.pos}'; + else + 'characters ${start.pos}-${end.pos}'; + } else { + 'lines ${start.line + 1}-${end.line + 1}'; + } + return '$path:${start.line + 1}: $pos'; + } + + public static inline function read(path:String) { + return new File(path, sys.io.File.getContent(path)); + } +} \ No newline at end of file diff --git a/tests/display/src/Macro.hx b/tests/display/src/Macro.hx new file mode 100644 index 00000000000..dc6fe3d8d8e --- /dev/null +++ b/tests/display/src/Macro.hx @@ -0,0 +1,52 @@ +import haxe.macro.Context; +import haxe.macro.Expr; + +class Macro { + static function buildTestCase():Array { + var fields = Context.getBuildFields(); + var markerRe = ~/{-(\d+)-}/g; + var testCases = []; + var c = Context.getLocalClass().get(); + for (field in fields) { + var markers = []; + var posAcc = 0; + var doc = (c.pack.length > 0 ? "package " + c.pack.join(".") + ";\n" : "") + field.doc; + var src = markerRe.map(doc, function(r) { + var p = r.matchedPos(); + var name = r.matched(1); + var pos = p.pos - posAcc; + posAcc += p.len; + markers.push(macro $v{Std.parseInt(name)} => $v{pos}); + return ""; + }); + testCases.push(macro function() { + ctx = new DisplayTestContext($v{Context.getPosInfos(c.pos).file}, $v{field.name}, $v{src}, $a{markers}); + $i{field.name}(); + }); + } + + fields.push((macro class { + public function new() { + testName = $v{c.name}; + numTests = 0; + numFailures = 0; + this.methods = $a{testCases}; + } + }).fields[0]); + + return fields; + } + + macro static public function getCases(pack:String) { + var path = Context.resolvePath(pack); + var cases = []; + for (file in sys.FileSystem.readDirectory(path)) { + var p = new haxe.io.Path(file); + if (p.ext == "hx") { + var tp = {pack: [pack], name: p.file}; + cases.push(macro new $tp()); + } + } + return macro $a{cases}; + } +} diff --git a/tests/display/src/Main.hx b/tests/display/src/Main.hx new file mode 100644 index 00000000000..c7ae3453dbf --- /dev/null +++ b/tests/display/src/Main.hx @@ -0,0 +1,24 @@ +class Main { + static function main() { + var tests = Macro.getCases("cases"); + var numTests = 0; + var numFailures = 0; + for (test in tests) { + try { + var result = test.run(); + numTests += result.numTests; + numFailures += result.numFailures; + Sys.println('${result.testName}: ${result.numTests} tests, ${result.numFailures} failures'); + } catch(e:DisplayTestContext.HaxeInvocationException) { + Sys.println("Error: " + e.message); + Sys.println("Field name: " + e.fieldName); + Sys.println("Arguments: " + e.arguments.join(" ")); + Sys.println("Source: " + e.source); + numTests++; + numFailures++; + } + } + Sys.println('Finished with $numTests tests, $numFailures failures'); + Sys.exit(numFailures == 0 ? 0 : 1); + } +} diff --git a/tests/display/src/cases/Basic.hx b/tests/display/src/cases/Basic.hx new file mode 100644 index 00000000000..1052d9978ba --- /dev/null +++ b/tests/display/src/cases/Basic.hx @@ -0,0 +1,52 @@ +package cases; + +class Basic extends DisplayTestCase { + /** + class Some { + function main() { + var a = 5; + a{-1-} + } + } + **/ + function testType1() { + eq("Int", type(pos(1))); + } + + /** + class Some { + function main() { + var {-1-}variable{-2-} = 5; + variable{-3-}; + } + } + **/ + function testPosition1() { + eq(range(1, 2), position(pos(3))); + } + + /** + class Some { + function main() { + var variable{-1-} = 5; + {-2-}variable{-3-}; + } + } + **/ + function testUsage1() { + eq(range(2, 3), usage(pos(1))[0]); + } + + /** + class Some { + function main() { + var variable{-1-} = 5; + {-2-}variable{-3-}; + {-4-}variable{-5-}; + } + } + **/ + function testUsage2() { + arrayEq([range(2, 3), range(4, 5)], usage(pos(1))); + } +}