diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 19:55:48 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 19:55:48 +0000 |
commit | 8be448d3881909fb0ce4b033cad71aa7575de0aa (patch) | |
tree | da33caff06645347a08c3c9c56dd703e4acb5aa3 /tests | |
parent | Initial commit. (diff) | |
download | lsprotocol-upstream.tar.xz lsprotocol-upstream.zip |
Adding upstream version 2023.0.0.upstream/2023.0.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
21 files changed, 1481 insertions, 0 deletions
diff --git a/tests/dotnet/lsprotocol_tests/.gitignore b/tests/dotnet/lsprotocol_tests/.gitignore new file mode 100644 index 0000000..cbbd0b5 --- /dev/null +++ b/tests/dotnet/lsprotocol_tests/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/
\ No newline at end of file diff --git a/tests/dotnet/lsprotocol_tests/LSPTests.cs b/tests/dotnet/lsprotocol_tests/LSPTests.cs new file mode 100644 index 0000000..80a19ae --- /dev/null +++ b/tests/dotnet/lsprotocol_tests/LSPTests.cs @@ -0,0 +1,123 @@ + +namespace lsprotocol_tests; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + + +public class LSPTests +{ + public static IEnumerable<object[]> JsonTestData() + { + string folderPath; + // Read test data path from environment variable + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("LSP_TEST_DATA_PATH"))) + { + folderPath = Environment.GetEnvironmentVariable("LSP_TEST_DATA_PATH"); + } + else + { + throw new Exception("LSP_TEST_DATA_PATH environment variable not set"); + } + + string[] jsonFiles = Directory.GetFiles(folderPath, "*.json"); + foreach (string filePath in jsonFiles) + { + yield return new object[] { filePath }; + } + } + + [Theory] + [MemberData(nameof(JsonTestData))] + public void ValidateLSPTypes(string filePath) + { + string original = File.ReadAllText(filePath); + + // Get the class name from the file name + // format: <class-name>-<valid>-<test-id>.json + // classname => Class name of the type to deserialize to + // valid => true if the file is valid, false if it is invalid + // test-id => unique id for the test + string fileName = Path.GetFileNameWithoutExtension(filePath); + string[] nameParts = fileName.Split('-'); + string className = nameParts[0]; + bool valid = nameParts[1] == "True"; + + Type type = Type.GetType($"Microsoft.LanguageServer.Protocol.{className}, lsprotocol") ?? throw new Exception($"Type {className} not found"); + RunTest(valid, original, type); + } + + private static void RunTest(bool valid, string data, Type type) + { + if (valid) + { + try + { + var settings = new JsonSerializerSettings + { + MissingMemberHandling = MissingMemberHandling.Error + }; + object? deserializedObject = JsonConvert.DeserializeObject(data, type, settings); + string newJson = JsonConvert.SerializeObject(deserializedObject, settings); + + JToken token1 = JToken.Parse(data); + JToken token2 = JToken.Parse(newJson); + RemoveNullProperties(token1); + RemoveNullProperties(token2); + Assert.True(JToken.DeepEquals(token1, token2), $"JSON before and after serialization don't match:\r\nBEFORE:{data}\r\nAFTER:{newJson}"); + } + catch (Exception e) + { + // Explicitly fail the test + Assert.True(false, $"Should not have thrown an exception for [{type.Name}]: {data} \r\n{e}"); + } + } + else + { + try + { + JsonConvert.DeserializeObject(data, type); + // Explicitly fail the test + Assert.True(false, $"Should have thrown an exception for [{type.Name}]: {data}"); + } + catch + { + // Worked as expected. + } + } + } + + private static void RemoveNullProperties(JToken token) + { + if (token.Type == JTokenType.Object) + { + var obj = (JObject)token; + + var propertiesToRemove = obj.Properties() + .Where(p => p.Value.Type == JTokenType.Null) + .ToList(); + + foreach (var property in propertiesToRemove) + { + property.Remove(); + } + + foreach (var property in obj.Properties()) + { + RemoveNullProperties(property.Value); + } + } + else if (token.Type == JTokenType.Array) + { + var array = (JArray)token; + + for (int i = array.Count - 1; i >= 0; i--) + { + RemoveNullProperties(array[i]); + if (array[i].Type == JTokenType.Null) + { + array.RemoveAt(i); + } + } + } + } +}
\ No newline at end of file diff --git a/tests/dotnet/lsprotocol_tests/Usings.cs b/tests/dotnet/lsprotocol_tests/Usings.cs new file mode 100644 index 0000000..5d95411 --- /dev/null +++ b/tests/dotnet/lsprotocol_tests/Usings.cs @@ -0,0 +1,2 @@ +global using Xunit; +global using Microsoft.LanguageServer.Protocol;
\ No newline at end of file diff --git a/tests/dotnet/lsprotocol_tests/lsprotocol_tests.csproj b/tests/dotnet/lsprotocol_tests/lsprotocol_tests.csproj new file mode 100644 index 0000000..194c872 --- /dev/null +++ b/tests/dotnet/lsprotocol_tests/lsprotocol_tests.csproj @@ -0,0 +1,29 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + + <IsPackable>false</IsPackable> + <IsTestProject>true</IsTestProject> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" /> + <PackageReference Include="xunit" Version="2.4.2" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + <PackageReference Include="coverlet.collector" Version="3.2.0"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\..\packages\dotnet\lsprotocol\lsprotocol.csproj" /> + </ItemGroup> + +</Project> diff --git a/tests/generator/test_model.py b/tests/generator/test_model.py new file mode 100644 index 0000000..50f2b6e --- /dev/null +++ b/tests/generator/test_model.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import pathlib + +import pytest + +import generator.model as model + +lsp_json_path = pathlib.Path(model.__file__).parent / "lsp.json" + + +def test_model_loading(): + json_model = json.loads((lsp_json_path).read_text(encoding="utf-8")) + model.LSPModel(**json_model) + + +def test_model_loading_failure(): + root = pathlib.Path(__file__).parent.parent / "generator" + json_model = json.loads((lsp_json_path).read_text(encoding="utf-8")) + + del json_model["structures"][0]["name"] + with pytest.raises(TypeError): + model.LSPModel(**json_model) diff --git a/tests/generator/test_schema.py b/tests/generator/test_schema.py new file mode 100644 index 0000000..dc24550 --- /dev/null +++ b/tests/generator/test_schema.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import pathlib + +import jsonschema + +lsp_json_path = pathlib.Path(__file__).parent.parent.parent / "generator" / "lsp.json" +lsp_schema_path = lsp_json_path.parent / "lsp.schema.json" + + +def test_validate_with_schema(): + model = json.loads((lsp_json_path).read_text(encoding="utf-8")) + schema = json.loads((lsp_schema_path).read_text(encoding="utf-8")) + + jsonschema.validate(model, schema) diff --git a/tests/python/__init__.py b/tests/python/__init__.py new file mode 100644 index 0000000..5b7f7a9 --- /dev/null +++ b/tests/python/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/tests/python/common/jsonrpc.py b/tests/python/common/jsonrpc.py new file mode 100644 index 0000000..79de35f --- /dev/null +++ b/tests/python/common/jsonrpc.py @@ -0,0 +1,52 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json + +from lsprotocol import converters, types + + +def to_json( + obj: types.MESSAGE_TYPES, + method: str = None, + converter=None, +) -> str: + """Converts a given LSP message object to JSON string using the provided + converter.""" + if not converter: + converter = converters.get_converter() + + if method is None: + method = obj.method if hasattr(obj, "method") else None + + if hasattr(obj, "result"): + if method is None: + raise ValueError(f"`method` must not be None for response type objects.") + obj_type = types.METHOD_TO_TYPES[method][1] + elif hasattr(obj, "error"): + obj_type = types.ResponseErrorMessage + else: + obj_type = types.METHOD_TO_TYPES[method][0] + return json.dumps(converter.unstructure(obj, unstructure_as=obj_type)) + + +def from_json(json_str: str, method: str = None, converter=None) -> types.MESSAGE_TYPES: + """Parses and given JSON string and returns LSP message object using the provided + converter.""" + if not converter: + converter = converters.get_converter() + + obj = json.loads(json_str) + + if method is None: + method = obj.get("method", None) + + if "result" in obj: + if method is None: + raise ValueError(f"`method` must not be None for response type objects.") + obj_type = types.METHOD_TO_TYPES[method][1] + elif "error" in obj: + obj_type = types.ResponseErrorMessage + else: + obj_type = types.METHOD_TO_TYPES[method][0] + return converter.structure(obj, obj_type) diff --git a/tests/python/notifications/test_exit.py b/tests/python/notifications/test_exit.py new file mode 100644 index 0000000..e42a0a9 --- /dev/null +++ b/tests/python/notifications/test_exit.py @@ -0,0 +1,41 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json + +import hamcrest +import jsonrpc +import pytest +from cattrs.errors import ClassValidationError + + +@pytest.mark.parametrize( + "data, expected", + [ + ( + {"method": "exit", "jsonrpc": "2.0"}, + json.dumps({"method": "exit", "jsonrpc": "2.0"}), + ), + ( + {"method": "exit", "params": None, "jsonrpc": "2.0"}, + json.dumps({"method": "exit", "jsonrpc": "2.0"}), + ), + ], +) +def test_exit_serialization(data, expected): + data_str = json.dumps(data) + parsed = jsonrpc.from_json(data_str) + actual_str = jsonrpc.to_json(parsed) + hamcrest.assert_that(actual_str, hamcrest.is_(expected)) + + +@pytest.mark.parametrize( + "data", + [ + json.dumps({}), # missing method and jsonrpc + json.dumps({"method": "invalid"}), # invalid method type + ], +) +def test_exit_invalid(data): + with pytest.raises((ClassValidationError, KeyError)): + jsonrpc.from_json(data) diff --git a/tests/python/notifications/test_progress.py b/tests/python/notifications/test_progress.py new file mode 100644 index 0000000..3f7e918 --- /dev/null +++ b/tests/python/notifications/test_progress.py @@ -0,0 +1,91 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json + +import hamcrest +import jsonrpc +import pytest + +from lsprotocol.types import ( + ProgressNotification, + ProgressParams, + WorkDoneProgressBegin, + WorkDoneProgressEnd, + WorkDoneProgressReport, +) + + +@pytest.mark.parametrize( + "obj, expected", + [ + ( + ProgressNotification( + params=ProgressParams( + token="id1", + value=WorkDoneProgressBegin(title="Begin Progress", percentage=0), + ) + ), + json.dumps( + { + "params": { + "token": "id1", + "value": { + "title": "Begin Progress", + "kind": "begin", + "percentage": 0, + }, + }, + "method": "$/progress", + "jsonrpc": "2.0", + } + ), + ), + ( + ProgressNotification( + params=ProgressParams( + token="id1", + value=WorkDoneProgressReport(message="Still going", percentage=50), + ) + ), + json.dumps( + { + "params": { + "token": "id1", + "value": { + "kind": "report", + "message": "Still going", + "percentage": 50, + }, + }, + "method": "$/progress", + "jsonrpc": "2.0", + } + ), + ), + ( + ProgressNotification( + params=ProgressParams( + token="id1", + value=WorkDoneProgressEnd(message="Finished"), + ) + ), + json.dumps( + { + "params": { + "token": "id1", + "value": { + "kind": "end", + "message": "Finished", + }, + }, + "method": "$/progress", + "jsonrpc": "2.0", + } + ), + ), + ], +) +def test_exit_serialization(obj, expected): + actual_str = jsonrpc.to_json(obj) + hamcrest.assert_that(actual_str, hamcrest.is_(expected)) diff --git a/tests/python/requests/test_initilize_request.py b/tests/python/requests/test_initilize_request.py new file mode 100644 index 0000000..1dc259c --- /dev/null +++ b/tests/python/requests/test_initilize_request.py @@ -0,0 +1,406 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import uuid + +import hamcrest +import jsonrpc +import pytest + +ID = str(uuid.uuid4()) +INITIALIZE_PARAMS = { + "processId": 1105947, + "rootPath": "/home/user/src/Personal/jedi-language-server", + "rootUri": "file:///home/user/src/Personal/jedi-language-server", + "capabilities": { + "workspace": { + "applyEdit": True, + "workspaceEdit": { + "documentChanges": True, + "resourceOperations": ["create", "rename", "delete"], + "failureHandling": "undo", + "normalizesLineEndings": True, + "changeAnnotationSupport": {"groupsOnLabel": False}, + }, + "didChangeConfiguration": {"dynamicRegistration": True}, + "didChangeWatchedFiles": { + "dynamicRegistration": True, + "relativePatternSupport": True, + }, + "codeLens": {"refreshSupport": True}, + "executeCommand": {"dynamicRegistration": True}, + "configuration": True, + "fileOperations": { + "dynamicRegistration": True, + "didCreate": True, + "didRename": True, + "didDelete": True, + "willCreate": True, + "willRename": True, + "willDelete": True, + }, + "semanticTokens": {"refreshSupport": True}, + "inlayHint": {"refreshSupport": True}, + "inlineValue": {"refreshSupport": True}, + "diagnostics": {"refreshSupport": True}, + "symbol": { + "dynamicRegistration": True, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + ] + }, + "tagSupport": {"valueSet": [1]}, + "resolveSupport": {"properties": ["location.range"]}, + }, + "workspaceFolders": True, + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": True, + "versionSupport": True, + "tagSupport": {"valueSet": [1, 2]}, + "codeDescriptionSupport": True, + "dataSupport": True, + }, + "synchronization": { + "dynamicRegistration": True, + "willSave": True, + "willSaveWaitUntil": True, + "didSave": True, + }, + "completion": { + "dynamicRegistration": True, + "contextSupport": True, + "completionItem": { + "snippetSupport": True, + "commitCharactersSupport": True, + "documentationFormat": ["markdown", "plaintext"], + "deprecatedSupport": True, + "preselectSupport": True, + "insertReplaceSupport": True, + "tagSupport": {"valueSet": [1]}, + "resolveSupport": { + "properties": ["documentation", "detail", "additionalTextEdits"] + }, + "labelDetailsSupport": True, + "insertTextModeSupport": {"valueSet": [1, 2]}, + }, + "completionItemKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + ] + }, + "insertTextMode": 2, + "completionList": { + "itemDefaults": [ + "commitCharacters", + "editRange", + "insertTextFormat", + "insertTextMode", + ] + }, + }, + "hover": { + "dynamicRegistration": True, + "contentFormat": ["markdown", "plaintext"], + }, + "signatureHelp": { + "dynamicRegistration": True, + "contextSupport": True, + "signatureInformation": { + "documentationFormat": ["markdown", "plaintext"], + "activeParameterSupport": True, + "parameterInformation": {"labelOffsetSupport": True}, + }, + }, + "references": {"dynamicRegistration": True}, + "definition": {"dynamicRegistration": True, "linkSupport": True}, + "documentHighlight": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + ] + }, + "hierarchicalDocumentSymbolSupport": True, + "tagSupport": {"valueSet": [1]}, + "labelSupport": True, + }, + "codeAction": { + "dynamicRegistration": True, + "isPreferredSupport": True, + "disabledSupport": True, + "dataSupport": True, + "honorsChangeAnnotations": False, + "resolveSupport": {"properties": ["edit"]}, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports", + ] + } + }, + }, + "codeLens": {"dynamicRegistration": True}, + "formatting": {"dynamicRegistration": True}, + "rangeFormatting": {"dynamicRegistration": True}, + "onTypeFormatting": {"dynamicRegistration": True}, + "rename": { + "dynamicRegistration": True, + "prepareSupport": True, + "honorsChangeAnnotations": True, + "prepareSupportDefaultBehavior": 1, + }, + "documentLink": {"dynamicRegistration": True, "tooltipSupport": True}, + "typeDefinition": {"dynamicRegistration": True, "linkSupport": True}, + "implementation": {"dynamicRegistration": True, "linkSupport": True}, + "declaration": {"dynamicRegistration": True, "linkSupport": True}, + "colorProvider": {"dynamicRegistration": True}, + "foldingRange": { + "dynamicRegistration": True, + "rangeLimit": 5000, + "lineFoldingOnly": True, + "foldingRangeKind": {"valueSet": ["comment", "imports", "region"]}, + "foldingRange": {"collapsedText": False}, + }, + "selectionRange": {"dynamicRegistration": True}, + "callHierarchy": {"dynamicRegistration": True}, + "linkedEditingRange": {"dynamicRegistration": True}, + "semanticTokens": { + "dynamicRegistration": True, + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "decorator", + "operator", + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary", + ], + "formats": ["relative"], + "requests": {"range": True, "full": {"delta": True}}, + "multilineTokenSupport": False, + "overlappingTokenSupport": False, + "serverCancelSupport": True, + "augmentsSyntaxTokens": True, + }, + "inlayHint": { + "dynamicRegistration": True, + "resolveSupport": { + "properties": [ + "tooltip", + "textEdits", + "label.tooltip", + "label.location", + "label.command", + ] + }, + }, + "inlineValue": {"dynamicRegistration": True}, + "diagnostic": {"dynamicRegistration": True, "relatedDocumentSupport": True}, + "typeHierarchy": {"dynamicRegistration": True}, + }, + "window": { + "showMessage": {"messageActionItem": {"additionalPropertiesSupport": True}}, + "showDocument": {"support": True}, + "workDoneProgress": True, + }, + "general": { + "regularExpressions": {"engine": "ECMAScript", "version": "ES2020"}, + "markdown": {"parser": "marked", "version": "4.0.10"}, + "positionEncodings": ["utf-16"], + "staleRequestSupport": { + "cancel": True, + "retryOnContentModified": [ + "textDocument/inlayHint", + "textDocument/semanticTokens/full", + "textDocument/semanticTokens/range", + "textDocument/semanticTokens/full/delta", + ], + }, + }, + }, + "initializationOptions": { + "enable": True, + "startupMessage": False, + "trace": {"server": "verbose"}, + "jediSettings": { + "autoImportModules": ["pygls"], + "caseInsensitiveCompletion": True, + "debug": False, + }, + "executable": {"args": [], "command": "jedi-language-server"}, + "codeAction": { + "nameExtractFunction": "jls_extract_def", + "nameExtractVariable": "jls_extract_var", + }, + "completion": { + "disableSnippets": False, + "resolveEagerly": False, + "ignorePatterns": [], + }, + "diagnostics": { + "enable": True, + "didOpen": True, + "didChange": True, + "didSave": True, + }, + "hover": { + "enable": True, + "disable": { + "class": {"all": False, "names": [], "fullNames": []}, + "function": {"all": False, "names": [], "fullNames": []}, + "instance": {"all": False, "names": [], "fullNames": []}, + "keyword": {"all": False, "names": [], "fullNames": []}, + "module": {"all": False, "names": [], "fullNames": []}, + "param": {"all": False, "names": [], "fullNames": []}, + "path": {"all": False, "names": [], "fullNames": []}, + "property": {"all": False, "names": [], "fullNames": []}, + "statement": {"all": False, "names": [], "fullNames": []}, + }, + }, + "workspace": { + "extraPaths": [], + "symbols": { + "maxSymbols": 20, + "ignoreFolders": [".nox", ".tox", ".venv", "__pycache__", "venv"], + }, + }, + }, + "trace": "verbose", + "workspaceFolders": [ + { + "uri": "file:///home/user/src/Personal/jedi-language-server", + "name": "jedi-language-server", + } + ], + "locale": "en_US", + "clientInfo": {"name": "coc.nvim", "version": "0.0.82"}, +} + + +TEST_DATA = [ + {"id": ID, "params": INITIALIZE_PARAMS, "method": "initialize", "jsonrpc": "2.0"}, +] + + +@pytest.mark.parametrize("index", list(range(0, len(TEST_DATA)))) +def test_initialize_request_params(index): + data = TEST_DATA[index] + data_str = json.dumps(data) + parsed = jsonrpc.from_json(data_str) + actual_str = jsonrpc.to_json(parsed) + actual_data = json.loads(actual_str) + hamcrest.assert_that(actual_data, hamcrest.is_(data)) diff --git a/tests/python/requests/test_inlay_hint_resolve_request.py b/tests/python/requests/test_inlay_hint_resolve_request.py new file mode 100644 index 0000000..6dc9f42 --- /dev/null +++ b/tests/python/requests/test_inlay_hint_resolve_request.py @@ -0,0 +1,86 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import uuid + +import hamcrest +import jsonrpc +import pytest +from cattrs.errors import ClassValidationError + +ID = str(uuid.uuid4()) + +TEST_DATA = [ + ( + { + "id": ID, + "method": "inlayHint/resolve", + "params": { + "position": {"line": 6, "character": 5}, + "label": "a label", + "kind": 1, + "paddingLeft": False, + "paddingRight": True, + }, + "jsonrpc": "2.0", + }, + json.dumps( + { + "id": ID, + "params": { + "position": {"line": 6, "character": 5}, + "label": "a label", + "kind": 1, + "paddingLeft": False, + "paddingRight": True, + }, + "method": "inlayHint/resolve", + "jsonrpc": "2.0", + } + ), + ), + ( + { + "id": ID, + "method": "inlayHint/resolve", + "params": { + "position": {"line": 6, "character": 5}, + "label": [ + {"value": "part 1"}, + {"value": "part 2", "tooltip": "a tooltip"}, + ], + "kind": 1, + "paddingLeft": False, + "paddingRight": True, + }, + "jsonrpc": "2.0", + }, + json.dumps( + { + "id": ID, + "params": { + "position": {"line": 6, "character": 5}, + "label": [ + {"value": "part 1"}, + {"value": "part 2", "tooltip": "a tooltip"}, + ], + "kind": 1, + "paddingLeft": False, + "paddingRight": True, + }, + "method": "inlayHint/resolve", + "jsonrpc": "2.0", + } + ), + ), +] + + +@pytest.mark.parametrize("index", list(range(0, len(TEST_DATA)))) +def test_inlay_hint_resolve_request_serialization(index): + data, expected = TEST_DATA[index] + data_str = json.dumps(data) + parsed = jsonrpc.from_json(data_str) + actual_str = jsonrpc.to_json(parsed) + hamcrest.assert_that(actual_str, hamcrest.is_(expected)) diff --git a/tests/python/requests/test_workspace_sematic_tokens_refresh.py b/tests/python/requests/test_workspace_sematic_tokens_refresh.py new file mode 100644 index 0000000..fd1c64a --- /dev/null +++ b/tests/python/requests/test_workspace_sematic_tokens_refresh.py @@ -0,0 +1,53 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import uuid + +import hamcrest +import jsonrpc +import pytest +from cattrs.errors import ClassValidationError + +ID = str(uuid.uuid4()) + +TEST_DATA = [ + ( + {"id": ID, "method": "workspace/semanticTokens/refresh", "jsonrpc": "2.0"}, + json.dumps( + {"id": ID, "method": "workspace/semanticTokens/refresh", "jsonrpc": "2.0"} + ), + ), + ( + { + "id": ID, + "method": "workspace/semanticTokens/refresh", + "params": None, + "jsonrpc": "2.0", + }, + json.dumps( + {"id": ID, "method": "workspace/semanticTokens/refresh", "jsonrpc": "2.0"} + ), + ), +] + + +@pytest.mark.parametrize("index", list(range(0, len(TEST_DATA)))) +def test_workspace_sematic_tokens_refresh_request_serialization(index): + data, expected = TEST_DATA[index] + data_str = json.dumps(data) + parsed = jsonrpc.from_json(data_str) + actual_str = jsonrpc.to_json(parsed) + hamcrest.assert_that(actual_str, hamcrest.is_(expected)) + + +@pytest.mark.parametrize( + "data", + [ + json.dumps({}), # missing method and jsonrpc + json.dumps({"method": "invalid"}), # invalid method type + ], +) +def test_workspace_sematic_tokens_refresh_request_invalid(data): + with pytest.raises((ClassValidationError, KeyError)): + jsonrpc.from_json(data) diff --git a/tests/python/test_cattrs_special_cases.py b/tests/python/test_cattrs_special_cases.py new file mode 100644 index 0000000..c5e0e9c --- /dev/null +++ b/tests/python/test_cattrs_special_cases.py @@ -0,0 +1,306 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from typing import Optional, Union + +import attrs +import hamcrest +import pytest +from cattrs.errors import ClassValidationError + +from lsprotocol import converters as cv +from lsprotocol import types as lsp + + +def test_simple(): + """Ensure that simple LSP types are serializable.""" + data = { + "range": { + "start": {"line": 0, "character": 0}, + "end": {"line": 0, "character": 0}, + }, + "message": "Missing module docstring", + "severity": 3, + "code": "C0114:missing-module-docstring", + "source": "my_lint", + } + converter = cv.get_converter() + obj = converter.structure(data, lsp.Diagnostic) + hamcrest.assert_that(obj, hamcrest.instance_of(lsp.Diagnostic)) + + +def test_numeric_validation(): + """Ensure that out of range numbers raise exception.""" + data = {"line": -1, "character": 0} + converter = cv.get_converter() + with pytest.raises((ClassValidationError, ValueError)): + converter.structure(data, lsp.Position) + + +def test_forward_refs(): + """Test that forward references are handled correctly by cattrs converter.""" + data = { + "uri": "something.py", + "diagnostics": [ + { + "range": { + "start": {"line": 0, "character": 0}, + "end": {"line": 0, "character": 0}, + }, + "message": "Missing module docstring", + "severity": 3, + "code": "C0114:missing-module-docstring", + "source": "my_lint", + }, + { + "range": { + "start": {"line": 2, "character": 6}, + "end": { + "line": 2, + "character": 7, + }, + }, + "message": "Undefined variable 'x'", + "severity": 1, + "code": "E0602:undefined-variable", + "source": "my_lint", + }, + { + "range": { + "start": {"line": 0, "character": 0}, + "end": { + "line": 0, + "character": 10, + }, + }, + "message": "Unused import sys", + "severity": 2, + "code": "W0611:unused-import", + "source": "my_lint", + }, + ], + } + converter = cv.get_converter() + obj = converter.structure(data, lsp.PublishDiagnosticsParams) + hamcrest.assert_that(obj, hamcrest.instance_of(lsp.PublishDiagnosticsParams)) + + +@pytest.mark.parametrize( + "data", + [ + {}, # No properties provided + {"documentSelector": None}, + {"documentSelector": []}, + {"documentSelector": [{"pattern": "something/**"}]}, + {"documentSelector": [{"language": "python"}]}, + {"documentSelector": [{"scheme": "file"}]}, + {"documentSelector": [{"notebook": "jupyter"}]}, + {"documentSelector": [{"language": "python"}]}, + {"documentSelector": [{"notebook": {"notebookType": "jupyter-notebook"}}]}, + {"documentSelector": [{"notebook": {"scheme": "file"}}]}, + {"documentSelector": [{"notebook": {"pattern": "something/**"}}]}, + { + "documentSelector": [ + {"pattern": "something/**"}, + {"language": "python"}, + {"scheme": "file"}, + {"scheme": "untitled", "language": "python"}, + {"notebook": {"pattern": "something/**"}}, + {"notebook": {"scheme": "untitled"}}, + {"notebook": {"notebookType": "jupyter-notebook"}}, + { + "notebook": {"notebookType": "jupyter-notebook"}, + "language": "jupyter", + }, + ] + }, + ], +) +def test_union_with_complex_type(data): + """Ensure types with multiple possible resolutions are handled correctly.""" + converter = cv.get_converter() + obj = converter.structure(data, lsp.TextDocumentRegistrationOptions) + hamcrest.assert_that(obj, hamcrest.instance_of(lsp.TextDocumentRegistrationOptions)) + + +def test_keyword_field(): + """Ensure that fields same names as keywords are handled correctly.""" + data = { + "from": { + "name": "something", + "kind": 5, + "uri": "something.py", + "range": { + "start": {"line": 0, "character": 0}, + "end": { + "line": 0, + "character": 10, + }, + }, + "selectionRange": { + "start": {"line": 0, "character": 2}, + "end": { + "line": 0, + "character": 8, + }, + }, + "data": {"something": "some other"}, + }, + "fromRanges": [ + { + "start": {"line": 0, "character": 0}, + "end": { + "line": 0, + "character": 10, + }, + }, + { + "start": {"line": 12, "character": 0}, + "end": { + "line": 13, + "character": 0, + }, + }, + ], + } + + converter = cv.get_converter() + obj = converter.structure(data, lsp.CallHierarchyIncomingCall) + hamcrest.assert_that(obj, hamcrest.instance_of(lsp.CallHierarchyIncomingCall)) + rev = converter.unstructure(obj, lsp.CallHierarchyIncomingCall) + hamcrest.assert_that(rev, hamcrest.is_(data)) + + +@pytest.mark.parametrize( + "data", + [ + {"settings": None}, + {"settings": 100000}, + {"settings": 1.23456}, + {"settings": True}, + {"settings": "something"}, + {"settings": {"something": "something"}}, + {"settings": []}, + {"settings": [None, None]}, + {"settings": [None, 1, 1.23, True]}, + ], +) +def test_LSPAny(data): + """Ensure that broad primitive and custom type alias is handled correctly.""" + converter = cv.get_converter() + obj = converter.structure(data, lsp.DidChangeConfigurationParams) + hamcrest.assert_that(obj, hamcrest.instance_of(lsp.DidChangeConfigurationParams)) + hamcrest.assert_that( + converter.unstructure(obj, lsp.DidChangeConfigurationParams), + hamcrest.is_(data), + ) + + +@pytest.mark.parametrize( + "data", + [ + {"label": "hi"}, + {"label": [0, 42]}, + ], +) +def test_ParameterInformation(data): + converter = cv.get_converter() + obj = converter.structure(data, lsp.ParameterInformation) + hamcrest.assert_that(obj, hamcrest.instance_of(lsp.ParameterInformation)) + hamcrest.assert_that( + converter.unstructure(obj, lsp.ParameterInformation), + hamcrest.is_(data), + ) + + +def test_completion_item(): + data = dict(label="example", documentation="This is documented") + converter = cv.get_converter() + obj = converter.structure(data, lsp.CompletionItem) + hamcrest.assert_that(obj, hamcrest.instance_of(lsp.CompletionItem)) + hamcrest.assert_that( + converter.unstructure(obj, lsp.CompletionItem), + hamcrest.is_(data), + ) + + +def test_notebook_change_event(): + data = { + "notebookDocument": { + "uri": "untitled:Untitled-1.ipynb?jupyter-notebook", + "notebookType": "jupyter-notebook", + "version": 0, + "cells": [ + { + "kind": 2, + "document": "vscode-notebook-cell:Untitled-1.ipynb?jupyter-notebook#W0sdW50aXRsZWQ%3D", + "metadata": {"custom": {"metadata": {}}}, + } + ], + "metadata": { + "custom": { + "cells": [], + "metadata": { + "orig_nbformat": 4, + "language_info": {"name": "python"}, + }, + }, + "indentAmount": " ", + }, + }, + "cellTextDocuments": [ + { + "uri": "vscode-notebook-cell:Untitled-1.ipynb?jupyter-notebook#W0sdW50aXRsZWQ%3D", + "languageId": "python", + "version": 1, + "text": "", + } + ], + } + + converter = cv.get_converter() + obj = converter.structure(data, lsp.DidOpenNotebookDocumentParams) + hamcrest.assert_that(obj, hamcrest.instance_of(lsp.DidOpenNotebookDocumentParams)) + hamcrest.assert_that( + converter.unstructure(obj, lsp.DidOpenNotebookDocumentParams), + hamcrest.is_(data), + ) + + +def test_notebook_sync_options(): + data = {"notebookSelector": [{"cells": [{"language": "python"}]}]} + + converter = cv.get_converter() + obj = converter.structure(data, lsp.NotebookDocumentSyncOptions) + hamcrest.assert_that(obj, hamcrest.instance_of(lsp.NotebookDocumentSyncOptions)) + hamcrest.assert_that( + converter.unstructure(obj, lsp.NotebookDocumentSyncOptions), + hamcrest.is_(data), + ) + + +@attrs.define +class TestPosEncoding: + """Defines the capabilities provided by a language + server.""" + + position_encoding: Optional[Union[lsp.PositionEncodingKind, str]] = attrs.field( + default=None + ) + + +@pytest.mark.parametrize("e", [None, "utf-8", "utf-16", "utf-32", "something"]) +def test_position_encoding_kind(e): + data = {"positionEncoding": e} + converter = cv.get_converter() + obj = converter.structure(data, TestPosEncoding) + hamcrest.assert_that(obj, hamcrest.instance_of(TestPosEncoding)) + + if e is None: + hamcrest.assert_that( + converter.unstructure(obj, TestPosEncoding), hamcrest.is_({}) + ) + else: + hamcrest.assert_that( + converter.unstructure(obj, TestPosEncoding), hamcrest.is_(data) + ) diff --git a/tests/python/test_custom_validators.py b/tests/python/test_custom_validators.py new file mode 100644 index 0000000..c7fe1bd --- /dev/null +++ b/tests/python/test_custom_validators.py @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import pytest + +import lsprotocol.types as lsp +import lsprotocol.validators as v + + +@pytest.mark.parametrize( + "number", [v.INTEGER_MIN_VALUE, v.INTEGER_MAX_VALUE, 0, 1, -1, 1000, -1000] +) +def test_integer_validator_basic(number): + lsp.VersionedTextDocumentIdentifier(version=number, uri="") + + +@pytest.mark.parametrize("number", [v.INTEGER_MIN_VALUE - 1, v.INTEGER_MAX_VALUE + 1]) +def test_integer_validator_out_of_range(number): + with pytest.raises(Exception): + lsp.VersionedTextDocumentIdentifier(version=number, uri="") + + +@pytest.mark.parametrize( + "number", [v.UINTEGER_MIN_VALUE, v.UINTEGER_MAX_VALUE, 0, 1, 1000, 10000] +) +def test_uinteger_validator_basic(number): + lsp.Position(line=number, character=0) + + +@pytest.mark.parametrize("number", [v.UINTEGER_MIN_VALUE - 1, v.UINTEGER_MAX_VALUE + 1]) +def test_uinteger_validator_out_of_range(number): + with pytest.raises(Exception): + lsp.Position(line=number, character=0) diff --git a/tests/python/test_enums.py b/tests/python/test_enums.py new file mode 100644 index 0000000..3db64c1 --- /dev/null +++ b/tests/python/test_enums.py @@ -0,0 +1,35 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import hamcrest +import pytest + +from lsprotocol import types as lsp + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + ("refactor", lsp.CodeActionKind.Refactor), + (lsp.CodeActionKind.Refactor, lsp.CodeActionKind.Refactor), + ("namespace", lsp.SemanticTokenTypes.Namespace), + (lsp.SemanticTokenTypes.Namespace, lsp.SemanticTokenTypes.Namespace), + ("declaration", lsp.SemanticTokenModifiers.Declaration), + ( + lsp.SemanticTokenModifiers.Declaration, + lsp.SemanticTokenModifiers.Declaration, + ), + ("comment", lsp.FoldingRangeKind.Comment), + (lsp.FoldingRangeKind.Comment, lsp.FoldingRangeKind.Comment), + ("utf-8", lsp.PositionEncodingKind.Utf8), + (lsp.PositionEncodingKind.Utf8, lsp.PositionEncodingKind.Utf8), + (1, lsp.WatchKind.Create), + (lsp.WatchKind.Create, lsp.WatchKind.Create), + (-32700, lsp.ErrorCodes.ParseError), + (lsp.ErrorCodes.ParseError, lsp.ErrorCodes.ParseError), + (-32803, lsp.LSPErrorCodes.RequestFailed), + (lsp.LSPErrorCodes.RequestFailed, lsp.LSPErrorCodes.RequestFailed), + ], +) +def test_custom_enum_types(value, expected): + hamcrest.assert_that(value, hamcrest.is_(expected)) diff --git a/tests/python/test_generated_data.py b/tests/python/test_generated_data.py new file mode 100644 index 0000000..0ecee3c --- /dev/null +++ b/tests/python/test_generated_data.py @@ -0,0 +1,34 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import pathlib +from typing import Generator, List, Union + +import pytest + +import lsprotocol.converters as cv +import lsprotocol.types as lsp + +TEST_DATA_ROOT = pathlib.Path(__file__).parent.parent.parent / "packages" / "testdata" + + +def get_all_json_files(root: Union[pathlib.Path, str]) -> List[pathlib.Path]: + root_path = pathlib.Path(root) + return list(root_path.glob("**/*.json")) + + +converter = cv.get_converter() + + +@pytest.mark.parametrize("json_file", get_all_json_files(TEST_DATA_ROOT)) +def test_generated_data(json_file: str) -> None: + type_name, result_type, _ = json_file.name.split("-", 2) + lsp_type = getattr(lsp, type_name) + data = json.loads(json_file.read_text(encoding="utf-8")) + + try: + converter.structure(data, lsp_type) + assert result_type == "True", "Expected error, but succeeded structuring" + except Exception as e: + assert result_type == "False", "Expected success, but failed structuring" diff --git a/tests/python/test_import.py b/tests/python/test_import.py new file mode 100644 index 0000000..db12729 --- /dev/null +++ b/tests/python/test_import.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import hamcrest + + +def test_import(): + """Ensure that LSP types are importable.""" + import lsprotocol.types as lsp + + hamcrest.assert_that(lsp.MarkupKind.Markdown.value, hamcrest.is_("markdown")) diff --git a/tests/python/test_location.py b/tests/python/test_location.py new file mode 100644 index 0000000..1a8cd8f --- /dev/null +++ b/tests/python/test_location.py @@ -0,0 +1,48 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import hamcrest +import pytest + +from lsprotocol import types as lsp + + +@pytest.mark.parametrize( + ("a", "b", "expected"), + [ + ( + lsp.Location( + "some_path", lsp.Range(lsp.Position(1, 23), lsp.Position(4, 56)) + ), + lsp.Location( + "some_path", lsp.Range(lsp.Position(1, 23), lsp.Position(4, 56)) + ), + True, + ), + ( + lsp.Location( + "some_path", lsp.Range(lsp.Position(1, 23), lsp.Position(4, 56)) + ), + lsp.Location( + "some_path2", lsp.Range(lsp.Position(1, 23), lsp.Position(4, 56)) + ), + False, + ), + ( + lsp.Location( + "some_path", lsp.Range(lsp.Position(1, 23), lsp.Position(4, 56)) + ), + lsp.Location( + "some_path", lsp.Range(lsp.Position(1, 23), lsp.Position(8, 91)) + ), + False, + ), + ], +) +def test_location_equality(a, b, expected): + hamcrest.assert_that(a == b, hamcrest.is_(expected)) + + +def test_location_repr(): + a = lsp.Location("some_path", lsp.Range(lsp.Position(1, 23), lsp.Position(4, 56))) + hamcrest.assert_that(f"{a!r}", hamcrest.is_("some_path:1:23-4:56")) diff --git a/tests/python/test_position.py b/tests/python/test_position.py new file mode 100644 index 0000000..67bff8b --- /dev/null +++ b/tests/python/test_position.py @@ -0,0 +1,49 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import hamcrest +import pytest + +from lsprotocol import types as lsp + + +@pytest.mark.parametrize( + ("a", "b", "comp", "expected"), + [ + (lsp.Position(1, 10), lsp.Position(1, 10), "==", True), + (lsp.Position(1, 10), lsp.Position(1, 11), "==", False), + (lsp.Position(1, 10), lsp.Position(1, 11), "!=", True), + (lsp.Position(1, 10), lsp.Position(2, 20), "!=", True), + (lsp.Position(2, 10), lsp.Position(1, 10), ">", True), + (lsp.Position(2, 10), lsp.Position(1, 10), ">=", True), + (lsp.Position(1, 11), lsp.Position(1, 10), ">", True), + (lsp.Position(1, 11), lsp.Position(1, 10), ">=", True), + (lsp.Position(1, 10), lsp.Position(1, 10), ">=", True), + (lsp.Position(1, 10), lsp.Position(2, 10), "<", True), + (lsp.Position(1, 10), lsp.Position(2, 10), "<=", True), + (lsp.Position(1, 10), lsp.Position(1, 10), "<=", True), + (lsp.Position(1, 10), lsp.Position(1, 11), "<", True), + (lsp.Position(1, 10), lsp.Position(1, 11), "<=", True), + ], +) +def test_position_comparison( + a: lsp.Position, b: lsp.Position, comp: str, expected: bool +): + if comp == "==": + result = a == b + elif comp == "!=": + result = a != b + elif comp == "<": + result = a < b + elif comp == "<=": + result = a <= b + elif comp == ">": + result = a > b + elif comp == ">=": + result = a >= b + hamcrest.assert_that(result, hamcrest.is_(expected)) + + +def test_position_repr(): + p = lsp.Position(1, 23) + hamcrest.assert_that(f"{p!r}", hamcrest.is_("1:23")) diff --git a/tests/python/test_range.py b/tests/python/test_range.py new file mode 100644 index 0000000..9977e95 --- /dev/null +++ b/tests/python/test_range.py @@ -0,0 +1,36 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import hamcrest +import pytest + +from lsprotocol import types as lsp + + +@pytest.mark.parametrize( + ("a", "b", "expected"), + [ + ( + lsp.Range(lsp.Position(1, 23), lsp.Position(4, 56)), + lsp.Range(lsp.Position(1, 23), lsp.Position(4, 56)), + True, + ), + ( + lsp.Range(lsp.Position(1, 23), lsp.Position(4, 56)), + lsp.Range(lsp.Position(1, 23), lsp.Position(4, 57)), + False, + ), + ( + lsp.Range(lsp.Position(1, 23), lsp.Position(4, 56)), + lsp.Range(lsp.Position(1, 23), lsp.Position(7, 56)), + False, + ), + ], +) +def test_range_equality(a, b, expected): + hamcrest.assert_that(a == b, hamcrest.is_(expected)) + + +def test_range_repr(): + a = lsp.Range(lsp.Position(1, 23), lsp.Position(4, 56)) + hamcrest.assert_that(f"{a!r}", hamcrest.is_("1:23-4:56")) |