summaryrefslogtreecommitdiffstats
path: root/tests/python
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 19:55:48 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-14 19:55:48 +0000
commit8be448d3881909fb0ce4b033cad71aa7575de0aa (patch)
treeda33caff06645347a08c3c9c56dd703e4acb5aa3 /tests/python
parentInitial commit. (diff)
downloadlsprotocol-8be448d3881909fb0ce4b033cad71aa7575de0aa.tar.xz
lsprotocol-8be448d3881909fb0ce4b033cad71aa7575de0aa.zip
Adding upstream version 2023.0.0.upstream/2023.0.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tests/python')
-rw-r--r--tests/python/__init__.py2
-rw-r--r--tests/python/common/jsonrpc.py52
-rw-r--r--tests/python/notifications/test_exit.py41
-rw-r--r--tests/python/notifications/test_progress.py91
-rw-r--r--tests/python/requests/test_initilize_request.py406
-rw-r--r--tests/python/requests/test_inlay_hint_resolve_request.py86
-rw-r--r--tests/python/requests/test_workspace_sematic_tokens_refresh.py53
-rw-r--r--tests/python/test_cattrs_special_cases.py306
-rw-r--r--tests/python/test_custom_validators.py33
-rw-r--r--tests/python/test_enums.py35
-rw-r--r--tests/python/test_generated_data.py34
-rw-r--r--tests/python/test_import.py11
-rw-r--r--tests/python/test_location.py48
-rw-r--r--tests/python/test_position.py49
-rw-r--r--tests/python/test_range.py36
15 files changed, 1283 insertions, 0 deletions
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"))