소스 검색

[CI] Run display tests with both xml and json rpc api (#11506)

* [tests] prepare for json rpc vs xml display tests

* [tests] add display test for static field completion

Also run again a couple tests after caching type

* [tests] display test with both xml and json rpc api

* [ci] run both xml and jsonrpc display tests

* [display] send completion error when no results

(same as xml display api)

* [tests] don't rely on result.result == null on completion error

* [tests] only run Toplevel.testDuplicates for xml display
Rudy Ges 1 년 전
부모
커밋
be88d42387

+ 4 - 1
src/compiler/displayOutput.ml

@@ -326,7 +326,10 @@ let handle_display_exception_json ctx dex api =
 		let ctx = DisplayJson.create_json_context api.jsonrpc (match dex with DisplayFields _ -> true | _ -> false) in
 		api.send_result (DisplayException.to_json ctx dex)
 	| DisplayNoResult ->
-		api.send_result JNull
+		(match ctx.com.display.dms_kind with
+			| DMDefault -> api.send_error [jstring "No completion point"]
+			| _ -> api.send_result JNull
+		)
 	| _ ->
 		handle_display_exception_old ctx dex
 

+ 0 - 1
std/haxe/display/Display.hx

@@ -98,7 +98,6 @@ class DisplayMethods {
 		TODO:
 
 		- finish completion
-		- diagnostics
 		- codeLens
 		- workspaceSymbols ("project/symbol"?)
 	 */

+ 56 - 0
tests/display/src/BaseDisplayTestContext.hx

@@ -0,0 +1,56 @@
+import haxe.io.Bytes;
+
+using StringTools;
+
+import Types;
+
+class BaseDisplayTestContext {
+	static var haxeServer = haxeserver.HaxeServerSync.launch("haxe", []);
+
+	var markers:Map<Int, Int>;
+	var fieldName:String;
+
+	public final source:File;
+
+	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):Position {
+		var r = markers[id];
+		if (r == null)
+			throw "No such marker: " + id;
+		return new Position(r);
+	}
+
+	public function range(pos1:Int, pos2:Int) {
+		return normalizePath(source.formatRange(pos(pos1), pos(pos2)));
+	}
+
+	public function hasErrorMessage(f:()->Void, message:String) {
+		return try {
+			f();
+			false;
+		} catch (exc:HaxeInvocationException) {
+			return exc.message.indexOf(message) != -1;
+		}
+	}
+
+	static public function runHaxe(args:Array<String>, ?stdin:String) {
+		return haxeServer.rawRequest(args, stdin == null ? null : Bytes.ofString(stdin));
+	}
+
+	static function normalizePath(p:String):String {
+		if (!haxe.io.Path.isAbsolute(p)) {
+			p = Sys.getCwd() + p;
+		}
+		if (Sys.systemName() == "Windows") {
+			// on windows, haxe returns paths with backslashes, drive letter uppercased
+			p = p.substr(0, 1).toUpperCase() + p.substr(1);
+			p = p.replace("/", "\\");
+		}
+		return p;
+	}
+}

+ 159 - 0
tests/display/src/DisplayPrinter.hx

@@ -0,0 +1,159 @@
+import haxe.display.Display;
+import haxe.display.JsonModuleTypes;
+
+using Lambda;
+
+class DisplayPrinter {
+	var indent = "";
+	public function new() {}
+
+	public function printPath(path:JsonTypePath) {
+		final qualified = !(path.moduleName == "StdTypes" && path.pack.length == 0);
+		final isSubType = path.moduleName != path.typeName;
+		final isToplevelType = path.pack.length == 0 && !isSubType;
+
+		if (isToplevelType && path.importStatus == Shadowed) {
+			path.pack.push("std");
+		}
+
+		function printFullPath() {
+			var printedPath = if (isSubType) path.typeName else path.moduleName;
+			if (path.pack.length > 0) {
+				printedPath = path.pack.join(".") + "." + printedPath;
+			}
+			return printedPath;
+		}
+
+		return if (qualified) printFullPath() else path.typeName;
+	}
+
+	public function printPathWithParams(path:JsonTypePathWithParams) {
+		final s = printPath(path.path);
+		if (path.params.length == 0) {
+			return s;
+		} else {
+			var sparams = path.params.map(printType).join(", ");
+			return '$s<$sparams>';
+		}
+	}
+
+	public function printType<T>(t:JsonType<T>) {
+		return switch t.kind {
+			case TMono: "Unknown<0>";
+			case TInst | TEnum | TType | TAbstract: printPathWithParams(t.args);
+			case TDynamic:
+				if (t.args == null) {
+					"Dynamic";
+				} else {
+					final s = printTypeRec(t.args);
+					'Dynamic<$s>';
+				}
+			case TAnonymous:
+				final fields = t.args.fields;
+				final s = [
+					for (field in fields) {
+						var prefix = if (hasMeta(field.meta, ":optional")) "?" else "";
+						'$prefix${field.name} : ${printTypeRec(field.type)}';
+					}
+				].join(", ");
+				s == '' ? '{ }' : '{ $s }';
+			case TFun:
+				var hasNamed = false;
+				function printFunctionArgument(arg:JsonFunctionArgument) {
+					if (arg.name != "") {
+						hasNamed = true;
+					}
+					return this.printFunctionArgument(arg);
+				}
+				final args = t.args.args.map(printFunctionArgument);
+				var r = printTypeRec(t.args.ret);
+				if (t.args.ret.kind == TFun) r = '($r)';
+				switch args.length {
+					case 0: '() -> $r';
+					case 1 if (hasNamed): '(${args[0]}) -> $r';
+					case 1: '${args[0]} -> $r';
+					case _: '(${args.join(", ")}) -> $r';
+				}
+		}
+	}
+
+	function printTypeRec<T>(t:JsonType<T>) {
+		final old = indent;
+		indent += "  ";
+		final t = printType(t);
+		indent = old;
+		return t;
+	}
+
+	public function printFunctionArgument<T>(arg:JsonFunctionArgument):String {
+		final nullRemoval = removeNulls(arg.t);
+		final concreteType = if (!arg.opt) arg.t else nullRemoval.type;
+
+		var argument = (if (arg.opt && arg.value == null) "?" else "") + arg.name;
+		if (concreteType.kind != TMono || arg.name == "") {
+			var hint = printTypeRec(concreteType);
+			if (concreteType.kind == TFun) hint = '($hint)';
+			argument += (arg.name == "" ? "" : " : ") + hint;
+		}
+		if (arg.value != null) {
+			argument += " = " + arg.value.string;
+		}
+		return argument;
+	}
+
+	public function printSignatureFunctionArgument<T>(arg:JsonFunctionArgument):String {
+		final nullRemoval = removeNulls(arg.t);
+		final concreteType = if (!arg.opt) arg.t else nullRemoval.type;
+
+		var argument = (if (arg.opt && arg.value == null) "?" else "") + arg.name;
+		var hint = printTypeRec(concreteType);
+		if (concreteType.kind == TFun) hint = '($hint)';
+		argument += ":" + hint;
+		if (arg.value != null) {
+			argument += " = " + arg.value.string;
+		}
+		return argument;
+	}
+
+	public function printCallArguments<T>(signature:JsonFunctionSignature, printFunctionArgument:JsonFunctionArgument->String) {
+		return "(" + signature.args.map(printFunctionArgument).join(", ") + ")";
+	}
+
+	function removeNulls<T>(type:JsonType<T>, nullable:Bool = false):{type:JsonType<T>, nullable:Bool} {
+		switch type.kind {
+			case TAbstract:
+				final path:JsonTypePathWithParams = type.args;
+				if (getDotPath(type) == "StdTypes.Null") {
+					if (path.params != null && path.params[0] != null) {
+						return removeNulls(path.params[0], true);
+					}
+				}
+			case _:
+		}
+		return {type: type, nullable: nullable};
+	}
+
+	inline function isVoid<T>(type:JsonType<T>) {
+		return getDotPath(type) == "StdTypes.Void";
+	}
+
+	function getDotPath<T>(type:JsonType<T>):Null<String> {
+		final path = getTypePath(type);
+		if (path == null) {
+			return null;
+		}
+		return printPath(path.path);
+	}
+
+	function getTypePath<T>(type:JsonType<T>):Null<JsonTypePathWithParams> {
+		return switch type.kind {
+			case null: null;
+			case TInst | TEnum | TType | TAbstract: type.args;
+			case _: null;
+		}
+	}
+
+	function hasMeta(?meta:JsonMetadata, name:String) {
+		return meta != null && meta.exists(meta -> meta.name == cast name);
+	}
+}

+ 5 - 114
tests/display/src/DisplayTestCase.hx

@@ -1,114 +1,5 @@
-import haxe.display.Position.Range;
-import utest.Assert;
-import Types;
-
-using Lambda;
-
-@:autoBuild(Macro.buildTestCase())
-class DisplayTestCase implements utest.ITest {
-	var ctx:DisplayTestContext;
-
-	public function new() {}
-
-	// api
-	inline function pos(name)
-		return ctx.pos(name);
-
-	inline function fields(pos)
-		return ctx.fields(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);
-
-	inline function signature(pos1)
-		return ctx.signature(pos1);
-
-	inline function doc(pos1)
-		return ctx.doc(pos1);
-
-	inline function metadataDoc(pos1)
-		return ctx.metadataDoc(pos1);
-
-	inline function diagnostics()
-		return ctx.diagnostics();
-
-	inline function noCompletionPoint(f)
-		return ctx.hasErrorMessage(f, "No completion point");
-
-	inline function typeNotFound(f, typeName)
-		return ctx.hasErrorMessage(f, "Type not found : " + typeName);
-
-	function assert(v:Bool)
-		Assert.isTrue(v);
-
-	function eq<T>(expected:T, actual:T, ?pos:haxe.PosInfos) {
-		Assert.equals(expected, actual, pos);
-	}
-
-	function arrayEq<T>(expected:Array<T>, actual:Array<T>, ?pos:haxe.PosInfos) {
-		Assert.same(expected, actual, pos);
-	}
-
-	function arrayCheck<T>(expected:Array<T>, actual:Array<T>, f:T->String, ?pos:haxe.PosInfos) {
-		var expected = [for (expected in expected) f(expected) => expected];
-		for (actual in actual) {
-			var key = f(actual);
-			Assert.isTrue(expected.exists(key), "Result not part of expected Array: " + Std.string(actual), pos);
-			expected.remove(key);
-		}
-
-		for (expected in expected) {
-			Assert.fail("Expected result was not part of actual Array: " + Std.string(expected), pos);
-			return;
-		}
-	}
-
-	function hasField(a:Array<FieldElement>, name:String, type:String, ?kind:String):Bool {
-		return a.exists(function(t) return t.type == type && t.name == name && (kind == null || t.kind == kind));
-	}
-
-	function hasToplevel(a:Array<ToplevelElement>, kind:String, name:String, ?type:String = null):Bool {
-		return a.exists(function(t) return t.kind == kind && t.name == name && (type == null || t.type == type));
-	}
-
-	function hasPath(a:Array<FieldElement>, name:String):Bool {
-		return a.exists(function(t) return t.name == name);
-	}
-
-	function diagnosticsRange(start:Position, end:Position):Range {
-		var range = ctx.source.findRange(start, end);
-		// this is probably correct...?
-		range.start.character--;
-		range.end.character--;
-		return range;
-	}
-
-	function sigEq(arg:Int, params:Array<Array<String>>, sig:SignatureHelp, ?pos:haxe.PosInfos) {
-		eq(arg, sig.activeParameter, pos);
-		eq(params.length, sig.signatures.length, pos);
-		for (i in 0...params.length) {
-			var sigInf = sig.signatures[i];
-			var args = params[i];
-			eq(sigInf.parameters.length, args.length, pos);
-			for (i in 0...args.length) {
-				eq(sigInf.parameters[i].label, args[i], pos);
-			}
-		}
-	}
-
-	function report(message, pos:haxe.PosInfos) {
-		Assert.fail(message, pos);
-	}
-}
+#if (display.protocol == "jsonrpc")
+typedef DisplayTestCase = RpcDisplayTestCase;
+#else
+typedef DisplayTestCase = XmlDisplayTestCase;
+#end

+ 5 - 209
tests/display/src/DisplayTestContext.hx

@@ -1,209 +1,5 @@
-import haxe.io.Bytes;
-import haxe.io.BytesBuffer;
-
-using StringTools;
-
-import Types;
-
-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;
-	}
-
-	public function toString() {
-		return 'HaxeInvocationException($message, $fieldName, $arguments, $source])';
-	}
-}
-
-class DisplayTestContext {
-	static var haxeServer = haxeserver.HaxeServerSync.launch("haxe", []);
-
-	var markers:Map<Int, Int>;
-	var fieldName:String;
-
-	public final source:File;
-
-	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):Position {
-		var r = markers[id];
-		if (r == null)
-			throw "No such marker: " + id;
-		return new Position(r);
-	}
-
-	public function range(pos1:Int, pos2:Int) {
-		return normalizePath(source.formatRange(pos(pos1), pos(pos2)));
-	}
-
-	public function fields(pos:Position):Array<FieldElement> {
-		return extractFields(callHaxe('$pos'));
-	}
-
-	public function signatures(pos:Position):Array<String> {
-		return extractSignatures(callHaxe('$pos'));
-	}
-
-	public function toplevel(pos:Position):Array<ToplevelElement> {
-		return extractToplevel(callHaxe('$pos@toplevel'));
-	}
-
-	public function type(pos:Position):String {
-		return extractType(callHaxe('$pos@type'));
-	}
-
-	public function positions(pos:Position):Array<String> {
-		return extractPositions(callHaxe('$pos@position'));
-	}
-
-	public function position(pos:Position):String {
-		return positions(pos)[0];
-	}
-
-	public function usage(pos:Position):Array<String> {
-		return extractPositions(callHaxe('$pos@usage'));
-	}
-
-	public function documentSymbols():Array<ModuleSymbolEntry> {
-		return haxe.Json.parse(callHaxe("0@module-symbols"))[0].symbols;
-	}
-
-	public function signature(pos:Position):SignatureHelp {
-		return haxe.Json.parse(callHaxe('$pos@signature'));
-	}
-
-	public function doc(pos:Position):String {
-		return extractDoc(callHaxe('$pos@type'));
-	}
-
-	public function metadataDoc(pos:Position):String {
-		return extractMetadata(callHaxe('$pos@type'));
-	}
-
-	public function diagnostics():Array<Diagnostic<Dynamic>> {
-		var result = haxe.Json.parse(callHaxe('0@diagnostics'))[0];
-		return if (result == null) [] else result.diagnostics;
-	}
-
-	public function hasErrorMessage(f:()->Void, message:String) {
-		return try {
-			f();
-			false;
-		} catch (exc:HaxeInvocationException) {
-			return exc.message.indexOf(message) != -1;
-		}
-	}
-
-	function callHaxe(displayPart:String) {
-		var args = ["--display", source.path + "@" + displayPart];
-		var result = runHaxe(args, source.content);
-		if (result.hasError || result.stderr == "") {
-			throw new HaxeInvocationException(result.stderr, fieldName, args, source.content);
-		}
-		return result.stderr;
-	}
-
-	static public function runHaxe(args:Array<String>, ?stdin:String) {
-		return haxeServer.rawRequest(args, stdin == null ? null : Bytes.ofString(stdin));
-	}
-
-	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 extractSignatures(result:String) {
-		var xml = Xml.parse('<x>$result</x>');
-		xml = xml.firstElement();
-		var ret = [];
-		for (xml in xml.elementsNamed("type")) {
-			ret.push(StringTools.trim(xml.firstChild().nodeValue));
-		}
-		return ret;
-	}
-
-	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 extractToplevel(result:String) {
-		var xml = Xml.parse(result);
-		xml = xml.firstElement();
-		if (xml.nodeName != "il") {
-			return null;
-		}
-		var ret = [];
-		for (xml in xml.elementsNamed("i")) {
-			ret.push({kind: xml.get("k"), type: xml.get("t"), name: xml.firstChild().nodeValue});
-		}
-		return ret;
-	}
-
-	static function extractFields(result:String) {
-		var xml = Xml.parse(result);
-		xml = xml.firstElement();
-		if (xml.nodeName != "list") {
-			return null;
-		}
-		var ret = [];
-		for (xml in xml.elementsNamed("i")) {
-			ret.push({name: xml.get("n"), type: xml.firstElement().firstChild().nodeValue, kind: xml.get("k")});
-		}
-		return ret;
-	}
-
-	static function extractDoc(result:String) {
-		var xml = Xml.parse(result);
-		xml = xml.firstElement();
-		if (xml.nodeName != "type") {
-			return null;
-		}
-		return StringTools.trim(xml.get('d'));
-	}
-
-	static function extractMetadata(result:String) {
-		var xml = Xml.parse(result);
-		xml = xml.firstElement();
-		if (xml.nodeName != "metadata") {
-			return null;
-		}
-		return xml.firstChild().nodeValue;
-	}
-
-	static function normalizePath(p:String):String {
-		if (!haxe.io.Path.isAbsolute(p)) {
-			p = Sys.getCwd() + p;
-		}
-		if (Sys.systemName() == "Windows") {
-			// on windows, haxe returns paths with backslashes, drive letter uppercased
-			p = p.substr(0, 1).toUpperCase() + p.substr(1);
-			p = p.replace("/", "\\");
-		}
-		return p;
-	}
-}
+#if (display.protocol == "jsonrpc")
+typedef DisplayTestContext = RpcDisplayTestContext;
+#else
+typedef DisplayTestContext = XmlDisplayTestContext;
+#end

+ 18 - 0
tests/display/src/HaxeInvocationException.hx

@@ -0,0 +1,18 @@
+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;
+	}
+
+	public function toString() {
+		return 'HaxeInvocationException($message, $fieldName, $arguments, $source])';
+	}
+}
+

+ 2 - 2
tests/display/src/Main.hx

@@ -11,9 +11,9 @@ class Main {
 		report.displayHeader = AlwaysShowHeader;
 		report.displaySuccessResults = NeverShowSuccessResults;
 
-		var haxeServer = @:privateAccess DisplayTestContext.haxeServer;
+		var haxeServer = @:privateAccess BaseDisplayTestContext.haxeServer;
 		haxeServer.setDefaultRequestArguments(["-cp", "src", "-cp", "src-shared", "--no-output"]);
-		DisplayTestContext.runHaxe([]);
+		BaseDisplayTestContext.runHaxe([]);
 		runner.run();
 		haxeServer.close();
 	}

+ 180 - 0
tests/display/src/RpcDisplayTestCase.hx

@@ -0,0 +1,180 @@
+import haxe.display.Display;
+import haxe.display.Position.Range;
+import utest.Assert;
+import Types;
+
+using Lambda;
+
+@:autoBuild(Macro.buildTestCase())
+class RpcDisplayTestCase implements utest.ITest {
+	var ctx:RpcDisplayTestContext;
+
+	public function new() {}
+
+	// api
+	inline function pos(name)
+		return ctx.pos(name);
+
+	inline function fields(pos)
+		return ctx.fields(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);
+
+	inline function signature(pos1)
+		return ctx.signature(pos1);
+
+	inline function doc(pos1)
+		return ctx.doc(pos1);
+
+	inline function metadataDoc(pos1)
+		return ctx.metadataDoc(pos1);
+
+	inline function diagnostics()
+		return ctx.diagnostics();
+
+	inline function noCompletionPoint(f)
+		return ctx.hasErrorMessage(f, "No completion point");
+
+	inline function typeNotFound(f, typeName)
+		return ctx.hasErrorMessage(f, "Type not found : " + typeName);
+
+	function assert(v:Bool)
+		Assert.isTrue(v);
+
+	function eq<T>(expected:T, actual:T, ?pos:haxe.PosInfos) {
+		Assert.equals(expected, actual, pos);
+	}
+
+	function arrayEq<T>(expected:Array<T>, actual:Array<T>, ?pos:haxe.PosInfos) {
+		Assert.same(expected, actual, pos);
+	}
+
+	function arrayCheck<T>(expected:Array<T>, actual:Array<T>, f:T->String, ?pos:haxe.PosInfos) {
+		var expected = [for (expected in expected) f(expected) => expected];
+		for (actual in actual) {
+			var key = f(actual);
+			Assert.isTrue(expected.exists(key), "Result not part of expected Array: " + Std.string(actual), pos);
+			expected.remove(key);
+		}
+
+		for (expected in expected) {
+			Assert.fail("Expected result was not part of actual Array: " + Std.string(expected), pos);
+			return;
+		}
+	}
+
+	function hasField<T>(a:Array<DisplayItem<T>>, name:String, type:String, ?kind:String):Bool {
+		return a.exists(t -> isField(t, name, type, kind));
+	}
+
+	function isField<T>(t:DisplayItem<T>, name:String, ?type:String, ?kind:String):Bool {
+		return switch (t.kind) {
+			case ClassField:
+				var f = t.args.field;
+				if (f.name != name) return false;
+
+				// Oh dear...
+				switch [kind, f.kind.kind] {
+					case [null, _]:
+					case ["static", _]: if (f.scope != Static) return false;
+					case ["member", _]: if (f.scope != Member) return false;
+					case ["method", FMethod]:
+					case ["var", FVar]:
+					case _: return false;
+				}
+
+				if (type == null || f.type.args.path.typeName == type) return true;
+				return type == ctx.displayPrinter.printType(f.type);
+			case EnumField:
+				if (kind != null && kind != "enum") return false;
+				t.args.field.name == name;
+			case EnumAbstractField:
+				if (kind != null && kind != "var") return false;
+				t.args.field.name == name;
+			case Module:
+				if (kind != null && kind != "type") return false;
+				t.args.path.moduleName == name;
+			case Type:
+				if (kind != null && kind != "type") return false;
+				t.args.path.typeName == name;
+			case TypeParameter:
+				if (kind != null && kind != "type") return false;
+				t.args.name == name;
+			case Package:
+				if (kind != null && kind != "package") return false;
+				t.args.path.pack[0] == name;
+			case Local:
+				if (kind != null && kind != "local") return false;
+				t.args.name == name;
+			case Literal:
+				if (kind != null && kind != "literal") return false;
+				t.args.name == name;
+			case Keyword:
+				if (kind != null && kind != "keyword") return false;
+				t.args.name == name;
+			case Metadata:
+				if (kind != null && kind != "metadata") return false;
+				t.args.name == name;
+			case _:
+				false;
+		}
+	}
+
+	function hasToplevel<T>(a:Array<DisplayItem<T>>, kind:String, name:String, ?type:String = null):Bool {
+		return a.exists(t -> isToplevel(t, name, type, kind));
+	}
+
+	function isToplevel<T>(t:DisplayItem<T>, name:String, ?type:String = null, ?kind:String = null):Bool {
+		return isField(t, name, type, kind);
+	}
+
+	function hasPath<T>(a:Array<DisplayItem<T>>, name:String):Bool {
+		return a.exists(function(t) {
+			return switch (t.kind) {
+				case ClassField: t.args.field.name == name;
+				case Type: t.args.path.typeName == name;
+				case Module: t.args.path.moduleName == name;
+				case Metadata: t.args.name == name;
+				case _: false;
+			}
+		});
+	}
+
+	function diagnosticsRange(start:Position, end:Position):Range {
+		var range = ctx.source.findRange(start, end);
+		// this is probably correct...?
+		range.start.character--;
+		range.end.character--;
+		return range;
+	}
+
+	function sigEq(arg:Int, params:Array<Array<String>>, sig:SignatureHelp, ?pos:haxe.PosInfos) {
+		eq(arg, sig.activeParameter, pos);
+		eq(params.length, sig.signatures.length, pos);
+		for (i in 0...params.length) {
+			var sigInf = sig.signatures[i];
+			var args = params[i];
+			eq(sigInf.parameters.length, args.length, pos);
+			for (i in 0...args.length) {
+				eq(sigInf.parameters[i].label, args[i], pos);
+			}
+		}
+	}
+
+	function report(message, pos:haxe.PosInfos) {
+		Assert.fail(message, pos);
+	}
+}

+ 165 - 0
tests/display/src/RpcDisplayTestContext.hx

@@ -0,0 +1,165 @@
+import haxe.Json;
+import haxe.display.Display;
+import haxe.display.FsPath;
+import haxe.display.Position;
+import haxe.display.Protocol;
+
+using StringTools;
+
+import Types;
+
+class RpcDisplayTestContext extends BaseDisplayTestContext {
+	public var displayPrinter(default,null):DisplayPrinter;
+
+	public function new(path:String, fieldName:String, source:String, markers:Map<Int, Int>) {
+		super(path, fieldName, source, markers);
+		displayPrinter = new DisplayPrinter();
+	}
+
+	public function fields(pos:Position):Array<DisplayItem<Dynamic>> {
+		return extractFields(callDisplay(DisplayMethods.Completion, {
+			file: new FsPath(source.path),
+			offset: pos,
+			wasAutoTriggered: false
+		}));
+	}
+
+	public function toplevel(pos:Position):Array<DisplayItem<Dynamic>> {
+		return fields(pos);
+	}
+
+	public function type(pos:Position):String {
+		return extractType(callDisplay(DisplayMethods.Hover, {
+			file: new FsPath(source.path),
+			offset: pos,
+		}).result);
+	}
+
+	public function positions(pos:Position):Array<String> {
+		return extractPositions(callDisplay(DisplayMethods.GotoDefinition, {
+			file: new FsPath(source.path),
+			offset: pos,
+		}).result);
+	}
+
+	public function position(pos:Position):String {
+		return positions(pos)[0];
+	}
+
+	public function usage(pos:Position):Array<String> {
+		return extractPositions(callDisplay(DisplayMethods.FindReferences, {
+			file: new FsPath(source.path),
+			offset: pos,
+		}).result);
+	}
+
+	// TODO: migrate module-symbols to json rpc and update here
+	public function documentSymbols():Array<ModuleSymbolEntry> {
+		return Json.parse(callHaxe("0@module-symbols"))[0].symbols;
+	}
+
+	public function signature(pos:Position):SignatureHelp {
+		var res = callDisplay(DisplayMethods.SignatureHelp, {
+			file: new FsPath(source.path),
+			offset: pos,
+			wasAutoTriggered: false
+		}).result;
+
+		return {
+			signatures: res.signatures.map(s -> {
+				label: displayPrinter.printCallArguments(s, displayPrinter.printSignatureFunctionArgument) + ':' + displayPrinter.printType(s.ret),
+				documentation: s.documentation,
+				parameters: s.args.map(a -> {
+					label: displayPrinter.printSignatureFunctionArgument(a),
+					documentation: null
+				})
+			}),
+			activeParameter: res.activeParameter,
+			activeSignature: res.activeSignature
+		};
+	}
+
+	public function doc(pos:Position):String {
+		return extractDoc(callDisplay(DisplayMethods.Hover, {
+			file: new FsPath(source.path),
+			offset: pos,
+		}).result);
+	}
+
+	public function metadataDoc(pos:Position):String {
+		return extractMetadata(callDisplay(DisplayMethods.Hover, {
+			file: new FsPath(source.path),
+			offset: pos,
+		}).result);
+	}
+
+	public function diagnostics():Array<Diagnostic<Any>> {
+		var result = callDisplay(DisplayMethods.Diagnostics, {
+			file: new FsPath(source.path),
+		}).result;
+		return if (result == null || result.length == 0) [] else cast result[0].diagnostics;
+	}
+
+	// Can be removed once module-symbols is migrated to json rpc
+	function callHaxe(displayPart:String) {
+		var args = ["--display", source.path + "@" + displayPart];
+		var result = BaseDisplayTestContext.runHaxe(args, source.content);
+		if (result.hasError || result.stderr == "") {
+			throw new HaxeInvocationException(result.stderr, fieldName, args, source.content);
+		}
+		return result.stderr;
+	}
+
+	function callDisplay<TParams, TResponse>(method:HaxeRequestMethod<TParams, TResponse>, methodArgs:TParams):TResponse {
+		var methodArgs = {method: method, id: 1, params: methodArgs};
+		var args = ['--display', Json.stringify(methodArgs)];
+
+		var result = BaseDisplayTestContext.runHaxe(args, source.content);
+		if (result.hasError || result.stderr == "") {
+			throw new HaxeInvocationException(result.stderr, fieldName, args, source.content);
+		}
+		var json = Json.parse(result.stderr);
+		if (json.result != null) {
+			return json.result;
+		} else {
+			throw new HaxeInvocationException(json.error.data.join("\n"), fieldName, args, source.content);
+		}
+	}
+
+	function extractType(result:HoverDisplayItemOccurence<Dynamic>) {
+		return displayPrinter.printType(result.item.type);
+	}
+
+	static function extractPositions(result:Array<Location>) {
+		return result.map(r -> formatRange(r.file, r.range));
+	}
+
+	static function extractFields(result:CompletionResult) {
+		return result.result.items;
+	}
+
+	static function formatRange(path:FsPath, range:Range):String {
+		// Offset difference with legacy display
+		var offset = 1;
+
+		var start = range.start;
+		var end = range.end;
+		var pos = if (start.line == end.line) {
+			if (start.character == end.character)
+				'character ${start.character + offset}';
+			else
+				'characters ${start.character + offset}-${end.character + offset}';
+		} else {
+			'lines ${start.line + 1}-${end.line + 1}';
+		}
+		return '$path:${start.line + 1}: $pos';
+	}
+
+	function extractDoc(result:HoverDisplayItemOccurence<Dynamic>) {
+		return StringTools.trim(result.item.args.field.doc);
+	}
+
+	function extractMetadata(result:HoverDisplayItemOccurence<Dynamic>) {
+		return result.item.args.doc;
+	}
+}

+ 122 - 0
tests/display/src/XmlDisplayTestCase.hx

@@ -0,0 +1,122 @@
+import haxe.display.Position.Range;
+import utest.Assert;
+import Types;
+
+using Lambda;
+
+@:autoBuild(Macro.buildTestCase())
+class XmlDisplayTestCase implements utest.ITest {
+	var ctx:XmlDisplayTestContext;
+
+	public function new() {}
+
+	// api
+	inline function pos(name)
+		return ctx.pos(name);
+
+	inline function fields(pos)
+		return ctx.fields(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);
+
+	inline function signature(pos1)
+		return ctx.signature(pos1);
+
+	inline function doc(pos1)
+		return ctx.doc(pos1);
+
+	inline function metadataDoc(pos1)
+		return ctx.metadataDoc(pos1);
+
+	inline function diagnostics()
+		return ctx.diagnostics();
+
+	inline function noCompletionPoint(f)
+		return ctx.hasErrorMessage(f, "No completion point");
+
+	inline function typeNotFound(f, typeName)
+		return ctx.hasErrorMessage(f, "Type not found : " + typeName);
+
+	function assert(v:Bool)
+		Assert.isTrue(v);
+
+	function eq<T>(expected:T, actual:T, ?pos:haxe.PosInfos) {
+		Assert.equals(expected, actual, pos);
+	}
+
+	function arrayEq<T>(expected:Array<T>, actual:Array<T>, ?pos:haxe.PosInfos) {
+		Assert.same(expected, actual, pos);
+	}
+
+	function arrayCheck<T>(expected:Array<T>, actual:Array<T>, f:T->String, ?pos:haxe.PosInfos) {
+		var expected = [for (expected in expected) f(expected) => expected];
+		for (actual in actual) {
+			var key = f(actual);
+			Assert.isTrue(expected.exists(key), "Result not part of expected Array: " + Std.string(actual), pos);
+			expected.remove(key);
+		}
+
+		for (expected in expected) {
+			Assert.fail("Expected result was not part of actual Array: " + Std.string(expected), pos);
+			return;
+		}
+	}
+
+	function hasField(a:Array<FieldElement>, name:String, type:String, ?kind:String):Bool {
+		return a.exists(t -> isField(t, name, type, kind));
+	}
+
+	function isField(t:FieldElement, name:String, ?type:String, ?kind:String):Bool {
+		return (type == null || t.type == type) && t.name == name && (kind == null || t.kind == kind);
+	}
+
+	function hasToplevel(a:Array<ToplevelElement>, kind:String, name:String, ?type:String = null):Bool {
+		return a.exists(t -> isToplevel(t, name, type, kind));
+	}
+
+	function isToplevel(t:ToplevelElement, name:String, ?type:String = null, ?kind:String = null):Bool {
+		return (kind == null || t.kind == kind) && t.name == name && (type == null || t.type == type);
+	}
+
+	function hasPath(a:Array<FieldElement>, name:String):Bool {
+		return a.exists(function(t) return t.name == name);
+	}
+
+	function diagnosticsRange(start:Position, end:Position):Range {
+		var range = ctx.source.findRange(start, end);
+		// this is probably correct...?
+		range.start.character--;
+		range.end.character--;
+		return range;
+	}
+
+	function sigEq(arg:Int, params:Array<Array<String>>, sig:SignatureHelp, ?pos:haxe.PosInfos) {
+		eq(arg, sig.activeParameter, pos);
+		eq(params.length, sig.signatures.length, pos);
+		for (i in 0...params.length) {
+			var sigInf = sig.signatures[i];
+			var args = params[i];
+			eq(sigInf.parameters.length, args.length, pos);
+			for (i in 0...args.length) {
+				eq(sigInf.parameters[i].label, args[i], pos);
+			}
+		}
+	}
+
+	function report(message, pos:haxe.PosInfos) {
+		Assert.fail(message, pos);
+	}
+}

+ 135 - 0
tests/display/src/XmlDisplayTestContext.hx

@@ -0,0 +1,135 @@
+import haxe.Json;
+import haxe.display.Display;
+import haxe.display.Protocol;
+
+import BaseDisplayTestContext.normalizePath;
+
+using StringTools;
+
+import Types;
+
+class XmlDisplayTestContext extends BaseDisplayTestContext {
+	public function new(path:String, fieldName:String, source:String, markers:Map<Int, Int>) {
+		super(path, fieldName, source, markers);
+	}
+
+	public function fields(pos:Position):Array<FieldElement> {
+		return extractFields(callHaxe('$pos'));
+	}
+
+	public function toplevel(pos:Position):Array<ToplevelElement> {
+		return extractToplevel(callHaxe('$pos@toplevel'));
+	}
+
+	public function type(pos:Position):String {
+		return extractType(callHaxe('$pos@type'));
+	}
+
+	public function positions(pos:Position):Array<String> {
+		return extractPositions(callHaxe('$pos@position'));
+	}
+
+	public function position(pos:Position):String {
+		return positions(pos)[0];
+	}
+
+	public function usage(pos:Position):Array<String> {
+		return extractPositions(callHaxe('$pos@usage'));
+	}
+
+	public function documentSymbols():Array<ModuleSymbolEntry> {
+		return Json.parse(callHaxe("0@module-symbols"))[0].symbols;
+	}
+
+	public function signature(pos:Position):SignatureHelp {
+		return Json.parse(callHaxe('$pos@signature'));
+	}
+
+	public function doc(pos:Position):String {
+		return extractDoc(callHaxe('$pos@type'));
+	}
+
+	public function metadataDoc(pos:Position):String {
+		return extractMetadata(callHaxe('$pos@type'));
+	}
+
+	public function diagnostics():Array<Diagnostic<Dynamic>> {
+		var result = Json.parse(callHaxe('0@diagnostics'))[0];
+		return if (result == null) [] else result.diagnostics;
+	}
+
+	function callHaxe(displayPart:String) {
+		var args = ["--display", source.path + "@" + displayPart];
+		var result = BaseDisplayTestContext.runHaxe(args, source.content);
+		if (result.hasError || result.stderr == "") {
+			throw new HaxeInvocationException(result.stderr, fieldName, args, source.content);
+		}
+		return result.stderr;
+	}
+
+	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 extractToplevel(result:String) {
+		var xml = Xml.parse(result);
+		xml = xml.firstElement();
+		if (xml.nodeName != "il") {
+			return null;
+		}
+		var ret = [];
+		for (xml in xml.elementsNamed("i")) {
+			ret.push({kind: xml.get("k"), type: xml.get("t"), name: xml.firstChild().nodeValue});
+		}
+		return ret;
+	}
+
+	static function extractFields(result:String) {
+		var xml = Xml.parse(result);
+		xml = xml.firstElement();
+		if (xml.nodeName != "list") {
+			return null;
+		}
+		var ret = [];
+		for (xml in xml.elementsNamed("i")) {
+			ret.push({name: xml.get("n"), type: xml.firstElement().firstChild().nodeValue, kind: xml.get("k")});
+		}
+		return ret;
+	}
+
+	static function extractDoc(result:String) {
+		var xml = Xml.parse(result);
+		xml = xml.firstElement();
+		if (xml.nodeName != "type") {
+			return null;
+		}
+		return StringTools.trim(xml.get('d'));
+	}
+
+	static function extractMetadata(result:String) {
+		var xml = Xml.parse(result);
+		xml = xml.firstElement();
+		if (xml.nodeName != "metadata") {
+			return null;
+		}
+		return xml.firstChild().nodeValue;
+	}
+}

+ 13 - 0
tests/display/src/cases/Completion.hx

@@ -37,6 +37,8 @@ class Completion extends DisplayTestCase {
 	**/
 	@:funcCode function testHaxeUnitPort4() {
 		eq(true, hasPath(fields(pos(1)), "Expr"));
+		BaseDisplayTestContext.runHaxe(['haxe.macro.Expr']);
+		eq(true, hasPath(fields(pos(1)), "Expr"));
 	}
 
 	/**
@@ -44,6 +46,17 @@ class Completion extends DisplayTestCase {
 	**/
 	@:funcCode function testHaxeUnitPort5() {
 		eq(true, hasPath(fields(pos(1)), "ExprDef"));
+		BaseDisplayTestContext.runHaxe(['haxe.macro.Expr']);
+		eq(true, hasPath(fields(pos(1)), "ExprDef"));
+	}
+
+	/**
+		haxe.Json.{-1-}
+	**/
+	@:funcCode function testStaticField() {
+		eq(true, hasPath(fields(pos(1)), "stringify"));
+		BaseDisplayTestContext.runHaxe(['haxe.Json']);
+		eq(true, hasPath(fields(pos(1)), "stringify"));
 	}
 
 	/**

+ 1 - 1
tests/display/src/cases/Issue6068.hx

@@ -20,7 +20,7 @@ class Issue6068 extends DisplayTestCase {
 		var result = try {
 			fn();
 			false;
-		} catch (e:DisplayTestContext.HaxeInvocationException) {
+		} catch (e:HaxeInvocationException) {
 			e.message.indexOf("Not a callable type") != -1;
 		}
 		assert(result);

+ 1 - 1
tests/display/src/cases/Issue7055.hx

@@ -23,7 +23,7 @@ class Issue7055 extends DisplayTestCase {
 		var results = toplevel(pos(1));
 		var i = 0;
 		function nextIs(name, ?pos) {
-			eq(results[i++].name, name, pos);
+			eq(true, isToplevel(results[i++], name), pos);
 		}
 		nextIs("Some");
 		nextIs("Random");

+ 1 - 1
tests/display/src/cases/Issue7059.hx

@@ -11,6 +11,6 @@ class Issue7059 extends DisplayTestCase {
 		}
 	**/
 	function test() {
-		eq(true, toplevel(pos(1)).exists(el -> el.name == "trace"));
+		eq(true, hasToplevel(toplevel(pos(1)), "literal", "trace"));
 	}
 }

+ 1 - 3
tests/display/src/cases/Issue7066.hx

@@ -20,8 +20,6 @@ class Issue7066 extends DisplayTestCase {
 	function test() {
 		var results = fields(pos(1));
 		eq(1, results.length);
-		eq("fieldB", results[0].name);
-		eq("var", results[0].kind);
-		eq("Null<String>", results[0].type);
+		eq(true, isField(results[0], "fieldB", "Null<String>", "var"));
 	}
 }

+ 2 - 2
tests/display/src/cases/Issue7068.hx

@@ -13,7 +13,7 @@ class Issue7068 extends DisplayTestCase {
 		}
 	**/
 	function test() {
-		eq("Bar", toplevel(pos(1))[0].name);
+		eq(true, isToplevel(toplevel(pos(1))[0], "Bar"));
 	}
 
 	/**
@@ -28,6 +28,6 @@ class Issue7068 extends DisplayTestCase {
 		}
 	**/
 	function test2() {
-		eq("Bar", toplevel(pos(1))[0].name);
+		eq(true, isToplevel(toplevel(pos(1))[0], "Bar"));
 	}
 }

+ 5 - 5
tests/display/src/cases/Issue7069.hx

@@ -17,10 +17,10 @@ class Issue7069 extends DisplayTestCase {
 	**/
 	function test() {
 		var results = toplevel(pos(1));
-		eq("i", results[0].name);
-		eq("blockLocal", results[1].name);
-		eq("local", results[2].name);
-		eq("argument", results[3].name);
-		eq("field", results[4].name);
+		eq(true, isToplevel(results[0], "i"));
+		eq(true, isToplevel(results[1], "blockLocal"));
+		eq(true, isToplevel(results[2], "local"));
+		eq(true, isToplevel(results[3], "argument"));
+		eq(true, isToplevel(results[4], "field"));
 	}
 }

+ 2 - 2
tests/display/src/cases/Issue7071.hx

@@ -13,7 +13,7 @@ class Issue7071 extends DisplayTestCase {
 		}
 	**/
 	function test() {
-		eq("bar", toplevel(pos(1))[0].name);
-		eq("bar", toplevel(pos(2))[0].name);
+		eq(true, isToplevel(toplevel(pos(1))[0], "bar"));
+		eq(true, isToplevel(toplevel(pos(2))[0], "bar"));
 	}
 }

+ 1 - 1
tests/display/src/cases/Issue7084.hx

@@ -29,6 +29,6 @@ class Issue7084 extends DisplayTestCase {
 		}
 	**/
 	function test2() {
-		eq("Value", fields(pos(1))[0].name);
+		eq(true, isField(fields(pos(1))[0], "Value"));
 	}
 }

+ 2 - 3
tests/display/src/cases/Issue7136.hx

@@ -18,8 +18,7 @@ class Issue7136 extends DisplayTestCase {
 	function test() {
 		var fields = fields(pos(1));
 		eq(2, fields.length);
-		eq("x", fields[0].name);
-		eq("y", fields[1].name);
-		eq("String", fields[1].type);
+		eq(true, isToplevel(fields[0], "x"));
+		eq(true, isToplevel(fields[1], "y", "String"));
 	}
 }

+ 0 - 1
tests/display/src/cases/Issue7326.hx

@@ -16,7 +16,6 @@ class Issue7326 extends DisplayTestCase {
 		}
 	**/
 	function test() {
-		// sigEq(0, [["v:Int"]], signature(pos(1)));
 		sigEq(0, [["v:Int"]], signature(pos(1)));
 		sigEq(0, [["v:Unknown<0>"]], signature(pos(2)));
 	}

+ 2 - 2
tests/display/src/cases/Issue7864.hx

@@ -12,7 +12,7 @@ class Issue7864 extends DisplayTestCase {
 		class Test {}
 	**/
 	function test() {
-		eq(true, hasField(fields(pos(1)), "@:enum", "", "metadata"));
-		eq(true, hasField(fields(pos(2)), "@:enum", "", "metadata"));
+		eq(true, hasField(fields(pos(1)), "@:enum", null, "metadata"));
+		eq(true, hasField(fields(pos(2)), "@:enum", null, "metadata"));
 	}
 }

+ 3 - 1
tests/display/src/cases/Issue7911.hx

@@ -5,6 +5,8 @@ class Issue7911 extends DisplayTestCase {
 		import misc.issue7911.{-1-}
 	**/
 	function test() {
-		arrayEq([{name: "Test", type: "", kind: "type"}], fields(pos(1)));
+		var fields = fields(pos(1));
+		eq(1, fields.length);
+		eq(true, isField(fields[0], "Test", null, "type"));
 	}
 }

+ 5 - 5
tests/display/src/cases/Issue9133.hx

@@ -16,8 +16,8 @@ class Issue9133 extends DisplayTestCase {
 	**/
 	function test1() {
 		var fields = toplevel(pos(1));
-		var i1 = fields.findIndex(item -> item.kind == "local" && item.name == "i");
-		var i2 = fields.findIndex(item -> item.kind == "local" && item.name == "s");
+		var i1 = fields.findIndex(item -> isToplevel(item, "i", null, "local"));
+		var i2 = fields.findIndex(item -> isToplevel(item, "s", null, "local"));
 		Assert.isTrue(i1 < i2);
 		Assert.isTrue(i1 != -1);
 	}
@@ -37,8 +37,8 @@ class Issue9133 extends DisplayTestCase {
 		// causes a `Duplicate key` error. See https://github.com/HaxeFoundation/haxe/issues/9144
 		// for more context.
 		var fields = toplevel(pos(1));
-		var i1 = fields.findIndex(item -> item.kind == "local" && item.name == "i");
-		var i2 = fields.findIndex(item -> item.kind == "local" && item.name == "s");
+		var i1 = fields.findIndex(item -> isToplevel(item, "i", null, "local"));
+		var i2 = fields.findIndex(item -> isToplevel(item, "s", null, "local"));
 		Assert.isTrue(i1 < i2);
 		Assert.isTrue(i1 != -1);
 	}
@@ -51,7 +51,7 @@ class Issue9133 extends DisplayTestCase {
 	**/
 	function test3() {
 		var fields = toplevel(pos(1));
-		var i1 = fields.findIndex(item -> item.kind == "local" && item.name == "i");
+		var i1 = fields.findIndex(item -> isToplevel(item, "i", null, "local"));
 		Assert.isTrue(i1 != -1);
 	}
 }

+ 4 - 2
tests/display/src/cases/Toplevel.hx

@@ -167,6 +167,7 @@ class Toplevel extends DisplayTestCase {
 		eq(true, hasToplevel(toplevel(pos(2)), "type", "FieldT2"));
 	}
 
+	#if (!display.protocol || display.protocol == "xml")
 	/**
 		import cases.Toplevel.E.a;
 
@@ -186,10 +187,11 @@ class Toplevel extends DisplayTestCase {
 	**/
 	function testDuplicates() {
 		var toplevels = toplevel(pos(1));
-		toplevels = toplevels.filter(function(t) return t.name == "a");
+		toplevels = toplevels.filter(t -> isToplevel(t, "a"));
 		eq(1, toplevels.length);
-		eq("local", toplevels[0].kind);
+		eq(true, isToplevel(toplevels[0], "a", null, "local"));
 	}
+	#end
 
 	/**
 		class Main {

+ 2 - 1
tests/runci/targets/Macro.hx

@@ -9,7 +9,8 @@ class Macro {
 
 		changeDirectory(displayDir);
 		haxelibInstallGit("Simn", "haxeserver");
-		runCommand("haxe", ["build.hxml"]);
+		runCommand("haxe", ["build.hxml", "-D", "display.protocol=xml"]);
+		runCommand("haxe", ["build.hxml", "-D", "display.protocol=jsonrpc"]);
 
 		changeDirectory(sourcemapsDir);
 		runCommand("haxe", ["run.hxml"]);

+ 3 - 3
tests/server/src/cases/display/issues/Issue8194.hx

@@ -17,7 +17,7 @@ class Issue8194 extends DisplayTestCase {
 			offset: offset(1),
 			wasAutoTriggered: true
 		});
-		var result = parseCompletion();
-		Assert.equals(null, result.result);
+		var error = haxe.Json.parse(lastResult.stderr).error;
+		Assert.equals("No completion point", error.data[0]);
 	}
-}
+}