浏览代码

add initial display tests

Simon Krajewski 9 年之前
父节点
当前提交
b7354176d5

+ 1 - 0
.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/

+ 4 - 0
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();

+ 4 - 0
tests/display/build.hxml

@@ -0,0 +1,4 @@
+-cp src
+-main Main
+--interp
+-D use-rtti-doc

+ 62 - 0
tests/display/src/DisplayTestCase.hx

@@ -0,0 +1,62 @@
+@:autoBuild(Macro.buildTestCase())
+class DisplayTestCase {
+	var ctx:DisplayTestContext;
+	var methods:Array<Void->Void>;
+	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<String>, actual:Array<String>, ?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
+		}
+	}
+}

+ 118 - 0
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<String>;
+	public var source:String;
+
+	public function new(message:String, fieldName:String, arguments:Array<String>, source:String) {
+		this.message = message;
+		this.fieldName = fieldName;
+		this.arguments = arguments;
+		this.source = source;
+	}
+}
+
+class DisplayTestContext {
+	var source:File;
+	var markers:Map<Int,Int>;
+	var fieldName:String;
+
+	public function new(path:String, fieldName:String, source:String, markers:Map<Int,Int>) {
+		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<String> {
+		return extractPositions(callHaxe('$pos@position'));
+	}
+
+	public function position(pos:Int):String {
+		return positions(pos)[0];
+	}
+
+	public function usage(pos:Int):Array<String> {
+		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;
+	}
+}

+ 62 - 0
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<Int>;
+
+	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));
+	}
+}

+ 52 - 0
tests/display/src/Macro.hx

@@ -0,0 +1,52 @@
+import haxe.macro.Context;
+import haxe.macro.Expr;
+
+class Macro {
+	static function buildTestCase():Array<Field> {
+		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};
+	}
+}

+ 24 - 0
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);
+	}
+}

+ 52 - 0
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)));
+	}
+}