Testing improvements (#662)
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
const std = @import("std");
|
||||
const zls = @import("zls");
|
||||
|
||||
const helper = @import("helper");
|
||||
const Context = @import("context").Context;
|
||||
const helper = @import("../helper.zig");
|
||||
const Context = @import("../context.zig").Context;
|
||||
const ErrorBuilder = @import("../ErrorBuilder.zig");
|
||||
|
||||
const types = zls.types;
|
||||
const offsets = zls.offsets;
|
||||
@@ -66,84 +67,70 @@ test "inlayhints - builtin call" {
|
||||
}
|
||||
|
||||
fn testInlayHints(source: []const u8) !void {
|
||||
const phr = try helper.collectClearPlaceholders(allocator, source);
|
||||
var phr = try helper.collectClearPlaceholders(allocator, source);
|
||||
defer phr.deinit(allocator);
|
||||
|
||||
var ctx = try Context.init();
|
||||
defer ctx.deinit();
|
||||
|
||||
const open_document = requests.OpenDocument{
|
||||
.params = .{
|
||||
.textDocument = .{
|
||||
.uri = "file:///test.zig",
|
||||
// .languageId = "zig",
|
||||
// .version = 420,
|
||||
.text = phr.source,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const did_open_method = try std.json.stringifyAlloc(allocator, open_document.params, .{});
|
||||
defer allocator.free(did_open_method);
|
||||
|
||||
try ctx.request("textDocument/didOpen", did_open_method, null);
|
||||
try ctx.requestDidOpen("file:///test.zig", phr.new_source);
|
||||
|
||||
const range = types.Range{
|
||||
.start = types.Position{ .line = 0, .character = 0 },
|
||||
.end = offsets.indexToPosition(phr.source, phr.source.len, .utf16),
|
||||
.end = offsets.indexToPosition(phr.new_source, phr.new_source.len, .utf16),
|
||||
};
|
||||
|
||||
const method = try std.json.stringifyAlloc(allocator, .{
|
||||
.textDocument = .{
|
||||
.uri = "file:///test.zig",
|
||||
},
|
||||
.range = range,
|
||||
}, .{});
|
||||
defer allocator.free(method);
|
||||
|
||||
const response_bytes = try ctx.requestAlloc("textDocument/inlayHint", method);
|
||||
defer allocator.free(response_bytes);
|
||||
|
||||
const InlayHint = struct {
|
||||
position: types.Position,
|
||||
label: []const u8,
|
||||
kind: types.InlayHintKind,
|
||||
};
|
||||
|
||||
const Response = struct {
|
||||
jsonrpc: []const u8,
|
||||
id: types.RequestId,
|
||||
result: []InlayHint,
|
||||
const request = requests.InlayHint{
|
||||
.params = .{
|
||||
.textDocument = .{ .uri = "file:///test.zig" },
|
||||
.range = range,
|
||||
},
|
||||
};
|
||||
|
||||
const parse_options = std.json.ParseOptions{
|
||||
.allocator = allocator,
|
||||
.ignore_unknown_fields = true,
|
||||
const response = try ctx.requestGetResponse(?[]InlayHint, "textDocument/inlayHint", request);
|
||||
defer response.deinit();
|
||||
|
||||
const hints: []InlayHint = response.result orelse {
|
||||
std.debug.print("Server returned `null` as the result\n", .{});
|
||||
return error.InvalidResponse;
|
||||
};
|
||||
var token_stream = std.json.TokenStream.init(response_bytes);
|
||||
var response = try std.json.parse(Response, &token_stream, parse_options);
|
||||
defer std.json.parseFree(Response, response, parse_options);
|
||||
|
||||
const hints = response.result;
|
||||
var error_builder = ErrorBuilder.init(allocator, phr.new_source);
|
||||
defer error_builder.deinit();
|
||||
errdefer error_builder.writeDebug();
|
||||
|
||||
try std.testing.expectEqual(phr.placeholder_locations.len, hints.len);
|
||||
var i: usize = 0;
|
||||
outer: while (i < phr.locations.len) : (i += 1) {
|
||||
const old_loc = phr.locations.items(.old)[i];
|
||||
const new_loc = phr.locations.items(.new)[i];
|
||||
|
||||
outer: for (phr.placeholder_locations) |loc, i| {
|
||||
const name = phr.placeholders[i].placeholderSlice(source);
|
||||
const expected_name = offsets.locToSlice(source, old_loc);
|
||||
const expected_label = expected_name[1 .. expected_name.len - 1]; // convert <name> to name
|
||||
|
||||
const position = offsets.indexToPosition(phr.source, loc, .utf16);
|
||||
const position = offsets.indexToPosition(phr.new_source, new_loc.start, ctx.server.offset_encoding);
|
||||
|
||||
for (hints) |hint| {
|
||||
if (position.line != hint.position.line or position.character != hint.position.character) continue;
|
||||
|
||||
const actual_label = hint.label[0 .. hint.label.len - 1]; // exclude :
|
||||
|
||||
try std.testing.expect(hint.label.len != 0);
|
||||
const trimmedLabel = hint.label[0 .. hint.label.len - 1]; // exclude :
|
||||
try std.testing.expectEqualStrings(name, trimmedLabel);
|
||||
try std.testing.expectEqual(types.InlayHintKind.Parameter, hint.kind);
|
||||
if (!std.mem.eql(u8, expected_label, actual_label)) {
|
||||
try error_builder.msgAtLoc("expected label `{s}` here but got `{s}`!", new_loc, .err, .{ expected_label, actual_label });
|
||||
}
|
||||
if (hint.kind != types.InlayHintKind.Parameter) {
|
||||
try error_builder.msgAtLoc("hint kind should be `{s}` but got `{s}`!", new_loc, .err, .{ @tagName(types.InlayHintKind.Parameter), @tagName(hint.kind) });
|
||||
}
|
||||
|
||||
continue :outer;
|
||||
}
|
||||
std.debug.print("Placeholder '{s}' at {}:{} (line:colon) not found!", .{ name, position.line, position.character });
|
||||
return error.PlaceholderNotFound;
|
||||
try error_builder.msgAtLoc("expected hint `{s}` here", new_loc, .err, .{expected_label});
|
||||
}
|
||||
|
||||
if (error_builder.hasMessages()) return error.InvalidResponse;
|
||||
}
|
||||
|
||||
189
tests/lsp_features/references.zig
Normal file
189
tests/lsp_features/references.zig
Normal file
@@ -0,0 +1,189 @@
|
||||
const std = @import("std");
|
||||
const zls = @import("zls");
|
||||
|
||||
const helper = @import("../helper.zig");
|
||||
const Context = @import("../context.zig").Context;
|
||||
const ErrorBuilder = @import("../ErrorBuilder.zig");
|
||||
|
||||
const types = zls.types;
|
||||
const requests = zls.requests;
|
||||
const offsets = zls.offsets;
|
||||
|
||||
const allocator: std.mem.Allocator = std.testing.allocator;
|
||||
|
||||
// TODO fix references so that we can stop skipping these tests
|
||||
const skip_references_tests = true;
|
||||
|
||||
test "references" {
|
||||
if (skip_references_tests) return error.SkipZigTest;
|
||||
try testReferences(
|
||||
\\const <0> = 0;
|
||||
\\const foo = <0>;
|
||||
);
|
||||
try testReferences(
|
||||
\\var <0> = 0;
|
||||
\\var foo = <0>;
|
||||
);
|
||||
try testReferences(
|
||||
\\const <0> = struct {};
|
||||
\\var foo: <0> = <0>{};
|
||||
);
|
||||
try testReferences(
|
||||
\\const <0> = enum {};
|
||||
\\var foo: <0> = undefined;
|
||||
);
|
||||
try testReferences(
|
||||
\\const <0> = union {};
|
||||
\\var foo: <0> = <0>{};
|
||||
);
|
||||
try testReferences(
|
||||
\\fn <0>() void {}
|
||||
\\var foo = <0>();
|
||||
);
|
||||
try testReferences(
|
||||
\\const <0> = error{};
|
||||
\\fn bar() <0>!void {}
|
||||
);
|
||||
}
|
||||
|
||||
test "references - global scope" {
|
||||
if (skip_references_tests) return error.SkipZigTest;
|
||||
try testReferences(
|
||||
\\const foo = <0>;
|
||||
\\const <0> = 0;
|
||||
\\const bar = <0>;
|
||||
);
|
||||
}
|
||||
|
||||
test "references - local scope" {
|
||||
try testReferences(
|
||||
\\fn foo(<0>: u32, bar: u32) void {
|
||||
\\ return <0> + bar;
|
||||
\\}
|
||||
);
|
||||
if (skip_references_tests) return error.SkipZigTest;
|
||||
try testReferences(
|
||||
\\const foo = blk: {
|
||||
\\ _ = blk: {
|
||||
\\ const <0> = 0;
|
||||
\\ break :blk <0>;
|
||||
\\ };
|
||||
\\ const <1> = 0;
|
||||
\\ break :blk <1>;
|
||||
\\};
|
||||
\\const bar = foo;
|
||||
);
|
||||
}
|
||||
|
||||
test "references - label" {
|
||||
if (skip_references_tests) return error.SkipZigTest;
|
||||
try testReferences(
|
||||
\\const foo = <0>: {
|
||||
\\ break :<0> 0;
|
||||
\\};
|
||||
);
|
||||
}
|
||||
|
||||
fn testReferences(source: []const u8) !void {
|
||||
const file_uri = "file:///test.zig";
|
||||
const new_name = "placeholder";
|
||||
|
||||
var phr = try helper.collectReplacePlaceholders(allocator, source, new_name);
|
||||
defer phr.deinit(allocator);
|
||||
|
||||
var ctx = try Context.init();
|
||||
defer ctx.deinit();
|
||||
|
||||
try ctx.requestDidOpen(file_uri, phr.new_source);
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < phr.locations.len) : (i += 1) {
|
||||
const var_loc = phr.locations.items(.old)[i];
|
||||
const var_name = offsets.locToSlice(source, var_loc);
|
||||
const var_loc_middle = var_loc.start + (var_loc.end - var_loc.start) / 2;
|
||||
|
||||
const request = requests.References{
|
||||
.params = .{
|
||||
.textDocument = .{ .uri = file_uri },
|
||||
.position = offsets.indexToPosition(source, var_loc_middle, ctx.server.offset_encoding),
|
||||
.context = .{ .includeDeclaration = true },
|
||||
},
|
||||
};
|
||||
|
||||
const response = try ctx.requestGetResponse(?[]types.Location, "textDocument/references", request);
|
||||
defer response.deinit();
|
||||
|
||||
const locations: []types.Location = response.result orelse {
|
||||
std.debug.print("Server returned `null` as the result\n", .{});
|
||||
return error.InvalidResponse;
|
||||
};
|
||||
|
||||
for (locations) |response_location| {
|
||||
const actual_name = offsets.rangeToSlice(phr.new_source, response_location.range, ctx.server.offset_encoding);
|
||||
try std.testing.expectEqualStrings(file_uri, response_location.uri);
|
||||
try std.testing.expectEqualStrings(new_name, actual_name);
|
||||
}
|
||||
|
||||
// collect all new placeholder locations with the given name
|
||||
const expected_locs: []offsets.Loc = blk: {
|
||||
var locs = std.ArrayListUnmanaged(offsets.Loc){};
|
||||
errdefer locs.deinit(allocator);
|
||||
|
||||
var j: usize = 0;
|
||||
while (j < phr.locations.len) : (j += 1) {
|
||||
const old_loc = phr.locations.items(.old)[j];
|
||||
const new_loc = phr.locations.items(.new)[j];
|
||||
|
||||
const old_loc_name = offsets.locToSlice(source, old_loc);
|
||||
if (!std.mem.eql(u8, var_name, old_loc_name)) continue;
|
||||
try locs.append(allocator, new_loc);
|
||||
}
|
||||
|
||||
break :blk locs.toOwnedSlice(allocator);
|
||||
};
|
||||
defer allocator.free(expected_locs);
|
||||
|
||||
var error_builder = ErrorBuilder.init(allocator, phr.new_source);
|
||||
defer error_builder.deinit();
|
||||
errdefer {
|
||||
const note_loc = phr.locations.items(.new)[i];
|
||||
error_builder.msgAtLoc("asked for references here", note_loc, .info, .{}) catch {};
|
||||
error_builder.writeDebug();
|
||||
}
|
||||
|
||||
// keeps track of expected locations that have been given by the server
|
||||
// used to detect double references and missing references
|
||||
var visited = try std.DynamicBitSetUnmanaged.initEmpty(allocator, expected_locs.len);
|
||||
defer visited.deinit(allocator);
|
||||
|
||||
for (locations) |response_location| {
|
||||
const actual_loc = offsets.rangeToLoc(phr.new_source, response_location.range, ctx.server.offset_encoding);
|
||||
|
||||
const index = found_index: {
|
||||
for (expected_locs) |expected_loc, idx| {
|
||||
if (expected_loc.start != actual_loc.start) continue;
|
||||
if (expected_loc.end != actual_loc.end) continue;
|
||||
break :found_index idx;
|
||||
}
|
||||
try error_builder.msgAtLoc("server returned unexpected reference!", actual_loc, .err, .{});
|
||||
return error.UnexpectedReference;
|
||||
};
|
||||
|
||||
if (visited.isSet(index)) {
|
||||
try error_builder.msgAtLoc("server returned duplicate reference!", actual_loc, .err, .{});
|
||||
return error.DuplicateReference;
|
||||
} else {
|
||||
visited.set(index);
|
||||
}
|
||||
}
|
||||
|
||||
var has_unvisited = false;
|
||||
var unvisited_it = visited.iterator(.{ .kind = .unset });
|
||||
while (unvisited_it.next()) |index| {
|
||||
try error_builder.msgAtLoc("expected reference here!", expected_locs[index], .err, .{});
|
||||
has_unvisited = true;
|
||||
}
|
||||
|
||||
if (has_unvisited) return error.ExpectedReference;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
const std = @import("std");
|
||||
const zls = @import("zls");
|
||||
|
||||
const Context = @import("context").Context;
|
||||
const Context = @import("../context.zig").Context;
|
||||
|
||||
const requests = zls.requests;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user