Skip to content

Commit

Permalink
[CI] Run display tests with both xml and json rpc api (#11506)
Browse files Browse the repository at this point in the history
* [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
  • Loading branch information
kLabz committed Jan 25, 2024
1 parent 104341a commit be88d42
Show file tree
Hide file tree
Showing 29 changed files with 899 additions and 361 deletions.
5 changes: 4 additions & 1 deletion src/compiler/displayOutput.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 0 additions & 1 deletion std/haxe/display/Display.hx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ class DisplayMethods {
TODO:
- finish completion
- diagnostics
- codeLens
- workspaceSymbols ("project/symbol"?)
*/
Expand Down
56 changes: 56 additions & 0 deletions tests/display/src/BaseDisplayTestContext.hx
Original file line number Diff line number Diff line change
@@ -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 changes: 159 additions & 0 deletions tests/display/src/DisplayPrinter.hx
Original file line number Diff line number Diff line change
@@ -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);
}
}
119 changes: 5 additions & 114 deletions tests/display/src/DisplayTestCase.hx
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit be88d42

Please sign in to comment.