From 68a35338bfe22eb46c39d58add02f1f5fbfd9324 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:49:36 +0200 Subject: [PATCH 01/42] feat: Add test util to get fixture name based on file path --- tests/test_utils.py | 31 +++++++++++++++++++++++++++ tests/util.py | 52 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 tests/test_utils.py diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..5f068ff --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,31 @@ +""" +Test the test utils module +""" + +from .util import fixture_name_from_file + + +def test_nominal_fixture_naming(): + path = "path/to/fixture.file" + name = fixture_name_from_file(path) + assert name == "path_to_fixture" + +def test_fixture_naming_ignore_prefix(): + path = "path/to/fixture.file" + name = fixture_name_from_file(path, root_folder="path/to") + assert name == "fixture" + +def test_fixture_naming_ignore_prefix_middle_of_path(): + path = "path/to/fixture.file" + name = fixture_name_from_file(path, root_folder="to") + assert name == "fixture" + +def test_fixture_naming_rename_extension(): + path = "path/to/fixture.file" + name = fixture_name_from_file(path, extension_renaming=lambda x: x) + assert name == "path_to_fixture_file" + +def test_fixture_naming_rename_extension_with_ignore_prefix(): + path = "path/to/fixture.file" + name = fixture_name_from_file(path, extension_renaming=lambda x: x, root_folder="path/to") + assert name == "fixture_file" diff --git a/tests/util.py b/tests/util.py index 6c8264d..9567b6e 100644 --- a/tests/util.py +++ b/tests/util.py @@ -10,6 +10,56 @@ from malls.mal_lsp import MALLSPServer +def fixture_name_from_file( + file_name: str | Path, + extension_renaming: typing.Callable[[str], str] | dict[str, str] | None = None, + root_folder: str | None = None) -> str: + """ + Compute the name of a fixture based on its file path. + + Params: + - `file_name`: A Path or str of the fixture. + - `extension_renaming`: A function that takes in a string and outputs a string or + a dictionary mapping strings to strings, input will always + be the extension of the file. Default is to map to nothing. + - `root_folder`: A root folder pattern. If supplied, anything up to and including + this value will be ignored when outputting the name. A following + slash will also be ignored. + """ + # Default extension naming is none + if extension_renaming is None: + def extension_renaming(x: str): + return "" + # If a dictionary was provided, alias it as a function call to make it consistent with function + # usage. If key/extension not present, default to no extension naming. + if extension_renaming is dict: + def extension_renaming(x: str): + return extension_renaming.get(x, "") + + # Default to handling of strings + if file_name is Path: + file_name = str(file_name) + + # If a ignore prefix was given, find it and only care for anything after it + # (on top of subsequent slash) + if root_folder: + post_prefix_index = file_name.find(root_folder) + file_name = file_name[post_prefix_index + len(root_folder) + 1:] + + extension_dot_index = file_name.rindex(".") + extension = file_name[extension_dot_index + 1:] + # Remove extension, e.g: .http/.lsp/.mal + fixture_name = file_name[: extension_dot_index] + # Replace dots with underscore, e.g: empty.out -> empty_out + fixture_name = fixture_name.replace(".", "_") + # Add subdirectory path as prefix if there was one + # Replace directory delimiters with underscores + fixture_name = fixture_name.replace("/", "_").replace("\\", "_") + # Add possible extension name + if (extension := extension_renaming(extension)): + fixture_name += "_" + extension + + return fixture_name def build_payload(to_include: list): result = b"" @@ -137,7 +187,7 @@ async def run_server(): "method": "textDocument/didOpen", "params": { "textDocument": { - "uri": main_file_path, + "uri": main_file_path, "languageId": "mal", "version": 0, "text": '#id: "org.mal-lang.testAnalyzer"\n#version:"0.0.0"\n\ncategory ' From 745e582d8c1554ccd266a7e9773148fece5677ff Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Tue, 19 Aug 2025 15:56:55 +0200 Subject: [PATCH 02/42] feat: Add test util to load fixture file as pytest fixture --- tests/util.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/util.py b/tests/util.py index 9567b6e..5cef586 100644 --- a/tests/util.py +++ b/tests/util.py @@ -4,12 +4,14 @@ import typing from pathlib import Path +import pytest from pylsp_jsonrpc.endpoint import Endpoint from pylsp_jsonrpc.exceptions import JsonRpcException from pylsp_jsonrpc.streams import JsonRpcStreamReader from malls.mal_lsp import MALLSPServer + def fixture_name_from_file( file_name: str | Path, extension_renaming: typing.Callable[[str], str] | dict[str, str] | None = None, @@ -61,6 +63,41 @@ def extension_renaming(x: str): return fixture_name + +def load_fixture_file_into_module( + path: str | Path, + module, + extension_renaming: typing.Callable[[str], str] | dict[str, str] | None = None, + root_folder: str | None = None) -> None: + """ + Load the raw contents of a file as a fixture into the provided module. + + Shares options with `fixture_name_from_file`. + """ + + # Generate a function that handles openening/closing of the file for fixture purposes. + def open_fixture_file(file: str): + def template() -> typing.BinaryIO: + """Opens a fixture in (r)ead (b)inary mode. See `open` for more details.""" + + with open(file, "rb") as file_descriptor: + yield file_descriptor + + return template + + open_fixture_file.__doc__ = open.__doc__ + + fixture_name = fixture_name_from_file(path, + extension_renaming=extension_renaming, + root_folder=root_folder) + + fixture = pytest.fixture( + open_fixture_file(path), + name=fixture_name, + ) + # Bind `fixture` as `fixture_name` inside this module so it gets exported + setattr(module, fixture_name, fixture) + def build_payload(to_include: list): result = b"" for payload in to_include: From dfbe86ba515b548f032b1f1589d3b8ae9e805dcd Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Fri, 22 Aug 2025 12:01:18 +0200 Subject: [PATCH 03/42] feat: LSP requests as fixture components and accompanying utils Adds the base life cycle (initialize, initalized, exit, shutdown, invalid request) messages into the global conftest. They are built on each fixture appending their messages into a list of notifications, requests, or responses (and a total messages) list. The added utils include a way to build these into a BytesIO buffer based on a previous 'build_payload' function. Furthermore, adds a function to add 'find_last_request' to aid with certain related scenarios when building fixtures. Lastly, a new constant 'CONTENT_HEADER_TYPE' with the standard added header of the python-lsp-jsonrpc library. --- tests/conftest.py | 210 ++++++++++++++++++++++++++++++++++++++++++++++ tests/util.py | 48 +++++++++++ 2 files changed, 258 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 8659657..2df2fee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,13 @@ +import json import logging import os import sys import typing from io import BytesIO +from malls.lsp.enums import ErrorCodes +# from ..src.malls.lsp.enums import ErrorCodes + import pytest from .util import ( @@ -22,6 +26,9 @@ OPEN_FILE_WITH_INCLUDED_FILE, OPEN_INCLUDED_FILE_WITH_ERROR, build_payload, + build_rpc_message_stream, + find_last_request, + CONTENT_TYPE_HEADER ) logging.getLogger().setLevel(logging.DEBUG) @@ -108,3 +115,206 @@ def template() -> typing.BinaryIO: ) # Bind `fixture` as `fixture_name` inside this module so it gets exported setattr(module, fixture_name, fixture) + +@pytest.fixture +def client_requests() -> list[dict]: + """ + Keeps track of all requests from the client made so far. + + Has to be manually requested and added to. + """ + return [] + +@pytest.fixture +def client_notifications() -> list[dict]: + """ + Keeps track of all notifications from the client made so far. + + Has to be manually requested and added to. + """ + return [] + +@pytest.fixture +def client_responses() -> list[dict]: + """ + Keeps track of all responses from the client made so far. + + Has to be manually requested and added to. + """ + return [] + +@pytest.fixture +def client_messages() -> list[dict]: + """ + Keeps track of all messages from the client made so far. + + Has to be manually requested and added to. + """ + return [] + +@pytest.fixture +def server_requests() -> list[dict]: + """ + Keeps track of all requests from the server made so far. + + Has to be manually requested and added to. + """ + return [] + +@pytest.fixture +def server_notifications() -> list[dict]: + """ + Keeps track of all notifications from the server made so far. + + Has to be manually requested and added to. + """ + return [] + +@pytest.fixture +def server_responses() -> list[dict]: + """ + Keeps track of all responses from the server made so far. + + Has to be manually requested and added to. + """ + return [] + +@pytest.fixture +def server_messages() -> list[dict]: + """ + Keeps track of all messages from the server made so far. + + Has to be manually requested and added to. If creatin + """ + return [] + +@pytest.fixture +def initalize_request(client_requests: list[dict], client_messages: list[dict]) -> dict: + """ + Defines an `initalize` LSP request from client to server. + """ + message = { + "jsonrpc": "2.0", + "id": len(client_requests), + "method": "initialize", + "params": { + "trace": "off", + "capabilities": {} + } + } + client_requests.append(message) + client_messages.append(message) + return message + +@pytest.fixture +def initalize_response(client_requests: list[dict], + server_responses: list[dict], + server_messages: list[dict]) -> dict: + """ + Creates a default response to the latest (id) `initalize` request from a client. + Defaults to ID 0. + """ + message = { + "jsonrpc": "2.0", + "id": find_last_request(client_requests, + lambda request: request.get("method") == "initalize", + {}).get("id", 0), + "result": { + # TODO: Replace with values from an actual server instance (e.g. via instance.capabilities()) + "capabilities": { + "positionEncoding": "utf-16", + "textDocumentSync": { + "openClose": True, + "change":1 + }, + "definitionProvider": True, + "completionProvider": {} + }, + # TODO: Replace with values from an actual server instance (e.g. via instance.server_info()) + "serverInfo": { + "name": "mal-ls" + } + } + } + server_responses.append(message) + server_messages.append(message) + return message + +@pytest.fixture +def initalized_notification(client_notifications: list[dict], client_messages: list[dict]) -> dict: + """ + Defines an `initalized` LSP notification from client to server. + """ + message = { + "jsonrpc": "2.0", + "method": "initialized" + } + client_notifications.append(message) + client_messages.append(message) + return message + +@pytest.fixture +def shutdown_request(client_requests: list[dict], client_messages: list[dict]) -> dict: + """ + Defines an `shutdown` LSP request from client to server. + """ + message = { + "jsonrpc": "2.0", + "id": len(client_requests), + "method": "shutdown" + } + client_requests.append(message) + client_messages.append(message) + return message + +@pytest.fixture +def exit_notification(client_notifications: list[dict], client_messages: list[dict]) -> dict: + """ + Defines an `exit` LSP notification from client to server. + """ + message = { + "jsonrpc": "2.0", + "method": "exit" + } + client_notifications.append(message) + client_messages.append(message) + return message + +@pytest.fixture +def invalid_request_response(server_responses: list[dict]) -> dict: + """ + Defines a template "invalid request" response. Field "message" and "id" must be filled in when + appropriate. + """ + message = { + "jsonrpc": "2.0", + "error": { + "code": ErrorCodes.InvalidRequest, + "message": "Must wait for `initalized` notification before other requests." + } + } + server_responses.append(message) + return message + +@pytest.fixture +def non_initialized_invalid_request_response(server_responses: list[dict], + invalid_request_response: dict) -> None: + """ + Defines a invalid request response for the case of a non-initalized server. + """ + message = "Must wait for `initalized` notification before other requests." + server_responses[-1]["error"]["message"] = message + +@pytest.fixture +def client_rpc_messages(client_messages: list[dict]) -> BytesIO: + """ + Builds the list of client messages into JSON RPC message stream. + """ + return build_rpc_message_stream(client_messages) + +@pytest.fixture +def server_rpc_messages(server_messages: list[dict]) -> BytesIO: + """ + Builds the list of server messages into JSON RPC message stream. + """ + return build_rpc_message_stream(server_messages, insert_header=CONTENT_TYPE_HEADER) diff --git a/tests/util.py b/tests/util.py index 5cef586..0ab7a89 100644 --- a/tests/util.py +++ b/tests/util.py @@ -11,6 +11,16 @@ from malls.mal_lsp import MALLSPServer +def find_last_request(requests: list[dict], + condition: typing.Callable[[dict], bool], + default = None): + """ + Searches through the list of requests in reverse order and returns first (logically last) + element fulfilling the condition. + + If none are found an error is raised unless a default is provided, which is returned instead. + """ + return next(filter(condition, reversed(requests)), default) def fixture_name_from_file( file_name: str | Path, @@ -98,6 +108,44 @@ def template() -> typing.BinaryIO: # Bind `fixture` as `fixture_name` inside this module so it gets exported setattr(module, fixture_name, fixture) +CONTENT_TYPE_HEADER = b"Content-Type: application/vscode-jsonrpc; charset=utf8" + +def build_rpc_message_stream( + messages: list[dict], + insert_header: typing.Callable[[dict, list[dict]], bytes | str] | bytes | str | None = None + ) -> io.BytesIO: + buffer = io.BytesIO() + for message in messages: + # get the length of the payload (+1 for the newline) + json_string = json.dumps(message, ensure_ascii=False, separators=(",", ":")) + json_payload = json_string.encode("utf-8") + payload_size = str(len(json_payload)) + + # write payload size + buffer.write(b"Content-Length: ") + buffer.write(payload_size.encode()) + + # Handle the setting of insert_header (fn, str, or bytes) + if insert_header is not None: + # Put header on new line + buffer.write(b"\r\n") + header = insert_header + # If insert_header is a callback function, evaluate it for the current + # message and total list of messages + if callable(insert_header): + header = insert_header(message, messages) + # Encode strings into bytes + if isinstance(insert_header, str): + header = insert_header.encode("utf-8") + # Insert header + buffer.write(header) + + # Write header separator and payload + buffer.write(b"\r\n\r\n") + buffer.write(json_payload) + buffer.seek(0) + return buffer + def build_payload(to_include: list): result = b"" for payload in to_include: From fa8b39a7e270bf56e6160a1d68d627be37da816e Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Fri, 22 Aug 2025 12:05:57 +0200 Subject: [PATCH 04/42] test: Use LSP fixture components for encoding_capability_check --- .../fixtures/encoding_capability_check.in.lsp | 15 ---------- .../fixtures/lsp/encoding_capability_check.py | 30 +++++++++++++++++++ tests/integration/test_encoding_capability.py | 6 ++-- 3 files changed, 34 insertions(+), 17 deletions(-) delete mode 100644 tests/fixtures/encoding_capability_check.in.lsp create mode 100644 tests/fixtures/lsp/encoding_capability_check.py diff --git a/tests/fixtures/encoding_capability_check.in.lsp b/tests/fixtures/encoding_capability_check.in.lsp deleted file mode 100644 index 73d503b..0000000 --- a/tests/fixtures/encoding_capability_check.in.lsp +++ /dev/null @@ -1,15 +0,0 @@ -Content-Length: 89 - -{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"trace":"off","capabilities":{}}} -Content-Length: 41 - -{"jsonrpc":"2.0","method":"initialized"} -Content-Length: 71 - -{"jsonrpc": "2.0","method":"$/setTrace","params":{"value":"messages"}} -Content-Length: 45 - -{"jsonrpc":"2.0","id":2,"method":"shutdown"} -Content-Length: 34 - -{"jsonrpc":"2.0","method":"exit"} diff --git a/tests/fixtures/lsp/encoding_capability_check.py b/tests/fixtures/lsp/encoding_capability_check.py new file mode 100644 index 0000000..2b373b8 --- /dev/null +++ b/tests/fixtures/lsp/encoding_capability_check.py @@ -0,0 +1,30 @@ +import io + +import pytest + + +@pytest.fixture +def set_trace_notification(client_notifications: list[dict], client_messages: list[dict]) -> dict: + message = { + "jsonrpc": "2.0", + "method": "$/setTrace", + "params": { + "value": "messages" + } + } + client_notifications.append(message) + client_messages.append(message) + return message + +@pytest.fixture +def encoding_capability_client_messages( + client_requests: list[dict], + client_notifications: list[dict], + client_messages: list[dict], + initalize_request, + initalized_notification, + set_trace_notification, + shutdown_request, + exit_notification, + client_rpc_messages: io.BytesIO) -> io.BytesIO: + return client_rpc_messages diff --git a/tests/integration/test_encoding_capability.py b/tests/integration/test_encoding_capability.py index 0c5f236..dde9cfd 100644 --- a/tests/integration/test_encoding_capability.py +++ b/tests/integration/test_encoding_capability.py @@ -4,9 +4,11 @@ from ..util import get_lsp_json, server_output +# Import fixtures since they're lying in nested sibling directory +pytest_plugins = ["tests.fixtures.lsp.encoding_capability_check"] -def test_encoding_capability_simple(encoding_capability_check_in: typing.BinaryIO): - output, ls, *_ = server_output(encoding_capability_check_in) +def test_encoding_capability_simple(encoding_capability_client_messages: typing.BinaryIO): + output, ls, *_ = server_output(encoding_capability_client_messages) # the test and server share the same buffer, # so we must reset the cursor From 97b90f47d3e71dc06ed8441499efd5cf66f55681 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:36:31 +0200 Subject: [PATCH 05/42] refactor: Minimize fixture dependency --- tests/fixtures/lsp/encoding_capability_check.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/fixtures/lsp/encoding_capability_check.py b/tests/fixtures/lsp/encoding_capability_check.py index 2b373b8..f31a678 100644 --- a/tests/fixtures/lsp/encoding_capability_check.py +++ b/tests/fixtures/lsp/encoding_capability_check.py @@ -18,9 +18,6 @@ def set_trace_notification(client_notifications: list[dict], client_messages: li @pytest.fixture def encoding_capability_client_messages( - client_requests: list[dict], - client_notifications: list[dict], - client_messages: list[dict], initalize_request, initalized_notification, set_trace_notification, From 086e991ba4cd95e9e1a43b55d132ee1c9c7b9c89 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:56:53 +0200 Subject: [PATCH 06/42] test: Use LSP fixture components for base_protocol --- tests/fixtures/init_exit.in.lsp | 8 -- tests/fixtures/init_exit.out.lsp | 7 -- tests/fixtures/lsp/base_protocol.py | 92 +++++++++++++++++++ tests/fixtures/pre_initialized_exit.in.lsp | 6 -- tests/fixtures/pre_initialized_exit.out.lsp | 7 -- .../fixtures/pre_initialized_shutdown.in.lsp | 8 -- .../fixtures/pre_initialized_shutdown.out.lsp | 7 -- tests/integration/test_base_protocol.py | 68 +++----------- 8 files changed, 107 insertions(+), 96 deletions(-) delete mode 100644 tests/fixtures/init_exit.in.lsp delete mode 100644 tests/fixtures/init_exit.out.lsp create mode 100644 tests/fixtures/lsp/base_protocol.py delete mode 100644 tests/fixtures/pre_initialized_exit.in.lsp delete mode 100644 tests/fixtures/pre_initialized_exit.out.lsp delete mode 100644 tests/fixtures/pre_initialized_shutdown.in.lsp delete mode 100644 tests/fixtures/pre_initialized_shutdown.out.lsp diff --git a/tests/fixtures/init_exit.in.lsp b/tests/fixtures/init_exit.in.lsp deleted file mode 100644 index cc9a13e..0000000 --- a/tests/fixtures/init_exit.in.lsp +++ /dev/null @@ -1,8 +0,0 @@ -Content-Length: 76 -Content-Type: application/vscode-jsonrpc; charset=utf8 - -{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{}}} -Content-Length: 41 -Content-Type: application/vscode-jsonrpc; charset=utf8 - -{"jsonrpc":"2.0","id":2,"method":"exit"} diff --git a/tests/fixtures/init_exit.out.lsp b/tests/fixtures/init_exit.out.lsp deleted file mode 100644 index 77f5141..0000000 --- a/tests/fixtures/init_exit.out.lsp +++ /dev/null @@ -1,7 +0,0 @@ -Content-Length: 210 -Content-Type: application/vscode-jsonrpc; charset=utf8 - -{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"positionEncoding":"utf-16","textDocumentSync":{"openClose":true,"change":1},"definitionProvider":true,"completionProvider":{}},"serverInfo":{"name":"mal-ls"}}}Content-Length: 123 -Content-Type: application/vscode-jsonrpc; charset=utf8 - -{"jsonrpc":"2.0","id":2,"error":{"code":-32600,"message":"Must wait for `initalized` notification before other requests."}} diff --git a/tests/fixtures/lsp/base_protocol.py b/tests/fixtures/lsp/base_protocol.py new file mode 100644 index 0000000..ea102d9 --- /dev/null +++ b/tests/fixtures/lsp/base_protocol.py @@ -0,0 +1,92 @@ +from io import BytesIO + +import pytest + + +@pytest.fixture +def init_exit_expected_exchange( + initalize_request, + initalize_response, + exit_notification, + non_initialized_invalid_request_response) -> None: + """ + client server + -------------------------- + initialize + initalize_response + exit + invalid request + """ + pass + +@pytest.fixture +def init_exit_client_messages( + init_exit_expected_exchange, + client_rpc_messages: BytesIO) -> BytesIO: + """ + client server + -------------------------- + initialize + (initalize_response) + exit + (invalid request) + """ + return client_rpc_messages + +@pytest.fixture +def init_exit_server_messages( + init_exit_expected_exchange, + server_rpc_messages: BytesIO) -> BytesIO: + """ + client server + ----------------------------------- + (initialize) + initalize_response + (exit) + invalid request + """ + return server_rpc_messages + +@pytest.fixture +def init_shutdown_expected_exchange( + initalize_request, + initalize_response, + shutdown_request, + non_initialized_invalid_request_response) -> None: + """ + client server + -------------------------- + initialize + initalize_response + shutdown + invalid request + """ + pass + +@pytest.fixture +def init_shutdown_client_messages( + init_shutdown_expected_exchange, + client_rpc_messages: BytesIO) -> BytesIO: + """ + client server + -------------------------- + initialize + (initalize_response) + shutdown + (invalid request) + """ + return client_rpc_messages + +@pytest.fixture +def init_shutdown_server_messages( + init_shutdown_expected_exchange, + server_rpc_messages: BytesIO) -> BytesIO: + """ + client server + ----------------------------------- + (initialize) + initalize_response + (shutdown) + invalid request + """ + return server_rpc_messages diff --git a/tests/fixtures/pre_initialized_exit.in.lsp b/tests/fixtures/pre_initialized_exit.in.lsp deleted file mode 100644 index a9e238c..0000000 --- a/tests/fixtures/pre_initialized_exit.in.lsp +++ /dev/null @@ -1,6 +0,0 @@ -Content-Length: 47 - -{"jsonrpc":"2.0","id":1,"method":"initialize"} -Content-Length: 41 - -{"jsonrpc":"2.0","id":2,"method":"exit"} diff --git a/tests/fixtures/pre_initialized_exit.out.lsp b/tests/fixtures/pre_initialized_exit.out.lsp deleted file mode 100644 index 74e3fc2..0000000 --- a/tests/fixtures/pre_initialized_exit.out.lsp +++ /dev/null @@ -1,7 +0,0 @@ -Content-Length: 84 -Content-Type: application/vscode-jsonrpc; charset=utf8 - -{"jsonrpc":"2.0","id":1,"result":{"capabilities":{},"serverInfo":{"name":"mal-ls"}}}Content-Length: 123 -Content-Type: application/vscode-jsonrpc; charset=utf8 - -{"jsonrpc":"2.0","id":2,"error":{"code":-32600,"message":"Must wait for `initalized` notification before other requests."}} diff --git a/tests/fixtures/pre_initialized_shutdown.in.lsp b/tests/fixtures/pre_initialized_shutdown.in.lsp deleted file mode 100644 index 22043db..0000000 --- a/tests/fixtures/pre_initialized_shutdown.in.lsp +++ /dev/null @@ -1,8 +0,0 @@ -Content-Length: 76 -Content-Type: application/vscode-jsonrpc; charset=utf8 - -{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{}}} -Content-Length: 45 -Content-Type: application/vscode-jsonrpc; charset=utf8 - -{"jsonrpc":"2.0","id":2,"method":"shutdown"} diff --git a/tests/fixtures/pre_initialized_shutdown.out.lsp b/tests/fixtures/pre_initialized_shutdown.out.lsp deleted file mode 100644 index 74e3fc2..0000000 --- a/tests/fixtures/pre_initialized_shutdown.out.lsp +++ /dev/null @@ -1,7 +0,0 @@ -Content-Length: 84 -Content-Type: application/vscode-jsonrpc; charset=utf8 - -{"jsonrpc":"2.0","id":1,"result":{"capabilities":{},"serverInfo":{"name":"mal-ls"}}}Content-Length: 123 -Content-Type: application/vscode-jsonrpc; charset=utf8 - -{"jsonrpc":"2.0","id":2,"error":{"code":-32600,"message":"Must wait for `initalized` notification before other requests."}} diff --git a/tests/integration/test_base_protocol.py b/tests/integration/test_base_protocol.py index 7c6b3ba..c246469 100644 --- a/tests/integration/test_base_protocol.py +++ b/tests/integration/test_base_protocol.py @@ -5,80 +5,42 @@ from malls.lsp.enums import ErrorCodes from malls.lsp.fsm import LifecycleState -from malls.mal_lsp import MALLSPServer -from ..util import get_lsp_json +from ..util import get_lsp_json, server_output log = logging.getLogger(__name__) -# wait for most 5s (arbitrary) -MAX_TIMEOUT = 2 +pytest_plugins = ["tests.fixtures.lsp.base_protocol"] -class SteppedBytesIO(io.BytesIO): - """ - SteppedBytesIO provide a way to stop the closing of the IO N-1 times, closing on the Nth time. - """ +def test_correct_base_lifecycle( + init_exit_client_messages: typing.BinaryIO, + init_exit_server_messages: typing.BinaryIO): + output, *_ = server_output(init_exit_client_messages) - def __init__(self, initial_bytes: bytes = b"", steps: int = 1): - self.steps = steps - - def close(self): - if self.steps <= 0: - super(io.BytesIO, self).close() - else: - self.steps -= 1 - - -# https://github.com/python-lsp/python-lsp-server/blob/develop/pylsp/python_lsp.py#L58 -def server_output( - input: typing.BinaryIO, timeout: float | None = MAX_TIMEOUT -) -> typing.Tuple[typing.BinaryIO, MALLSPServer, TimeoutError | None]: - intermediary = SteppedBytesIO() - ls = MALLSPServer(input, intermediary) - time_out_err = None - - async def run_server(): - ls.start() - - try: - server_future = run_server() - asyncio.run(asyncio.wait_for(server_future, 1)) - except TimeoutError as e: - ls.m_exit() - time_out_err = e - except Exception as e: - intermediary.close() - raise e - - return intermediary, ls, time_out_err - - -def test_correct_base_lifecycle(init_exit_in: typing.BinaryIO, init_exit_out: typing.BinaryIO): - output, *_ = server_output(init_exit_in) - - assert output.getvalue() == init_exit_out.read().strip() + assert output.getvalue() == init_exit_server_messages.read() output.close() -def test_pre_initialized_exit_does_not_change_state(pre_initialized_exit_in: typing.BinaryIO): - output, ls, *_ = server_output(pre_initialized_exit_in) +def test_pre_initialized_exit_does_not_change_state( + init_exit_client_messages: typing.BinaryIO): + output, ls, *_ = server_output(init_exit_client_messages) assert ls.state.current_state == LifecycleState.INITIALIZE output.close() def test_pre_initialized_shutdown_does_not_change_state( - pre_initialized_shutdown_in: typing.BinaryIO, -): - output, ls, *_ = server_output(pre_initialized_shutdown_in) + init_shutdown_client_messages: typing.BinaryIO): + output, ls, *_ = server_output(init_shutdown_client_messages) assert ls.state.current_state == LifecycleState.INITIALIZE output.close() -def test_pre_initialized_shutdown_errs(pre_initialized_shutdown_in: typing.BinaryIO): - output, ls, *_ = server_output(pre_initialized_shutdown_in) +def test_pre_initialized_shutdown_errs( + init_shutdown_client_messages: typing.BinaryIO): + output, ls, *_ = server_output(init_shutdown_client_messages) # the test and server share the same buffer, # so we must reset the cursor From 765e9c4aee4837c591ae0d91979656668e6f3fd5 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:57:38 +0200 Subject: [PATCH 07/42] chore: format and lint --- tests/conftest.py | 113 +++++++++--------- tests/fixtures/lsp/base_protocol.py | 39 +++--- .../fixtures/lsp/encoding_capability_check.py | 22 ++-- tests/integration/test_base_protocol.py | 15 +-- tests/integration/test_encoding_capability.py | 1 + tests/test_utils.py | 4 + tests/util.py | 52 ++++---- 7 files changed, 127 insertions(+), 119 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2df2fee..baa6e99 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,15 +1,14 @@ -import json import logging import os import sys import typing from io import BytesIO -from malls.lsp.enums import ErrorCodes # from ..src.malls.lsp.enums import ErrorCodes - import pytest +from malls.lsp.enums import ErrorCodes + from .util import ( BASE_OPEN_FILE, CHANGE_FILE_1, @@ -19,6 +18,7 @@ CHANGE_FILE_5, CHANGE_FILE_WITH_ERROR, COMPLETION_PAYLOADS, + CONTENT_TYPE_HEADER, GOTO_DEFINITION_PAYLOADS, OPEN_FILE_WITH_ERROR, OPEN_FILE_WITH_FAKE_INCLUDE, @@ -28,7 +28,6 @@ build_payload, build_rpc_message_stream, find_last_request, - CONTENT_TYPE_HEADER ) logging.getLogger().setLevel(logging.DEBUG) @@ -116,6 +115,7 @@ def template() -> typing.BinaryIO: # Bind `fixture` as `fixture_name` inside this module so it gets exported setattr(module, fixture_name, fixture) + @pytest.fixture def client_requests() -> list[dict]: """ @@ -125,6 +125,7 @@ def client_requests() -> list[dict]: """ return [] + @pytest.fixture def client_notifications() -> list[dict]: """ @@ -134,6 +135,7 @@ def client_notifications() -> list[dict]: """ return [] + @pytest.fixture def client_responses() -> list[dict]: """ @@ -143,6 +145,7 @@ def client_responses() -> list[dict]: """ return [] + @pytest.fixture def client_messages() -> list[dict]: """ @@ -152,6 +155,7 @@ def client_messages() -> list[dict]: """ return [] + @pytest.fixture def server_requests() -> list[dict]: """ @@ -161,6 +165,7 @@ def server_requests() -> list[dict]: """ return [] + @pytest.fixture def server_notifications() -> list[dict]: """ @@ -170,6 +175,7 @@ def server_notifications() -> list[dict]: """ return [] + @pytest.fixture def server_responses() -> list[dict]: """ @@ -179,6 +185,7 @@ def server_responses() -> list[dict]: """ return [] + @pytest.fixture def server_messages() -> list[dict]: """ @@ -188,98 +195,86 @@ def server_messages() -> list[dict]: """ return [] + @pytest.fixture def initalize_request(client_requests: list[dict], client_messages: list[dict]) -> dict: """ Defines an `initalize` LSP request from client to server. """ message = { - "jsonrpc": "2.0", - "id": len(client_requests), - "method": "initialize", - "params": { - "trace": "off", - "capabilities": {} - } - } + "jsonrpc": "2.0", + "id": len(client_requests), + "method": "initialize", + "params": {"trace": "off", "capabilities": {}}, + } client_requests.append(message) client_messages.append(message) return message + @pytest.fixture -def initalize_response(client_requests: list[dict], - server_responses: list[dict], - server_messages: list[dict]) -> dict: +def initalize_response( + client_requests: list[dict], server_responses: list[dict], server_messages: list[dict] +) -> dict: """ Creates a default response to the latest (id) `initalize` request from a client. Defaults to ID 0. """ message = { - "jsonrpc": "2.0", - "id": find_last_request(client_requests, - lambda request: request.get("method") == "initalize", - {}).get("id", 0), - "result": { - # TODO: Replace with values from an actual server instance (e.g. via instance.capabilities()) - "capabilities": { - "positionEncoding": "utf-16", - "textDocumentSync": { - "openClose": True, - "change":1 - }, - "definitionProvider": True, - "completionProvider": {} - }, - # TODO: Replace with values from an actual server instance (e.g. via instance.server_info()) - "serverInfo": { - "name": "mal-ls" - } - } - } + "jsonrpc": "2.0", + "id": find_last_request( + client_requests, lambda request: request.get("method") == "initalize", {} + ).get("id", 0), + "result": { + # TODO: Replace with values from an actual server instance (e.g. via instance.capabilities()) + "capabilities": { + "positionEncoding": "utf-16", + "textDocumentSync": {"openClose": True, "change": 1}, + "definitionProvider": True, + "completionProvider": {}, + }, + # TODO: Replace with values from an actual server instance (e.g. via instance.server_info()) + "serverInfo": {"name": "mal-ls"}, + }, + } server_responses.append(message) server_messages.append(message) return message + @pytest.fixture def initalized_notification(client_notifications: list[dict], client_messages: list[dict]) -> dict: """ Defines an `initalized` LSP notification from client to server. """ - message = { - "jsonrpc": "2.0", - "method": "initialized" - } + message = {"jsonrpc": "2.0", "method": "initialized"} client_notifications.append(message) client_messages.append(message) return message + @pytest.fixture def shutdown_request(client_requests: list[dict], client_messages: list[dict]) -> dict: """ Defines an `shutdown` LSP request from client to server. """ - message = { - "jsonrpc": "2.0", - "id": len(client_requests), - "method": "shutdown" - } + message = {"jsonrpc": "2.0", "id": len(client_requests), "method": "shutdown"} client_requests.append(message) client_messages.append(message) return message + @pytest.fixture def exit_notification(client_notifications: list[dict], client_messages: list[dict]) -> dict: """ Defines an `exit` LSP notification from client to server. """ - message = { - "jsonrpc": "2.0", - "method": "exit" - } + message = {"jsonrpc": "2.0", "method": "exit"} client_notifications.append(message) client_messages.append(message) return message + @pytest.fixture def invalid_request_response(server_responses: list[dict]) -> dict: """ @@ -287,24 +282,27 @@ def invalid_request_response(server_responses: list[dict]) -> dict: appropriate. """ message = { - "jsonrpc": "2.0", - "error": { - "code": ErrorCodes.InvalidRequest, - "message": "Must wait for `initalized` notification before other requests." - } - } + "jsonrpc": "2.0", + "error": { + "code": ErrorCodes.InvalidRequest, + "message": "Must wait for `initalized` notification before other requests.", + }, + } server_responses.append(message) return message + @pytest.fixture -def non_initialized_invalid_request_response(server_responses: list[dict], - invalid_request_response: dict) -> None: +def non_initialized_invalid_request_response( + server_responses: list[dict], invalid_request_response: dict +) -> None: """ Defines a invalid request response for the case of a non-initalized server. """ message = "Must wait for `initalized` notification before other requests." server_responses[-1]["error"]["message"] = message + @pytest.fixture def client_rpc_messages(client_messages: list[dict]) -> BytesIO: """ @@ -312,6 +310,7 @@ def client_rpc_messages(client_messages: list[dict]) -> BytesIO: """ return build_rpc_message_stream(client_messages) + @pytest.fixture def server_rpc_messages(server_messages: list[dict]) -> BytesIO: """ diff --git a/tests/fixtures/lsp/base_protocol.py b/tests/fixtures/lsp/base_protocol.py index ea102d9..4160832 100644 --- a/tests/fixtures/lsp/base_protocol.py +++ b/tests/fixtures/lsp/base_protocol.py @@ -5,10 +5,11 @@ @pytest.fixture def init_exit_expected_exchange( - initalize_request, - initalize_response, - exit_notification, - non_initialized_invalid_request_response) -> None: + initalize_request, + initalize_response, + exit_notification, + non_initialized_invalid_request_response, +) -> None: """ client server -------------------------- @@ -19,10 +20,9 @@ def init_exit_expected_exchange( """ pass + @pytest.fixture -def init_exit_client_messages( - init_exit_expected_exchange, - client_rpc_messages: BytesIO) -> BytesIO: +def init_exit_client_messages(init_exit_expected_exchange, client_rpc_messages: BytesIO) -> BytesIO: """ client server -------------------------- @@ -33,10 +33,9 @@ def init_exit_client_messages( """ return client_rpc_messages + @pytest.fixture -def init_exit_server_messages( - init_exit_expected_exchange, - server_rpc_messages: BytesIO) -> BytesIO: +def init_exit_server_messages(init_exit_expected_exchange, server_rpc_messages: BytesIO) -> BytesIO: """ client server ----------------------------------- @@ -47,12 +46,14 @@ def init_exit_server_messages( """ return server_rpc_messages + @pytest.fixture def init_shutdown_expected_exchange( - initalize_request, - initalize_response, - shutdown_request, - non_initialized_invalid_request_response) -> None: + initalize_request, + initalize_response, + shutdown_request, + non_initialized_invalid_request_response, +) -> None: """ client server -------------------------- @@ -63,10 +64,11 @@ def init_shutdown_expected_exchange( """ pass + @pytest.fixture def init_shutdown_client_messages( - init_shutdown_expected_exchange, - client_rpc_messages: BytesIO) -> BytesIO: + init_shutdown_expected_exchange, client_rpc_messages: BytesIO +) -> BytesIO: """ client server -------------------------- @@ -77,10 +79,11 @@ def init_shutdown_client_messages( """ return client_rpc_messages + @pytest.fixture def init_shutdown_server_messages( - init_shutdown_expected_exchange, - server_rpc_messages: BytesIO) -> BytesIO: + init_shutdown_expected_exchange, server_rpc_messages: BytesIO +) -> BytesIO: """ client server ----------------------------------- diff --git a/tests/fixtures/lsp/encoding_capability_check.py b/tests/fixtures/lsp/encoding_capability_check.py index f31a678..0ff18e2 100644 --- a/tests/fixtures/lsp/encoding_capability_check.py +++ b/tests/fixtures/lsp/encoding_capability_check.py @@ -5,23 +5,19 @@ @pytest.fixture def set_trace_notification(client_notifications: list[dict], client_messages: list[dict]) -> dict: - message = { - "jsonrpc": "2.0", - "method": "$/setTrace", - "params": { - "value": "messages" - } - } + message = {"jsonrpc": "2.0", "method": "$/setTrace", "params": {"value": "messages"}} client_notifications.append(message) client_messages.append(message) return message + @pytest.fixture def encoding_capability_client_messages( - initalize_request, - initalized_notification, - set_trace_notification, - shutdown_request, - exit_notification, - client_rpc_messages: io.BytesIO) -> io.BytesIO: + initalize_request, + initalized_notification, + set_trace_notification, + shutdown_request, + exit_notification, + client_rpc_messages: io.BytesIO, +) -> io.BytesIO: return client_rpc_messages diff --git a/tests/integration/test_base_protocol.py b/tests/integration/test_base_protocol.py index c246469..b028b28 100644 --- a/tests/integration/test_base_protocol.py +++ b/tests/integration/test_base_protocol.py @@ -1,5 +1,3 @@ -import asyncio -import io import logging import typing @@ -14,16 +12,15 @@ def test_correct_base_lifecycle( - init_exit_client_messages: typing.BinaryIO, - init_exit_server_messages: typing.BinaryIO): + init_exit_client_messages: typing.BinaryIO, init_exit_server_messages: typing.BinaryIO +): output, *_ = server_output(init_exit_client_messages) assert output.getvalue() == init_exit_server_messages.read() output.close() -def test_pre_initialized_exit_does_not_change_state( - init_exit_client_messages: typing.BinaryIO): +def test_pre_initialized_exit_does_not_change_state(init_exit_client_messages: typing.BinaryIO): output, ls, *_ = server_output(init_exit_client_messages) assert ls.state.current_state == LifecycleState.INITIALIZE @@ -31,15 +28,15 @@ def test_pre_initialized_exit_does_not_change_state( def test_pre_initialized_shutdown_does_not_change_state( - init_shutdown_client_messages: typing.BinaryIO): + init_shutdown_client_messages: typing.BinaryIO, +): output, ls, *_ = server_output(init_shutdown_client_messages) assert ls.state.current_state == LifecycleState.INITIALIZE output.close() -def test_pre_initialized_shutdown_errs( - init_shutdown_client_messages: typing.BinaryIO): +def test_pre_initialized_shutdown_errs(init_shutdown_client_messages: typing.BinaryIO): output, ls, *_ = server_output(init_shutdown_client_messages) # the test and server share the same buffer, diff --git a/tests/integration/test_encoding_capability.py b/tests/integration/test_encoding_capability.py index dde9cfd..cb6778c 100644 --- a/tests/integration/test_encoding_capability.py +++ b/tests/integration/test_encoding_capability.py @@ -7,6 +7,7 @@ # Import fixtures since they're lying in nested sibling directory pytest_plugins = ["tests.fixtures.lsp.encoding_capability_check"] + def test_encoding_capability_simple(encoding_capability_client_messages: typing.BinaryIO): output, ls, *_ = server_output(encoding_capability_client_messages) diff --git a/tests/test_utils.py b/tests/test_utils.py index 5f068ff..26f348e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -10,21 +10,25 @@ def test_nominal_fixture_naming(): name = fixture_name_from_file(path) assert name == "path_to_fixture" + def test_fixture_naming_ignore_prefix(): path = "path/to/fixture.file" name = fixture_name_from_file(path, root_folder="path/to") assert name == "fixture" + def test_fixture_naming_ignore_prefix_middle_of_path(): path = "path/to/fixture.file" name = fixture_name_from_file(path, root_folder="to") assert name == "fixture" + def test_fixture_naming_rename_extension(): path = "path/to/fixture.file" name = fixture_name_from_file(path, extension_renaming=lambda x: x) assert name == "path_to_fixture_file" + def test_fixture_naming_rename_extension_with_ignore_prefix(): path = "path/to/fixture.file" name = fixture_name_from_file(path, extension_renaming=lambda x: x, root_folder="path/to") diff --git a/tests/util.py b/tests/util.py index 0ab7a89..242110a 100644 --- a/tests/util.py +++ b/tests/util.py @@ -11,9 +11,8 @@ from malls.mal_lsp import MALLSPServer -def find_last_request(requests: list[dict], - condition: typing.Callable[[dict], bool], - default = None): + +def find_last_request(requests: list[dict], condition: typing.Callable[[dict], bool], default=None): """ Searches through the list of requests in reverse order and returns first (logically last) element fulfilling the condition. @@ -22,10 +21,12 @@ def find_last_request(requests: list[dict], """ return next(filter(condition, reversed(requests)), default) + def fixture_name_from_file( - file_name: str | Path, - extension_renaming: typing.Callable[[str], str] | dict[str, str] | None = None, - root_folder: str | None = None) -> str: + file_name: str | Path, + extension_renaming: typing.Callable[[str], str] | dict[str, str] | None = None, + root_folder: str | None = None, +) -> str: """ Compute the name of a fixture based on its file path. @@ -40,11 +41,14 @@ def fixture_name_from_file( """ # Default extension naming is none if extension_renaming is None: + def extension_renaming(x: str): return "" + # If a dictionary was provided, alias it as a function call to make it consistent with function # usage. If key/extension not present, default to no extension naming. if extension_renaming is dict: + def extension_renaming(x: str): return extension_renaming.get(x, "") @@ -56,29 +60,30 @@ def extension_renaming(x: str): # (on top of subsequent slash) if root_folder: post_prefix_index = file_name.find(root_folder) - file_name = file_name[post_prefix_index + len(root_folder) + 1:] + file_name = file_name[post_prefix_index + len(root_folder) + 1 :] extension_dot_index = file_name.rindex(".") - extension = file_name[extension_dot_index + 1:] + extension = file_name[extension_dot_index + 1 :] # Remove extension, e.g: .http/.lsp/.mal - fixture_name = file_name[: extension_dot_index] + fixture_name = file_name[:extension_dot_index] # Replace dots with underscore, e.g: empty.out -> empty_out fixture_name = fixture_name.replace(".", "_") # Add subdirectory path as prefix if there was one # Replace directory delimiters with underscores fixture_name = fixture_name.replace("/", "_").replace("\\", "_") # Add possible extension name - if (extension := extension_renaming(extension)): + if extension := extension_renaming(extension): fixture_name += "_" + extension return fixture_name def load_fixture_file_into_module( - path: str | Path, - module, - extension_renaming: typing.Callable[[str], str] | dict[str, str] | None = None, - root_folder: str | None = None) -> None: + path: str | Path, + module, + extension_renaming: typing.Callable[[str], str] | dict[str, str] | None = None, + root_folder: str | None = None, +) -> None: """ Load the raw contents of a file as a fixture into the provided module. @@ -97,9 +102,9 @@ def template() -> typing.BinaryIO: open_fixture_file.__doc__ = open.__doc__ - fixture_name = fixture_name_from_file(path, - extension_renaming=extension_renaming, - root_folder=root_folder) + fixture_name = fixture_name_from_file( + path, extension_renaming=extension_renaming, root_folder=root_folder + ) fixture = pytest.fixture( open_fixture_file(path), @@ -108,12 +113,14 @@ def template() -> typing.BinaryIO: # Bind `fixture` as `fixture_name` inside this module so it gets exported setattr(module, fixture_name, fixture) + CONTENT_TYPE_HEADER = b"Content-Type: application/vscode-jsonrpc; charset=utf8" + def build_rpc_message_stream( - messages: list[dict], - insert_header: typing.Callable[[dict, list[dict]], bytes | str] | bytes | str | None = None - ) -> io.BytesIO: + messages: list[dict], + insert_header: typing.Callable[[dict, list[dict]], bytes | str] | bytes | str | None = None, +) -> io.BytesIO: buffer = io.BytesIO() for message in messages: # get the length of the payload (+1 for the newline) @@ -139,13 +146,14 @@ def build_rpc_message_stream( header = insert_header.encode("utf-8") # Insert header buffer.write(header) - + # Write header separator and payload buffer.write(b"\r\n\r\n") buffer.write(json_payload) buffer.seek(0) return buffer + def build_payload(to_include: list): result = b"" for payload in to_include: @@ -272,7 +280,7 @@ async def run_server(): "method": "textDocument/didOpen", "params": { "textDocument": { - "uri": main_file_path, + "uri": main_file_path, "languageId": "mal", "version": 0, "text": '#id: "org.mal-lang.testAnalyzer"\n#version:"0.0.0"\n\ncategory ' From fc4e7525c3453db93a46fc2a990440229d068502 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:01:19 +0200 Subject: [PATCH 08/42] docs: Add fixture doc comment for encoding capability --- tests/fixtures/lsp/encoding_capability_check.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/fixtures/lsp/encoding_capability_check.py b/tests/fixtures/lsp/encoding_capability_check.py index 0ff18e2..b36654a 100644 --- a/tests/fixtures/lsp/encoding_capability_check.py +++ b/tests/fixtures/lsp/encoding_capability_check.py @@ -20,4 +20,13 @@ def encoding_capability_client_messages( exit_notification, client_rpc_messages: io.BytesIO, ) -> io.BytesIO: + """ + client server + -------------------------- + initialize + initalized + $/setTrace + shutdown + exit + """ return client_rpc_messages From 04b8072e3382e9ec75e23ae69ed13b95f4af660b Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Sat, 23 Aug 2025 11:44:36 +0200 Subject: [PATCH 09/42] refactor: Split load fixture into module to load fixture function --- tests/util.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/tests/util.py b/tests/util.py index 242110a..96fc479 100644 --- a/tests/util.py +++ b/tests/util.py @@ -78,14 +78,13 @@ def extension_renaming(x: str): return fixture_name -def load_fixture_file_into_module( +def load_file_as_fixture( path: str | Path, - module, extension_renaming: typing.Callable[[str], str] | dict[str, str] | None = None, root_folder: str | None = None, -) -> None: +) -> (typing.Callable, str): """ - Load the raw contents of a file as a fixture into the provided module. + Load the raw contents of a file as a fixture and returns it alongside its name. Shares options with `fixture_name_from_file`. """ @@ -110,7 +109,24 @@ def template() -> typing.BinaryIO: open_fixture_file(path), name=fixture_name, ) - # Bind `fixture` as `fixture_name` inside this module so it gets exported + + return fixture, fixture_name + + +def load_fixture_file_into_module( + path: str | Path, + module, + extension_renaming: typing.Callable[[str], str] | dict[str, str] | None = None, + root_folder: str | None = None, +) -> None: + """ + Load the raw contents of a file as a fixture into the provided module. + + Shares options with `fixture_name_from_file`. + """ + fixture, fixture_name = load_file_as_fixture(path, + extension_renaming=extension_renaming, + root_folder=root_folder) setattr(module, fixture_name, fixture) From 5c31db0a14d8d054f488c268e16663e396d2e58e Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Sun, 24 Aug 2025 00:09:57 +0200 Subject: [PATCH 10/42] feat: Test util func to load dir files and uri's as fixtures --- tests/util.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/tests/util.py b/tests/util.py index 96fc479..7ab0b86 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,8 +1,12 @@ import asyncio +import glob import io import json +import os import typing +from os import path from pathlib import Path +import uritools import pytest from pylsp_jsonrpc.endpoint import Endpoint @@ -82,9 +86,10 @@ def load_file_as_fixture( path: str | Path, extension_renaming: typing.Callable[[str], str] | dict[str, str] | None = None, root_folder: str | None = None, -) -> (typing.Callable, str): +) -> (typing.Callable, str, typing.Callable, str): """ - Load the raw contents of a file as a fixture and returns it alongside its name. + Load the raw contents of a file as a fixture and its name, accompanied by uri as fixture and + its name. Shares options with `fixture_name_from_file`. """ @@ -99,6 +104,11 @@ def template() -> typing.BinaryIO: return template + def fixture_uri(file_path: str | Path): + uri = uritools.uricompose(scheme="file", path=file_path) + def template() -> str: + return uri + open_fixture_file.__doc__ = open.__doc__ fixture_name = fixture_name_from_file( @@ -110,7 +120,14 @@ def template() -> typing.BinaryIO: name=fixture_name, ) - return fixture, fixture_name + uri_fixture_name = fixture_name + "_uri" + + uri_fixture = pytest.fixture( + fixture_uri(Path(path).absolute()), + name=uri_fixture_name + ) + + return fixture, fixture_name, uri_fixture, uri_fixture_name def load_fixture_file_into_module( @@ -124,11 +141,45 @@ def load_fixture_file_into_module( Shares options with `fixture_name_from_file`. """ - fixture, fixture_name = load_file_as_fixture(path, - extension_renaming=extension_renaming, - root_folder=root_folder) + fixture, fixture_name, *_ = load_file_as_fixture(path, + extension_renaming=extension_renaming, + root_folder=root_folder) setattr(module, fixture_name, fixture) +# NOTE: On noqa C417 +# Ruff wants to use list generators instead, but that will end up creating many useless +# intermediary lists which is hurtful for performance. +def load_directory_files_as_fixtures( + dir_path: str | Path, + extension: str | None = None, + extension_renaming: typing.Callable[[str], str] | dict[str, str] | None = None) \ + -> [(typing.Callable, str, typing.Callable, str)]: + """ + Loads all file contents in a given directory, aside from .py, and their URI's as fixtures, + using `load_file_as_fixture`. + + Shares option `extension_renaming` with `fixture_name_from_file`. + """ + # Find all files in the directory with the extension, or if none is provided + # all non-python files + if extension: + # Glob find all files matching the extension in the given directory + files = glob.iglob(path.join(dir_path, f"*.{extension}")) + else: + # Filter all entries in the directory to non-python files + def non_python_file(entry: os.DirEntry) -> bool: + return entry.is_file() and not entry.path.endswith(".py") + file_entries = os.scandir(dir_path) + non_python_file_entries = filter(non_python_file, file_entries) + files = map(lambda entry: entry.path, non_python_file_entries) # noqa C417 + + # Concat the file names with the directory to get relative to root path + # then load the file as a fixture, getting the name and absolute path in the process + file_paths = map(lambda file: path.join(dir_path, file), files) # noqa C417 + fixture_name_paths = map(lambda path: load_file_as_fixture(path, root_folder=dir_path), # noqa C417 + file_paths) + return list(fixture_name_paths) + CONTENT_TYPE_HEADER = b"Content-Type: application/vscode-jsonrpc; charset=utf8" From f3a36fb538b55f20c2d8cdeb982adbbf19d52003 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Mon, 25 Aug 2025 10:44:47 +0200 Subject: [PATCH 11/42] feat: Add option to use string/method name for find_last_request --- tests/util.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/util.py b/tests/util.py index 7ab0b86..48cc353 100644 --- a/tests/util.py +++ b/tests/util.py @@ -16,13 +16,21 @@ from malls.mal_lsp import MALLSPServer -def find_last_request(requests: list[dict], condition: typing.Callable[[dict], bool], default=None): +def find_last_request( + requests: list[dict], + condition: typing.Callable[[dict], bool] | str, + default=None) -> dict: """ Searches through the list of requests in reverse order and returns first (logically last) - element fulfilling the condition. + element fulfilling the condition. If condition is a string, then it finds the last request + with the method matching the string. If none are found an error is raised unless a default is provided, which is returned instead. """ + if isinstance(condition, str): + method_name = condition + def condition(request: dict) -> bool: + request.get("method") == method_name return next(filter(condition, reversed(requests)), default) From 5c20caab7c47d2bf203392f75422e5b6999b527e Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:52:15 +0200 Subject: [PATCH 12/42] feat: Create URI fixtures for fixture files --- tests/conftest.py | 97 +++++++++++++++-------------------------------- 1 file changed, 31 insertions(+), 66 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index baa6e99..61f416e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,29 +3,16 @@ import sys import typing from io import BytesIO +from pathlib import Path # from ..src.malls.lsp.enums import ErrorCodes import pytest +import uritools from malls.lsp.enums import ErrorCodes from .util import ( - BASE_OPEN_FILE, - CHANGE_FILE_1, - CHANGE_FILE_2, - CHANGE_FILE_3, - CHANGE_FILE_4, - CHANGE_FILE_5, - CHANGE_FILE_WITH_ERROR, - COMPLETION_PAYLOADS, CONTENT_TYPE_HEADER, - GOTO_DEFINITION_PAYLOADS, - OPEN_FILE_WITH_ERROR, - OPEN_FILE_WITH_FAKE_INCLUDE, - OPEN_FILE_WITH_INCLUDE_WITH_ERROR, - OPEN_FILE_WITH_INCLUDED_FILE, - OPEN_INCLUDED_FILE_WITH_ERROR, - build_payload, build_rpc_message_stream, find_last_request, ) @@ -36,7 +23,11 @@ module = sys.modules[__name__] # Generate pytest fixtures from all fixture files in 'fixtures' and its subdirectories for directory, _, files in os.walk("tests/fixtures"): + if "__pycache__" in directory: + continue for file_name in files: + if file_name.endswith(".py") or file_name == "__pycache__": + continue # Remove extension, e.g: .http/.lsp/.mal fixture_name = file_name[: file_name.rindex(".")] # Replace dots with underscore, e.g: empty.out -> empty_out @@ -53,67 +44,41 @@ # Get full path of file so its usable by `open` file_path = os.path.join(directory, file_name) - def open_fixture_for_writing(file: str, payload: bytes): + def open_fixture_file(file: str): def template() -> typing.BinaryIO: with open(file, "rb") as file_descriptor: - bio = BytesIO(file_descriptor.read()) + yield file_descriptor - bio.seek(0, 2) - bio.write(payload) - bio.seek(0) - return bio + file_name = Path(file).name + template.__doc__ = f"Opens {file_name} in (r)ead (b)inary mode and returns the reader." return template - def open_fixture_file(file: str): - def template() -> typing.BinaryIO: - """Opens a fixture in (r)ead (b)inary mode. See `open` for more details.""" + fixture = pytest.fixture( + open_fixture_file(file_path), + name=fixture_name, + ) + # Bind `fixture` as `fixture_name` inside this module so it gets exported + setattr(module, fixture_name, fixture) - with open(file, "rb") as file_descriptor: - yield file_descriptor + def fixture_uri(file: str): + path = Path(file) + file_path = str(path.resolve()) + uri = uritools.uricompose(scheme="file", path=file_path) + def template() -> str: + return uri + + file_name = path.name + template.__doc__ = f"Returns the URI for {file_name} using file scheme." return template - open_fixture_file.__doc__ = open.__doc__ - - # Define the fixture from `open_file` on `file_path` as `fixture_name` - if directory == "tests/fixtures/writeable_fixtures": - # create different fixtures from the sabe base file - payloads = ( - [ - ([BASE_OPEN_FILE], fixture_name + "_base_open_file"), - ([OPEN_FILE_WITH_INCLUDED_FILE], fixture_name + "_with_included_file"), - ([OPEN_FILE_WITH_FAKE_INCLUDE], fixture_name + "_with_fake_include"), - ([BASE_OPEN_FILE, CHANGE_FILE_1], "change_middle_of_file_single_line"), - ([BASE_OPEN_FILE, CHANGE_FILE_2], "change_middle_of_file_multiple_lines"), - ([BASE_OPEN_FILE, CHANGE_FILE_3], "change_end_of_file"), - ([BASE_OPEN_FILE, CHANGE_FILE_4], "change_middle_of_file_twice"), - ([BASE_OPEN_FILE, CHANGE_FILE_5], "change_whole_file"), - ([OPEN_FILE_WITH_ERROR], "open_file_with_error"), - ([OPEN_FILE_WITH_INCLUDE_WITH_ERROR], "open_file_with_include_error"), - ([BASE_OPEN_FILE, CHANGE_FILE_WITH_ERROR], "change_file_with_error"), - ( - [OPEN_FILE_WITH_INCLUDE_WITH_ERROR, OPEN_INCLUDED_FILE_WITH_ERROR], - "open_file_with_include_error_and_open_file", - ), - ] - + GOTO_DEFINITION_PAYLOADS - + COMPLETION_PAYLOADS - ) - for payload, new_name in payloads: - fixture = pytest.fixture( - open_fixture_for_writing(file_path, build_payload(payload)), - name=new_name, - ) - # Bind `fixture` as `fixture_name` inside this module so it gets exported - setattr(module, new_name, fixture) - else: - fixture = pytest.fixture( - open_fixture_file(file_path), - name=fixture_name, - ) - # Bind `fixture` as `fixture_name` inside this module so it gets exported - setattr(module, fixture_name, fixture) + uri_fixture = pytest.fixture( + fixture_uri(file_path), + name=fixture_name + "_uri", + ) + + setattr(module, fixture_name + "_uri", uri_fixture) @pytest.fixture From f42ff6ad3b4121353be736dc8f2ba426154c5217 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:50:26 +0200 Subject: [PATCH 13/42] test: Use LSP fixture components for test_completion --- tests/fixtures/mal/completion_document.mal | 22 +++++ tests/integration/test_completion.py | 101 ++++++++++++++++++--- 2 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 tests/fixtures/mal/completion_document.mal diff --git a/tests/fixtures/mal/completion_document.mal b/tests/fixtures/mal/completion_document.mal new file mode 100644 index 0000000..f1593cd --- /dev/null +++ b/tests/fixtures/mal/completion_document.mal @@ -0,0 +1,22 @@ +#id: "org.mal-lang.testAnalyzer" +#version:"0.0.0" + +category Example { + + abstract asset Asset1 + { + let var = c + | compromise + -> var.destroy + } + asset Asset2 extends Asset3 + { + | destroy + } +} +associations +{ + Asset1 [a] * <-- L --> * [c] Asset2 + Asset2 [d] 1 <-- M --> 1 [e] Asset2 +} + diff --git a/tests/integration/test_completion.py b/tests/integration/test_completion.py index 068f83d..9f61818 100644 --- a/tests/integration/test_completion.py +++ b/tests/integration/test_completion.py @@ -1,11 +1,13 @@ +import io import logging +import typing from pathlib import Path import pytest import tree_sitter_mal as ts_mal from tree_sitter import Language, Parser -from ..util import get_lsp_json, server_output +from ..util import build_rpc_message_stream, get_lsp_json, server_output log = logging.getLogger(__name__) MAL_LANGUAGE = Language(ts_mal.language()) @@ -53,30 +55,103 @@ ] parameters = [ - ("completion_category", symbols_in_category_hierarchy), - ("completion_associations", symbols_in_associations_hierarchy), - ("completion_asset1", symbols_in_asset1_hierarchy), - ("completion_asset2", symbols_in_asset2_hierarchy), - ("completion_root_node", symbols_in_root_node_hierarchy), + ((0, 4), symbols_in_category_hierarchy), + ((0, 18), symbols_in_associations_hierarchy), + ((0, 7), symbols_in_asset1_hierarchy), + ((0, 13), symbols_in_asset2_hierarchy), + ((0, 0), symbols_in_root_node_hierarchy), ] +parameter_names = [ + "symbols_in_category_hierarchy", + "symbols_in_associations_hierarchy", + "symbols_in_asset1_hierarchy", + "symbols_in_asset2_hierarchy", + "symbols_in_root_node_hierarchy", + ] +pytest_plugins = ["tests.fixtures.mal"] + +@pytest.fixture +def open_completion_document_notification( + client_notifications: list[dict], + client_messages: list[dict], + mal_completion_document: io.BytesIO, + mal_completion_document_uri: str) -> dict: + """ + Sends a didOpen notification bound to the MAL fixture file completion_document. + """ + message = { + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": mal_completion_document_uri, + "languageId": "mal", + "version": 0, + "text": mal_completion_document.read().decode("utf8"), + } + }, + } + client_notifications.append(message) + client_messages.append(message) + return message + +@pytest.fixture +def completion_request( + client_requests: list[dict], + client_messages: list[dict], + mal_completion_document_uri: str) -> typing.Callable[[(int, int)], dict]: + def make(position: (int, int)): + character, line = position + message = { + "id": len(client_requests), + "jsonrpc": "2.0", + "method": "textDocument/completion", + "params": { + "textDocument": { + "uri": mal_completion_document_uri, # find_symbols_in_scope_path + }, + "position": { + "line": line, + "character": character, + }, + }, + } + client_requests.append(message) + client_messages.append(message) + return message + return make + +@pytest.fixture +def completion_client_messages( + client_messages: list[dict], + initalize_request, + initalized_notification, + open_completion_document_notification, + completion_request: typing.Callable[[(int, int)], dict]) -> typing.Callable[[(int, int)], io.BytesIO]: # noqa: E501 + def make(position: (int, int)) -> io.BytesIO: + completion_request(position) + return build_rpc_message_stream(client_messages) + return make @pytest.mark.parametrize( - "fixture_name,completion_list", - [(fixture_name, completion_list) for fixture_name, completion_list in parameters], + "location,completion_list", + parameters, + ids=parameter_names ) -def test_completion(request, fixture_name, completion_list): +def test_completion( + location: (int, int), + completion_list: list[str], + completion_client_messages: typing.Callable[[(int, int)], io.BytesIO]): # send to server - fixture = request.getfixturevalue(fixture_name) + fixture = completion_client_messages(location) output, ls, *_ = server_output(fixture) output.seek(0) response = get_lsp_json(output) response = get_lsp_json(output) - returned_completion_list = [] - for item in response["result"]: - returned_completion_list.append(item["label"]) + returned_completion_list = [completion["label"] for completion in response["result"]] assert set(returned_completion_list) == set(completion_list) From fe2c278a725270961d99d228df20fce8e4c35c49 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:55:16 +0200 Subject: [PATCH 14/42] fix: Use isinstance instead of keyword is --- tests/util.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/util.py b/tests/util.py index 48cc353..09410fe 100644 --- a/tests/util.py +++ b/tests/util.py @@ -6,9 +6,9 @@ import typing from os import path from pathlib import Path -import uritools import pytest +import uritools from pylsp_jsonrpc.endpoint import Endpoint from pylsp_jsonrpc.exceptions import JsonRpcException from pylsp_jsonrpc.streams import JsonRpcStreamReader @@ -65,7 +65,7 @@ def extension_renaming(x: str): return extension_renaming.get(x, "") # Default to handling of strings - if file_name is Path: + if isinstance(file_name, Path): file_name = str(file_name) # If a ignore prefix was given, find it and only care for anything after it @@ -113,12 +113,10 @@ def template() -> typing.BinaryIO: return template def fixture_uri(file_path: str | Path): - uri = uritools.uricompose(scheme="file", path=file_path) + uri = uritools.uricompose(scheme="file", path=str(file_path)) def template() -> str: return uri - open_fixture_file.__doc__ = open.__doc__ - fixture_name = fixture_name_from_file( path, extension_renaming=extension_renaming, root_folder=root_folder ) @@ -168,6 +166,8 @@ def load_directory_files_as_fixtures( Shares option `extension_renaming` with `fixture_name_from_file`. """ + if isinstance(dir_path, Path): + dir_path = str(dir_path.absolute()) # Find all files in the directory with the extension, or if none is provided # all non-python files if extension: From 02c80b3f5af3b9fc4299aec28496feb380df3f42 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:07:39 +0200 Subject: [PATCH 15/42] fix: Add missing quotes around meta tag info --- tests/fixtures/mal/find_symbols_in_scope.mal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fixtures/mal/find_symbols_in_scope.mal b/tests/fixtures/mal/find_symbols_in_scope.mal index 982ee7e..0d421f7 100644 --- a/tests/fixtures/mal/find_symbols_in_scope.mal +++ b/tests/fixtures/mal/find_symbols_in_scope.mal @@ -16,6 +16,6 @@ category Example { } associations { - Asset1 [a] * <-- L --> * [c] Asset2 developer info: some info + Asset1 [a] * <-- L --> * [c] Asset2 developer info: "some info" Asset2 [d] 1 <-- M --> 1 [e] Asset2 } From d3e02d7a7ed7d8654e8502bd4ecd6b06207ca010 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:08:29 +0200 Subject: [PATCH 16/42] feat!: Change URI fixtures to use Path.as_uri Previously used uritools. For consistency, at least for now, its being switched to Path.as_uri. --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 61f416e..708de44 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -63,8 +63,8 @@ def template() -> typing.BinaryIO: def fixture_uri(file: str): path = Path(file) - file_path = str(path.resolve()) - uri = uritools.uricompose(scheme="file", path=file_path) + file_path = path.resolve() + uri = file_path.as_uri() def template() -> str: return uri From adf958627f79d66e063285058d926280ea6ad88d Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:11:58 +0200 Subject: [PATCH 17/42] test: Use LSP component fixtures for test_goto_definition --- tests/integration/test_goto_definition.py | 245 ++++++++++++++++------ 1 file changed, 185 insertions(+), 60 deletions(-) diff --git a/tests/integration/test_goto_definition.py b/tests/integration/test_goto_definition.py index 477081a..bbd3c50 100644 --- a/tests/integration/test_goto_definition.py +++ b/tests/integration/test_goto_definition.py @@ -1,67 +1,181 @@ -import logging -from pathlib import Path +import itertools +import typing import pytest -import tree_sitter_mal as ts_mal -from tree_sitter import Language, Parser -from ..util import get_lsp_json, server_output - -log = logging.getLogger(__name__) -MAL_LANGUAGE = Language(ts_mal.language()) -PARSER = Parser(MAL_LANGUAGE) -FILE_PATH = str(Path(__file__).parent.parent.resolve()) + "/fixtures/mal/" - -simplified_file_path = FILE_PATH + "main.mal" - - -def filepath_to_uri(filepath: str) -> str: - """ - Converts a native filesystem path to a file:// URI. - """ - path_obj = Path(filepath) - - absolute_path_obj = path_obj.resolve() - - return absolute_path_obj.as_uri() - - -find_symbols_in_scope_expected_results = [ - ("goto_def_1", (3, 0), "find_symbols_in_scope"), - ("goto_def_2", (11, 4), "find_symbols_in_scope"), - ("goto_def_3", (7, 6), "find_symbols_in_scope"), - ("goto_def_4", (8, 8), "find_symbols_in_scope"), - ("goto_def_5", (2, 4), "symbol_def_extended_asset_aux3"), - ("goto_def_6", (4, 6), "symbol_def_extended_asset_aux3"), - ("goto_def_7", (5, 6), "symbol_def_variable_call_extend_chain_aux2"), - ("goto_def_8", (5, 4), "symbol_def_variable_declaration_main"), - ("goto_def_9", (13, 4), "symbol_def_variable_declaration_main"), - ("goto_def_10", (15, 4), "symbol_def_variable_declaration_main"), - ("goto_def_11", (15, 4), "symbol_def_variable_declaration_main"), - ("goto_def_12", (8, 4), "symbol_def_variable_declaration_main"), - ("goto_def_13", (7, 4), "symbol_def_variable_declaration_main"), - ("goto_def_14", (28, 4), "symbol_def_variable_declaration_main"), - ("goto_def_15", (28, 4), "symbol_def_variable_declaration_main"), - ("goto_def_16", (30, 4), "symbol_def_variable_declaration_main"), - ("goto_def_17", (5, 4), "symbol_def_preconditions"), - ("goto_def_18", (14, 4), "symbol_def_preconditions"), - ("goto_def_19", (16, 4), "symbol_def_preconditions"), - ("goto_def_20", (16, 4), "symbol_def_preconditions"), - ("goto_def_21", (6, 8), "symbol_def_reaches"), - ("goto_def_22", (15, 6), "symbol_def_reaches"), +from ..util import build_rpc_message_stream, get_lsp_json, server_output + +# Test parameters +mal_find_symbols_in_scope_points = zip([ + (3, 12), # category_declaration, "goto_def_1" + (11, 10), # asset declaration, asset name, "goto_def_2" + (7, 10), # asset variable, "goto_def_3" + (8, 10), # attack step, "goto_def_4" + ], itertools.repeat("find_symbols_in_scope")) +mal_symbol_def_extended_asset_main_points = zip([ + (6, 25), # asset declaration, extended asset, "goto_def_5" + (9, 11), # variable call, "goto_def_6" + ], itertools.repeat("symbol_def_extended_asset_main")) +mal_symbol_def_variable_call_extend_chain_main_points = zip([ + (9, 11) # variable call, extend chain, "goto_def_7" + ], itertools.repeat("symbol_def_variable_call_extend_chain_main")) +symbol_def_variable_declaration_main_points = zip([ + (10, 20), # variable declaration, "goto_def_8" + (17, 21), # variable declaration, extended asset, "goto_def_9" + (21, 36), # variable declaration complex 1, "goto_def_10" + (22, 36), # variable declaration complex 2, "goto_def_11" + (26, 6), # association asset name 1, "goto_def_12" + (27, 37), # association asset name 2, "goto_def_13" + (28, 12), # association field name 1, "goto_def_14" + (28, 32), # association field name 2, "goto_def_15" + (30, 22), # link name, "goto_def_16" + ], itertools.repeat("symbol_def_variable_declaration_main")) +mal_symbol_def_preconditions_points = zip([ + (11, 15), # preconditions, "goto_def_17" + (19, 13), # preconditions extended asset, "goto_def_18" + (24, 28), # preconditions complex 1, "goto_def_19" + (26, 28), # preconditions complex 1, "goto_def_20" + ], itertools.repeat("symbol_def_preconditions")) +# this one is specifically list so that the last case can be reused in +# test_goto_definition_wrong_symbol +mal_symbol_def_reaches_points = list(zip([ + (13, 22), # reaches, "goto_def_21" + (14, 14), # reaches single attack step, "goto_def_22" + (10, 7), # random non-user defined symbol, "goto_def_23" + ], itertools.repeat("symbol_def_reaches"))) + + +goto_input_location_and_files = itertools.chain(mal_find_symbols_in_scope_points, + mal_symbol_def_extended_asset_main_points, + mal_symbol_def_variable_call_extend_chain_main_points, + symbol_def_variable_declaration_main_points, + mal_symbol_def_preconditions_points, + mal_symbol_def_reaches_points) + +goto_expected_location_and_files = [ + ((3, 0), "find_symbols_in_scope"), # "goto_def_1" + ((11, 4), "find_symbols_in_scope"), # "goto_def_2" + ((7, 6), "find_symbols_in_scope"), # "goto_def_3" + ((8, 8), "find_symbols_in_scope"), # "goto_def_4" + ((2, 4), "symbol_def_extended_asset_aux3"), # "goto_def_5" + ((4, 6), "symbol_def_extended_asset_aux3"), # "goto_def_6" + ((5, 6), "symbol_def_variable_call_extend_chain_aux2"), # "goto_def_7" + ((5, 4), "symbol_def_variable_declaration_main"), # "goto_def_8" + ((13, 4), "symbol_def_variable_declaration_main"), # "goto_def_9" + ((15, 4), "symbol_def_variable_declaration_main"), # "goto_def_10" + ((15, 4), "symbol_def_variable_declaration_main"), # "goto_def_11" + ((8, 4), "symbol_def_variable_declaration_main"), # "goto_def_12" + ((7, 4), "symbol_def_variable_declaration_main"), # "goto_def_13" + ((28, 4), "symbol_def_variable_declaration_main"), # "goto_def_14" + ((28, 4), "symbol_def_variable_declaration_main"), # "goto_def_15" + ((30, 4), "symbol_def_variable_declaration_main"), # "goto_def_16" + ((5, 4), "symbol_def_preconditions"), # "goto_def_17" + ((14, 4), "symbol_def_preconditions"), # "goto_def_18" + ((16, 4), "symbol_def_preconditions"), # "goto_def_19" + ((16, 4), "symbol_def_preconditions"), # "goto_def_20" + ((6, 8), "symbol_def_reaches"), # "goto_def_21" + ((15, 6), "symbol_def_reaches"), # "goto_def_22" ] - +# [((input location, input file), (expected location, expected file))] +find_symbols_in_scope_expected_results = list(zip(goto_input_location_and_files, + goto_expected_location_and_files)) + +# Fixtures +type FixtureCallback[T] = typing.Callable[[str, typing.BinaryIO, (int, int)], T] + +@pytest.fixture +def open_document_notification( + client_notifications: list[dict], + client_messages: list[dict]) -> FixtureCallback[dict]: + def make(uri: str, file: typing.BinaryIO, _location) -> dict: + message = { + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": uri, + "languageId": "mal", + "version": 0, + "text": file.read().decode("utf8"), + } + }, + } + client_notifications.append(message) + client_messages.append(message) + return message + return make + +@pytest.fixture +def definition_request( + client_requests: list[dict], + client_messages: list[dict]) -> FixtureCallback[dict]: + def make(uri: str, _file, location: (int, int)) -> dict: + line, char = location + message = { + "id": len(client_requests), + "jsonrpc": "2.0", + "method": "textDocument/definition", + "params": { + "textDocument": { + "uri": uri, + }, + "position": { + "line": line, + "character": char, + }, + }, + } + client_requests.append(message) + client_messages.append(message) + return message + return make + +@pytest.fixture +def goto_definition_client_messages( + client_messages: list[dict], + initalize_request, + initalized_notification, + open_document_notification: FixtureCallback[dict], + definition_request: FixtureCallback[dict]) -> FixtureCallback[typing.BinaryIO]: + def make(uri: str, file: typing.BinaryIO, location: (int, int)) -> typing.BinaryIO: + args = (uri, file, location) + open_document_notification(*args) + definition_request(*args) + return build_rpc_message_stream(client_messages) + return make + + +def parameter_id(argvalue: (((int, int), str), ((int, int), str))) -> object: + # Turns the combination of test parameters of find_symbols_in_scope_expected_results + # into more legigble/simpler/useful names + inputs, outputs = argvalue + (origin_line, origin_char), origin_file = inputs + (expected_line, expected_char), expected_file = outputs + return (f"{origin_file}:L{origin_line},C{origin_char}" + "-" + f"{expected_file}:L{expected_line},C{expected_char}") + +# Tests @pytest.mark.parametrize( - "fixture_name,expected_point,expected_file", - [ - (fixture_name, point, file) - for fixture_name, point, file in find_symbols_in_scope_expected_results - ], + "inputs,expected_outputs", + find_symbols_in_scope_expected_results, + ids=map(parameter_id, find_symbols_in_scope_expected_results) ) -def test_goto_definition(request, fixture_name, expected_point, expected_file): - # send to server - fixture = request.getfixturevalue(fixture_name) +def test_goto_definition( + request: pytest.FixtureRequest, + inputs: ((int, int), str), + expected_outputs: ((int, int), str), + goto_definition_client_messages: FixtureCallback[typing.BinaryIO]): + origin_location, origin_file = inputs + expected_point, expected_file = expected_outputs + + expected_file_uri = request.getfixturevalue(f"mal_{expected_file}_uri") + + uri_fixture = request.getfixturevalue(f"mal_{origin_file}_uri") + file_fixture = request.getfixturevalue(f"mal_{origin_file}") + fixture = goto_definition_client_messages(uri_fixture, file_fixture, origin_location) + output, ls, *_ = server_output(fixture) output.seek(0) @@ -73,18 +187,29 @@ def test_goto_definition(request, fixture_name, expected_point, expected_file): file = response["result"]["uri"] assert start_point == expected_point - assert file == filepath_to_uri(FILE_PATH + expected_file + ".mal") + assert file == expected_file_uri output.close() -def test_goto_definition_wrong_symbol(goto_def_23): +def test_goto_definition_wrong_symbol( + request: pytest.FixtureRequest, + goto_definition_client_messages: FixtureCallback[typing.BinaryIO]): """ This test aims to check that the LS can handle requests for symbols which are not user-defined """ + # previously known as goto_def_23 + inputs = next(itertools.islice(mal_symbol_def_reaches_points, 2, 3)) + + origin_location, origin_file = inputs + + uri_fixture = request.getfixturevalue(f"mal_{origin_file}_uri") + file_fixture = request.getfixturevalue(f"mal_{origin_file}") + fixture = goto_definition_client_messages(uri_fixture, file_fixture, origin_location) + # send to server - output, ls, *_ = server_output(goto_def_23) + output, ls, *_ = server_output(fixture) output.seek(0) response = get_lsp_json(output) From 317a291faa12fa940dff01761fdc9d64ea928e07 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:38:24 +0200 Subject: [PATCH 18/42] refactor: Use paratremization in test_visit_expr --- tests/integration/test_visit_expr.py | 187 ++++++--------------------- 1 file changed, 40 insertions(+), 147 deletions(-) diff --git a/tests/integration/test_visit_expr.py b/tests/integration/test_visit_expr.py index 28b6eb4..52ebf7b 100644 --- a/tests/integration/test_visit_expr.py +++ b/tests/integration/test_visit_expr.py @@ -1,64 +1,32 @@ -import logging +import typing +import pytest import tree_sitter_mal as ts_mal -from tree_sitter import Language, Parser +from tree_sitter import Language, Parser, Tree, TreeCursor from malls.ts.utils import visit_expr -log = logging.getLogger(__name__) MAL_LANGUAGE = Language(ts_mal.language()) PARSER = Parser(MAL_LANGUAGE) +@pytest.fixture +def tree(mal_visit_expr: typing.BinaryIO) -> Tree: + return PARSER.parse(mal_visit_expr.read()) -def test_visit_expr_only_collects(mal_visit_expr): - tree = PARSER.parse(mal_visit_expr.read()) - cursor = tree.walk() - - point = (9, 11) +@pytest.fixture +def cursor(tree: Tree) -> TreeCursor: + return tree.walk() +def goto_asset_expression(cursor: TreeCursor, point: (int, int)): while cursor.node.type != "asset_expr": cursor.goto_first_child_for_point(point) - # we use sets to ensure order does not matter cursor.goto_first_child() - found = [] - visit_expr(cursor, found) - - assert found == [b"a", b"b", b"c"] - - -def test_visit_expr_simple_paranthesized(mal_visit_expr): - tree = PARSER.parse(mal_visit_expr.read()) - cursor = tree.walk() - point = (10, 11) - - while cursor.node.type != "asset_expr": - cursor.goto_first_child_for_point(point) - - # we use sets to ensure order does not matter - cursor.goto_first_child() - found = [] - visit_expr(cursor, found) - - assert found == [b"a", b"z", b"b", b"c"] - - -def test_visit_expr_various_paranthesized(mal_visit_expr): - tree = PARSER.parse(mal_visit_expr.read()) - cursor = tree.walk() - - point = (11, 11) - - while cursor.node.type != "asset_expr": - cursor.goto_first_child_for_point(point) - - # we use sets to ensure order does not matter - cursor.goto_first_child() - found = [] - visit_expr(cursor, found) - - assert found == [ +point_found_parameters = [ + ((9, 11), [b"a", b"b", b"c"]), + ((10, 11), [b"a", b"z", b"b", b"c"]), + ((11, 11), [ b"a", b"z", b"b", @@ -73,106 +41,31 @@ def test_visit_expr_various_paranthesized(mal_visit_expr): b"x", b"u", b"t", - ] - - -def test_visit_expr_unop(mal_visit_expr): - tree = PARSER.parse(mal_visit_expr.read()) - cursor = tree.walk() - - point = (12, 11) - - while cursor.node.type != "asset_expr": - cursor.goto_first_child_for_point(point) - - # we use sets to ensure order does not matter - cursor.goto_first_child() - found = [] - visit_expr(cursor, found) - - assert found == [b"a", b"b", b"c"] - - -def test_visit_expr_single_binop(mal_visit_expr): - tree = PARSER.parse(mal_visit_expr.read()) - cursor = tree.walk() - - point = (13, 11) - - while cursor.node.type != "asset_expr": - cursor.goto_first_child_for_point(point) - - # we use sets to ensure order does not matter - cursor.goto_first_child() + ]), + ((12, 11), [b"a", b"b", b"c"]), + ((13, 11), [b"a", b"b", b"c"]), + ((14, 11), [b"a", b"b", b"d", b"e", b"f", b"h", b"i"]), + ((15, 11), [(b"d", "asset")]), + ((16, 11), [(b"g", "asset"), b"h", b"i"]), + ((17, 11), [b"w", b"x", b"y", b"z", b"a"]) +] + +parameter_names = ["only_collects", + "simple_paranthesized", + "various_paranthesized", + "unop", + "single_binop", + "various_binop", + "single_type", + "various_type", + "variable"] + +@pytest.mark.parametrize("point,expected", point_found_parameters, ids=parameter_names) +def test_visir_expr( + point: (int, int), + expected: list[bytes], + cursor: TreeCursor): + goto_asset_expression(cursor, point) found = [] visit_expr(cursor, found) - - assert found == [b"a", b"b", b"c"] - - -def test_visit_expr_various_binop(mal_visit_expr): - tree = PARSER.parse(mal_visit_expr.read()) - cursor = tree.walk() - - point = (14, 11) - - while cursor.node.type != "asset_expr": - cursor.goto_first_child_for_point(point) - - # we use sets to ensure order does not matter - cursor.goto_first_child() - found = [] - visit_expr(cursor, found) - - assert found == [b"a", b"b", b"d", b"e", b"f", b"h", b"i"] - - -def test_visit_expr_single_type(mal_visit_expr): - tree = PARSER.parse(mal_visit_expr.read()) - cursor = tree.walk() - - point = (15, 11) - - while cursor.node.type != "asset_expr": - cursor.goto_first_child_for_point(point) - - # we use sets to ensure order does not matter - cursor.goto_first_child() - found = [] - visit_expr(cursor, found) - - assert found == [(b"d", "asset")] - - -def test_visit_expr_various_type(mal_visit_expr): - tree = PARSER.parse(mal_visit_expr.read()) - cursor = tree.walk() - - point = (16, 11) - - while cursor.node.type != "asset_expr": - cursor.goto_first_child_for_point(point) - - # we use sets to ensure order does not matter - cursor.goto_first_child() - found = [] - visit_expr(cursor, found) - - assert found == [(b"g", "asset"), b"h", b"i"] - - -def test_visit_expr_variable(mal_visit_expr): - tree = PARSER.parse(mal_visit_expr.read()) - cursor = tree.walk() - - point = (17, 11) - - while cursor.node.type != "asset_expr": - cursor.goto_first_child_for_point(point) - - # we use sets to ensure order does not matter - cursor.goto_first_child() - found = [] - visit_expr(cursor, found) - - assert found == [b"w", b"x", b"y", b"z", b"a"] + assert found == expected From b7fd583a335b8bd30a4e4e4869a5e724efad08c3 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:39:39 +0200 Subject: [PATCH 19/42] misc: Move test_visir_expr to unit tests --- tests/{integration => unit}/test_visit_expr.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{integration => unit}/test_visit_expr.py (100%) diff --git a/tests/integration/test_visit_expr.py b/tests/unit/test_visit_expr.py similarity index 100% rename from tests/integration/test_visit_expr.py rename to tests/unit/test_visit_expr.py From 8cb054639929fdf38a8259a01080dc52e6ecdf23 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Tue, 26 Aug 2025 17:18:36 +0200 Subject: [PATCH 20/42] refactor: Move LSP fixture components to LSP fixtures subfolder --- tests/conftest.py | 214 ---------------- tests/fixtures/lsp/conftest.py | 230 ++++++++++++++++++ .../fixtures/lsp/encoding_capability_check.py | 2 + tests/integration/test_completion.py | 2 + 4 files changed, 234 insertions(+), 214 deletions(-) create mode 100644 tests/fixtures/lsp/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py index 708de44..2cb47d1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,20 +2,9 @@ import os import sys import typing -from io import BytesIO from pathlib import Path -# from ..src.malls.lsp.enums import ErrorCodes import pytest -import uritools - -from malls.lsp.enums import ErrorCodes - -from .util import ( - CONTENT_TYPE_HEADER, - build_rpc_message_stream, - find_last_request, -) logging.getLogger().setLevel(logging.DEBUG) log = logging.getLogger(__name__) @@ -79,206 +68,3 @@ def template() -> str: ) setattr(module, fixture_name + "_uri", uri_fixture) - - -@pytest.fixture -def client_requests() -> list[dict]: - """ - Keeps track of all requests from the client made so far. - - Has to be manually requested and added to. - """ - return [] - - -@pytest.fixture -def client_notifications() -> list[dict]: - """ - Keeps track of all notifications from the client made so far. - - Has to be manually requested and added to. - """ - return [] - - -@pytest.fixture -def client_responses() -> list[dict]: - """ - Keeps track of all responses from the client made so far. - - Has to be manually requested and added to. - """ - return [] - - -@pytest.fixture -def client_messages() -> list[dict]: - """ - Keeps track of all messages from the client made so far. - - Has to be manually requested and added to. - """ - return [] - - -@pytest.fixture -def server_requests() -> list[dict]: - """ - Keeps track of all requests from the server made so far. - - Has to be manually requested and added to. - """ - return [] - - -@pytest.fixture -def server_notifications() -> list[dict]: - """ - Keeps track of all notifications from the server made so far. - - Has to be manually requested and added to. - """ - return [] - - -@pytest.fixture -def server_responses() -> list[dict]: - """ - Keeps track of all responses from the server made so far. - - Has to be manually requested and added to. - """ - return [] - - -@pytest.fixture -def server_messages() -> list[dict]: - """ - Keeps track of all messages from the server made so far. - - Has to be manually requested and added to. If creatin - """ - return [] - - -@pytest.fixture -def initalize_request(client_requests: list[dict], client_messages: list[dict]) -> dict: - """ - Defines an `initalize` LSP request from client to server. - """ - message = { - "jsonrpc": "2.0", - "id": len(client_requests), - "method": "initialize", - "params": {"trace": "off", "capabilities": {}}, - } - client_requests.append(message) - client_messages.append(message) - return message - - -@pytest.fixture -def initalize_response( - client_requests: list[dict], server_responses: list[dict], server_messages: list[dict] -) -> dict: - """ - Creates a default response to the latest (id) `initalize` request from a client. - Defaults to ID 0. - """ - message = { - "jsonrpc": "2.0", - "id": find_last_request( - client_requests, lambda request: request.get("method") == "initalize", {} - ).get("id", 0), - "result": { - # TODO: Replace with values from an actual server instance (e.g. via instance.capabilities()) - "capabilities": { - "positionEncoding": "utf-16", - "textDocumentSync": {"openClose": True, "change": 1}, - "definitionProvider": True, - "completionProvider": {}, - }, - # TODO: Replace with values from an actual server instance (e.g. via instance.server_info()) - "serverInfo": {"name": "mal-ls"}, - }, - } - server_responses.append(message) - server_messages.append(message) - return message - - -@pytest.fixture -def initalized_notification(client_notifications: list[dict], client_messages: list[dict]) -> dict: - """ - Defines an `initalized` LSP notification from client to server. - """ - message = {"jsonrpc": "2.0", "method": "initialized"} - client_notifications.append(message) - client_messages.append(message) - return message - - -@pytest.fixture -def shutdown_request(client_requests: list[dict], client_messages: list[dict]) -> dict: - """ - Defines an `shutdown` LSP request from client to server. - """ - message = {"jsonrpc": "2.0", "id": len(client_requests), "method": "shutdown"} - client_requests.append(message) - client_messages.append(message) - return message - - -@pytest.fixture -def exit_notification(client_notifications: list[dict], client_messages: list[dict]) -> dict: - """ - Defines an `exit` LSP notification from client to server. - """ - message = {"jsonrpc": "2.0", "method": "exit"} - client_notifications.append(message) - client_messages.append(message) - return message - - -@pytest.fixture -def invalid_request_response(server_responses: list[dict]) -> dict: - """ - Defines a template "invalid request" response. Field "message" and "id" must be filled in when - appropriate. - """ - message = { - "jsonrpc": "2.0", - "error": { - "code": ErrorCodes.InvalidRequest, - "message": "Must wait for `initalized` notification before other requests.", - }, - } - server_responses.append(message) - return message - - -@pytest.fixture -def non_initialized_invalid_request_response( - server_responses: list[dict], invalid_request_response: dict -) -> None: - """ - Defines a invalid request response for the case of a non-initalized server. - """ - message = "Must wait for `initalized` notification before other requests." - server_responses[-1]["error"]["message"] = message - - -@pytest.fixture -def client_rpc_messages(client_messages: list[dict]) -> BytesIO: - """ - Builds the list of client messages into JSON RPC message stream. - """ - return build_rpc_message_stream(client_messages) - - -@pytest.fixture -def server_rpc_messages(server_messages: list[dict]) -> BytesIO: - """ - Builds the list of server messages into JSON RPC message stream. - """ - return build_rpc_message_stream(server_messages, insert_header=CONTENT_TYPE_HEADER) diff --git a/tests/fixtures/lsp/conftest.py b/tests/fixtures/lsp/conftest.py new file mode 100644 index 0000000..765b24c --- /dev/null +++ b/tests/fixtures/lsp/conftest.py @@ -0,0 +1,230 @@ +import typing + +import pytest + +from malls.lsp.enums import ErrorCodes + +from ...util import CONTENT_TYPE_HEADER, build_rpc_message_stream, find_last_request + + +@pytest.fixture +def client_requests() -> list[dict]: + """ + Keeps track of all requests from the client made so far. + + Has to be manually requested and added to. + """ + return [] + + +@pytest.fixture +def client_notifications() -> list[dict]: + """ + Keeps track of all notifications from the client made so far. + + Has to be manually requested and added to. + """ + return [] + + +@pytest.fixture +def client_responses() -> list[dict]: + """ + Keeps track of all responses from the client made so far. + + Has to be manually requested and added to. + """ + return [] + + +@pytest.fixture +def client_messages() -> list[dict]: + """ + Keeps track of all messages from the client made so far. + + Has to be manually requested and added to. + """ + return [] + + +@pytest.fixture +def server_requests() -> list[dict]: + """ + Keeps track of all requests from the server made so far. + + Has to be manually requested and added to. + """ + return [] + + +@pytest.fixture +def server_notifications() -> list[dict]: + """ + Keeps track of all notifications from the server made so far. + + Has to be manually requested and added to. + """ + return [] + + +@pytest.fixture +def server_responses() -> list[dict]: + """ + Keeps track of all responses from the server made so far. + + Has to be manually requested and added to. + """ + return [] + + +@pytest.fixture +def server_messages() -> list[dict]: + """ + Keeps track of all messages from the server made so far. + + Has to be manually requested and added to. If creatin + """ + return [] + + +@pytest.fixture +def initalize_request(client_requests: list[dict], client_messages: list[dict]) -> dict: + """ + Defines an `initalize` LSP request from client to server. + """ + message = { + "jsonrpc": "2.0", + "id": len(client_requests), + "method": "initialize", + "params": {"trace": "off", "capabilities": {}}, + } + client_requests.append(message) + client_messages.append(message) + return message + + +@pytest.fixture +def initalize_response( + client_requests: list[dict], server_responses: list[dict], server_messages: list[dict] +) -> dict: + """ + Creates a default response to the latest (id) `initalize` request from a client. + Defaults to ID 0. + """ + message = { + "jsonrpc": "2.0", + "id": find_last_request(client_requests, "initalize", {}).get("id", 0), + "result": { + # TODO: Replace with values from an actual server instance (e.g. via instance.capabilities()) + "capabilities": { + "positionEncoding": "utf-16", + "textDocumentSync": {"openClose": True, "change": 1}, + "definitionProvider": True, + "completionProvider": {}, + }, + # TODO: Replace with values from an actual server instance (e.g. via instance.server_info()) + "serverInfo": {"name": "mal-ls"}, + }, + } + server_responses.append(message) + server_messages.append(message) + return message + + +@pytest.fixture +def initalized_notification(client_notifications: list[dict], client_messages: list[dict]) -> dict: + """ + Defines an `initalized` LSP notification from client to server. + """ + message = {"jsonrpc": "2.0", "method": "initialized"} + client_notifications.append(message) + client_messages.append(message) + return message + + +@pytest.fixture +def shutdown_request(client_requests: list[dict], client_messages: list[dict]) -> dict: + """ + Defines an `shutdown` LSP request from client to server. + """ + message = {"jsonrpc": "2.0", "id": len(client_requests), "method": "shutdown"} + client_requests.append(message) + client_messages.append(message) + return message + + +@pytest.fixture +def exit_notification(client_notifications: list[dict], client_messages: list[dict]) -> dict: + """ + Defines an `exit` LSP notification from client to server. + """ + message = {"jsonrpc": "2.0", "method": "exit"} + client_notifications.append(message) + client_messages.append(message) + return message + + +@pytest.fixture +def invalid_request_response(server_responses: list[dict]) -> dict: + """ + Defines a template "invalid request" response. Field "message" and "id" must be filled in when + appropriate. + """ + message = { + "jsonrpc": "2.0", + "error": { + "code": ErrorCodes.InvalidRequest, + "message": "Must wait for `initalized` notification before other requests.", + }, + } + server_responses.append(message) + return message + + +@pytest.fixture +def non_initialized_invalid_request_response( + server_responses: list[dict], invalid_request_response: dict +) -> None: + """ + Defines a invalid request response for the case of a non-initalized server. + """ + message = "Must wait for `initalized` notification before other requests." + server_responses[-1]["error"]["message"] = message + + +@pytest.fixture +def client_rpc_messages(client_messages: list[dict]) -> typing.BinaryIO: + """ + Builds the list of client messages into JSON RPC message stream. + """ + return build_rpc_message_stream(client_messages) + + +@pytest.fixture +def server_rpc_messages(server_messages: list[dict]) -> typing.BinaryIO: + """ + Builds the list of server messages into JSON RPC message stream. + """ + return build_rpc_message_stream(server_messages, insert_header=CONTENT_TYPE_HEADER) + +@pytest.fixture +def did_open_notification( + client_notifications: list[dict], + client_messages: list[dict]) -> dict: + """ + Defines an `textDocument/didOPen` LSP notification from client to server. Fields + `uri` and `text` in `params.textDocument` must be edited. + """ + message = { + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "languageId": "mal", + "version": 0, + } + }, + } + client_notifications.append(message) + client_messages.append(message) + return message diff --git a/tests/fixtures/lsp/encoding_capability_check.py b/tests/fixtures/lsp/encoding_capability_check.py index b36654a..d6f0960 100644 --- a/tests/fixtures/lsp/encoding_capability_check.py +++ b/tests/fixtures/lsp/encoding_capability_check.py @@ -2,6 +2,8 @@ import pytest +# So that importers are aware of the conftest fixtures +pytest_plugins = ["tests.fixtures.lsp.conftest"] @pytest.fixture def set_trace_notification(client_notifications: list[dict], client_messages: list[dict]) -> dict: diff --git a/tests/integration/test_completion.py b/tests/integration/test_completion.py index 9f61818..b15760d 100644 --- a/tests/integration/test_completion.py +++ b/tests/integration/test_completion.py @@ -9,6 +9,8 @@ from ..util import build_rpc_message_stream, get_lsp_json, server_output +pytest_plugins = ["tests.fixtures.lsp.conftest"] + log = logging.getLogger(__name__) MAL_LANGUAGE = Language(ts_mal.language()) PARSER = Parser(MAL_LANGUAGE) From 449f2150d916852478e3c449f4aaf72013abf07a Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 11:42:40 +0200 Subject: [PATCH 21/42] feat: initalize fixture capabilities, add didChange/didOpen base_open fixtures --- tests/fixtures/lsp/conftest.py | 58 ++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/tests/fixtures/lsp/conftest.py b/tests/fixtures/lsp/conftest.py index 765b24c..aea78af 100644 --- a/tests/fixtures/lsp/conftest.py +++ b/tests/fixtures/lsp/conftest.py @@ -96,7 +96,19 @@ def initalize_request(client_requests: list[dict], client_messages: list[dict]) "jsonrpc": "2.0", "id": len(client_requests), "method": "initialize", - "params": {"trace": "off", "capabilities": {}}, + "params": { + "capabilities": { + "textDocument": { + "definition": { + "dynamicRegistration": False + }, + "synchronization": { + "dynamicRegistration": False + } + } + }, + "trace": "off" + }, } client_requests.append(message) client_messages.append(message) @@ -212,7 +224,7 @@ def did_open_notification( client_notifications: list[dict], client_messages: list[dict]) -> dict: """ - Defines an `textDocument/didOPen` LSP notification from client to server. Fields + Defines an `textDocument/didOpen` LSP notification from client to server. Fields `uri` and `text` in `params.textDocument` must be edited. """ message = { @@ -228,3 +240,45 @@ def did_open_notification( client_notifications.append(message) client_messages.append(message) return message + +@pytest.fixture +def did_change_notification( + client_notifications: list[dict], + client_messages: list[dict]) -> dict: + """ + Defines an `textDocument/didChange` LSP notification from client to server. Fields + `uri`, `version` in `params.textDocument` and `range`, `text` in `params.contentChanges` + must be edited. + """ + message = { + "jsonrpc": "2.0", + "method": "textDocument/didChange", + "params": { + "textDocument": {}, + "contentChanges": [ + { + "range": { + "start": {}, + "end": {}, + }, + } + ], + }, + } + client_notifications.append(message) + client_messages.append(message) + return message + +@pytest.fixture +def did_open_base_open_notification( + client_notifications: list[dict], + client_messages: list[dict], + did_open_notification, + mal_base_open: typing.BinaryIO, + mal_base_open_uri: str): + # since did_open_notification is a dependency here + # we know the notification is the latest one + open_notification = client_notifications[-1] + text_doc_params = open_notification["params"]["textDocument"] + text_doc_params["uri"] = mal_base_open_uri + text_doc_params["text"] = mal_base_open.read().decode("utf8") From 692486df3ae0ea26c052e0c0c72ded713d0a7f69 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 11:59:58 +0200 Subject: [PATCH 22/42] test!: Use LSP fixture components for test_publish_diagnostics breaking: Added skip to test_diagnostics_when_changing_file_with_error because the server is strangely not sending any diagnostic notifications. --- tests/conftest.py | 1 - tests/fixtures/lsp/publish_diagnostics.py | 102 ++++++++++++++++++ tests/fixtures/mal/base_open.mal | 8 ++ tests/fixtures/mal/erroneous.mal | 7 ++ tests/fixtures/mal/erroneous_include.mal | 4 + tests/fixtures/mal/file_with_error.mal | 2 +- tests/integration/test_base_protocol.py | 3 - tests/integration/test_completion.py | 2 - .../integration/test_diagnostics_are_saved.py | 3 - ...t_did_change_text_document_notification.py | 2 - ...est_did_open_text_document_notification.py | 3 - .../test_find_symbol_definition_function.py | 2 - tests/integration/test_publish_diagnostics.py | 34 +++--- tests/integration/test_trace.py | 3 - 14 files changed, 137 insertions(+), 39 deletions(-) create mode 100644 tests/fixtures/lsp/publish_diagnostics.py create mode 100644 tests/fixtures/mal/base_open.mal create mode 100644 tests/fixtures/mal/erroneous.mal create mode 100644 tests/fixtures/mal/erroneous_include.mal diff --git a/tests/conftest.py b/tests/conftest.py index 2cb47d1..d6af2f6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,6 @@ import pytest logging.getLogger().setLevel(logging.DEBUG) -log = logging.getLogger(__name__) module = sys.modules[__name__] # Generate pytest fixtures from all fixture files in 'fixtures' and its subdirectories diff --git a/tests/fixtures/lsp/publish_diagnostics.py b/tests/fixtures/lsp/publish_diagnostics.py new file mode 100644 index 0000000..f28a028 --- /dev/null +++ b/tests/fixtures/lsp/publish_diagnostics.py @@ -0,0 +1,102 @@ +import typing + +import pytest + +# So that importers are aware of the conftest fixtures +pytest_plugins = ["tests.fixtures.lsp.conftest"] + + +@pytest.fixture +def did_open_erroneous_notification( + client_notifications: list[dict], + did_open_notification, + mal_erroneous: typing.BinaryIO, + mal_erroneous_uri: str): + # since did_open_notification is a dependency here + # we know the notification is the latest one + open_notification = client_notifications[-1] + text_doc_params = open_notification["params"]["textDocument"] + text_doc_params["uri"] = mal_erroneous_uri + text_doc_params["text"] = mal_erroneous.read().decode("utf8") + +@pytest.fixture +def erroneous_file_client_messages( + initalize_request, + initalized_notification, + did_open_erroneous_notification, + client_rpc_messages: typing.BinaryIO) -> typing.BinaryIO: + return client_rpc_messages + +@pytest.fixture +def did_open_erroneous_include_notification( + client_notifications: list[dict], + did_open_notification, + mal_erroneous_include: typing.BinaryIO, + mal_erroneous_include_uri: str): + # since did_open_notification is a dependency here + # we know the notification is the latest one + open_notification = client_notifications[-1] + text_doc_params = open_notification["params"]["textDocument"] + text_doc_params["uri"] = mal_erroneous_include_uri + text_doc_params["text"] = mal_erroneous_include.read().decode("utf8") + + +@pytest.fixture +def erroneous_include_file_client_messages( + initalize_request, + initalized_notification, + did_open_erroneous_include_notification, + client_rpc_messages: typing.BinaryIO) -> typing.BinaryIO: + return client_rpc_messages + +@pytest.fixture +def did_open_file_with_error_notification( + client_notifications: list[dict], + did_open_notification, + mal_file_with_error: typing.BinaryIO, + mal_file_with_error_uri: str): + # since did_open_notification is a dependency here + # we know the notification is the latest one + open_notification = client_notifications[-1] + text_doc_params = open_notification["params"]["textDocument"] + text_doc_params["uri"] = mal_file_with_error_uri + text_doc_params["text"] = mal_file_with_error.read().decode("utf8") + +@pytest.fixture +def erroenous_include_and_file_with_error_client_messages( + initalize_request, + initalized_notification, + did_open_erroneous_notification, + did_open_file_with_error_notification, + client_rpc_messages: typing.BinaryIO) -> typing.BinaryIO: + return client_rpc_messages + +@pytest.fixture +def did_change_with_error_notification( + client_notifications: list[dict], + did_change_notification, + mal_base_open_uri: str): + client_notifications[-1]["params"] = { + "textDocument": { + "uri": mal_base_open_uri, + "version": 1, + }, + "contentChanges": [ + { + "range": { + "start": {"line": 4, "character": 19}, + "end": {"line": 5, "character": 28}, + }, + "text": "Bar {}\nasset Foo extends Bar {}", + } + ], + } + +@pytest.fixture +def change_file_with_error_client_messages( + initalize_request, + initalized_notification, + did_open_base_open_notification, + did_change_with_error_notification, + client_rpc_messages: typing.BinaryIO) -> typing.BinaryIO: + return client_rpc_messages diff --git a/tests/fixtures/mal/base_open.mal b/tests/fixtures/mal/base_open.mal new file mode 100644 index 0000000..dc00894 --- /dev/null +++ b/tests/fixtures/mal/base_open.mal @@ -0,0 +1,8 @@ +#id: "org.mal-lang.testAnalyzer" +#version:"0.0.0" + +category System { + abstract asset Foo {} + asset Bar extends Foo {} +} + diff --git a/tests/fixtures/mal/erroneous.mal b/tests/fixtures/mal/erroneous.mal new file mode 100644 index 0000000..5f47642 --- /dev/null +++ b/tests/fixtures/mal/erroneous.mal @@ -0,0 +1,7 @@ +#id: "org.mal-lang.testAnalyzer" +#version:"0.0.0" + +category System { + abstract aet Foo {} + asset Bar extends Foo {} +} diff --git a/tests/fixtures/mal/erroneous_include.mal b/tests/fixtures/mal/erroneous_include.mal new file mode 100644 index 0000000..3a9cee7 --- /dev/null +++ b/tests/fixtures/mal/erroneous_include.mal @@ -0,0 +1,4 @@ +#id: "org.mal-lang.testAnalyzer" +#version:"0.0.0" + +include "file_with_error.mal" diff --git a/tests/fixtures/mal/file_with_error.mal b/tests/fixtures/mal/file_with_error.mal index ed6eb8b..2ea8a76 100644 --- a/tests/fixtures/mal/file_with_error.mal +++ b/tests/fixtures/mal/file_with_error.mal @@ -1,3 +1,3 @@ class Category { Asset1 {} - } +} diff --git a/tests/integration/test_base_protocol.py b/tests/integration/test_base_protocol.py index b028b28..f182417 100644 --- a/tests/integration/test_base_protocol.py +++ b/tests/integration/test_base_protocol.py @@ -1,4 +1,3 @@ -import logging import typing from malls.lsp.enums import ErrorCodes @@ -6,8 +5,6 @@ from ..util import get_lsp_json, server_output -log = logging.getLogger(__name__) - pytest_plugins = ["tests.fixtures.lsp.base_protocol"] diff --git a/tests/integration/test_completion.py b/tests/integration/test_completion.py index b15760d..823c609 100644 --- a/tests/integration/test_completion.py +++ b/tests/integration/test_completion.py @@ -1,5 +1,4 @@ import io -import logging import typing from pathlib import Path @@ -11,7 +10,6 @@ pytest_plugins = ["tests.fixtures.lsp.conftest"] -log = logging.getLogger(__name__) MAL_LANGUAGE = Language(ts_mal.language()) PARSER = Parser(MAL_LANGUAGE) FILE_PATH = str(Path(__file__).parent.parent.resolve()) + "/fixtures/mal/" diff --git a/tests/integration/test_diagnostics_are_saved.py b/tests/integration/test_diagnostics_are_saved.py index 8071dab..729aa47 100644 --- a/tests/integration/test_diagnostics_are_saved.py +++ b/tests/integration/test_diagnostics_are_saved.py @@ -1,4 +1,3 @@ -import logging import typing from pathlib import Path @@ -6,8 +5,6 @@ from ..util import server_output -log = logging.getLogger(__name__) - # calculate file path of mal files FILE_PATH = str(Path(__file__).parent.parent.resolve()) + "/fixtures/mal/" simplified_file_path = FILE_PATH + "main.mal" diff --git a/tests/integration/test_did_change_text_document_notification.py b/tests/integration/test_did_change_text_document_notification.py index 7316aba..798ae0e 100644 --- a/tests/integration/test_did_change_text_document_notification.py +++ b/tests/integration/test_did_change_text_document_notification.py @@ -1,5 +1,4 @@ import json -import logging import typing from pathlib import Path @@ -8,7 +7,6 @@ from ..util import server_output -log = logging.getLogger(__name__) MAL_LANGUAGE = Language(ts_mal.language()) PARSER = Parser(MAL_LANGUAGE) diff --git a/tests/integration/test_did_open_text_document_notification.py b/tests/integration/test_did_open_text_document_notification.py index b4f10d0..a473216 100644 --- a/tests/integration/test_did_open_text_document_notification.py +++ b/tests/integration/test_did_open_text_document_notification.py @@ -1,4 +1,3 @@ -import logging import typing from pathlib import Path @@ -6,8 +5,6 @@ from ..util import server_output -log = logging.getLogger(__name__) - # calculate file path of mal files FILE_PATH = str(Path(__file__).parent.parent.resolve()) + "/fixtures/mal/" simplified_file_path = FILE_PATH + "main.mal" diff --git a/tests/integration/test_find_symbol_definition_function.py b/tests/integration/test_find_symbol_definition_function.py index b33f4d2..5c374ed 100644 --- a/tests/integration/test_find_symbol_definition_function.py +++ b/tests/integration/test_find_symbol_definition_function.py @@ -1,4 +1,3 @@ -import logging from pathlib import Path import pytest @@ -9,7 +8,6 @@ from malls.lsp.utils import recursive_parsing from malls.ts.utils import INCLUDED_FILES_QUERY, find_symbol_definition, run_query -log = logging.getLogger(__name__) MAL_LANGUAGE = Language(ts_mal.language()) PARSER = Parser(MAL_LANGUAGE) FILE_PATH = str(Path(__file__).parent.parent.resolve()) + "/fixtures/mal/" diff --git a/tests/integration/test_publish_diagnostics.py b/tests/integration/test_publish_diagnostics.py index afdae2a..5338c35 100644 --- a/tests/integration/test_publish_diagnostics.py +++ b/tests/integration/test_publish_diagnostics.py @@ -1,24 +1,23 @@ -import logging import typing from pathlib import Path +import pytest + from malls.lsp.enums import DiagnosticSeverity from ..util import get_lsp_json, server_output -log = logging.getLogger(__name__) - # calculate file path of mal files FILE_PATH = str(Path(__file__).parent.parent.resolve()) + "/fixtures/mal/" simplified_file_path = FILE_PATH + "main.mal" included_file_path = FILE_PATH + "file_with_error.mal" +pytest_plugins = ["tests.fixtures.lsp.publish_diagnostics"] def test_diagnostics_when_opening_file_with_error( - open_file_with_error: typing.BinaryIO, -): + erroneous_file_client_messages: typing.BinaryIO): # send to server - output, ls, *_ = server_output(open_file_with_error) + output, ls, *_ = server_output(erroneous_file_client_messages) # Ensure LSP stored everything correctly @@ -31,17 +30,16 @@ def test_diagnostics_when_opening_file_with_error( assert len(params["diagnostics"]) == 1 start_point = params["diagnostics"][0]["range"]["start"] - assert (start_point["line"], start_point["character"]) == (4, 9) + assert (start_point["line"], start_point["character"]) == (4, 13) assert params["diagnostics"][0]["severity"] == DiagnosticSeverity.Error output.close() def test_diagnostics_when_opening_file_with_include_error( - open_file_with_include_error: typing.BinaryIO, -): + erroneous_include_file_client_messages: typing.BinaryIO): # send to server - output, ls, *_ = server_output(open_file_with_include_error) + output, ls, *_ = server_output(erroneous_include_file_client_messages) output.seek(0) get_lsp_json(output) @@ -57,17 +55,15 @@ def test_diagnostics_when_opening_file_with_include_error( def test_diagnostics_when_opening_file_with_include_error_and_opening_bad_file( - open_file_with_include_error_and_open_file: typing.BinaryIO, -): + erroenous_include_and_file_with_error_client_messages: typing.BinaryIO): # send to server - output, ls, *_ = server_output(open_file_with_include_error_and_open_file) + output, ls, *_ = server_output(erroenous_include_and_file_with_error_client_messages) output.seek(0) response = get_lsp_json(output) response = get_lsp_json(output) # this time the problematic file was opened, so there should be a diagnostic - log.info(response) assert "textDocument/publishDiagnostics" in response["method"] params = response["params"] assert len(params["diagnostics"]) == 1 @@ -78,19 +74,19 @@ def test_diagnostics_when_opening_file_with_include_error_and_opening_bad_file( output.close() - +# FIXME: +@pytest.mark.skip(("Server is not sending diagnostics for unknown reasons. " + "Only sending initalize response.")) def test_diagnostics_when_changing_file_with_error( - change_file_with_error: typing.BinaryIO, -): + change_file_with_error_client_messages: typing.BinaryIO): # send to server - output, ls, *_ = server_output(change_file_with_error) + output, ls, *_ = server_output(change_file_with_error_client_messages) output.seek(0) response = get_lsp_json(output) response = get_lsp_json(output) # this time the problematic file was opened, so there should be a diagnostic - log.info(response) assert "textDocument/publishDiagnostics" in response["method"] params = response["params"] assert len(params["diagnostics"]) == 1 diff --git a/tests/integration/test_trace.py b/tests/integration/test_trace.py index 6015f17..08158e0 100644 --- a/tests/integration/test_trace.py +++ b/tests/integration/test_trace.py @@ -1,12 +1,9 @@ -import logging import typing from malls.lsp.enums import ErrorCodes, TraceValue from ..util import get_lsp_json, server_output -log = logging.getLogger(__name__) - def test_wrong_trace_value_in_initialization(wrong_trace_value_in: typing.BinaryIO): output, ls, *_ = server_output(wrong_trace_value_in) From f9f3c6f9e8e703848a41ada5f2f23b34e65434f0 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 12:06:32 +0200 Subject: [PATCH 23/42] refactor: Move unit tests files to unit test folder --- tests/{integration => unit}/test_find_current_scope.py | 0 .../{integration => unit}/test_find_symbol_definition_function.py | 0 .../test_find_symbols_in_context_hierarchy.py | 0 tests/{integration => unit}/test_find_symbols_in_scope.py | 0 tests/{integration => unit}/test_position_conversion.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename tests/{integration => unit}/test_find_current_scope.py (100%) rename tests/{integration => unit}/test_find_symbol_definition_function.py (100%) rename tests/{integration => unit}/test_find_symbols_in_context_hierarchy.py (100%) rename tests/{integration => unit}/test_find_symbols_in_scope.py (100%) rename tests/{integration => unit}/test_position_conversion.py (100%) diff --git a/tests/integration/test_find_current_scope.py b/tests/unit/test_find_current_scope.py similarity index 100% rename from tests/integration/test_find_current_scope.py rename to tests/unit/test_find_current_scope.py diff --git a/tests/integration/test_find_symbol_definition_function.py b/tests/unit/test_find_symbol_definition_function.py similarity index 100% rename from tests/integration/test_find_symbol_definition_function.py rename to tests/unit/test_find_symbol_definition_function.py diff --git a/tests/integration/test_find_symbols_in_context_hierarchy.py b/tests/unit/test_find_symbols_in_context_hierarchy.py similarity index 100% rename from tests/integration/test_find_symbols_in_context_hierarchy.py rename to tests/unit/test_find_symbols_in_context_hierarchy.py diff --git a/tests/integration/test_find_symbols_in_scope.py b/tests/unit/test_find_symbols_in_scope.py similarity index 100% rename from tests/integration/test_find_symbols_in_scope.py rename to tests/unit/test_find_symbols_in_scope.py diff --git a/tests/integration/test_position_conversion.py b/tests/unit/test_position_conversion.py similarity index 100% rename from tests/integration/test_position_conversion.py rename to tests/unit/test_position_conversion.py From 0c1fc75d66e215ffb0e9c03af2c317790a64f37f Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 12:21:07 +0200 Subject: [PATCH 24/42] test: Paratremize test_position_conversion.py --- tests/unit/test_position_conversion.py | 87 +++++++++++--------------- 1 file changed, 35 insertions(+), 52 deletions(-) diff --git a/tests/unit/test_position_conversion.py b/tests/unit/test_position_conversion.py index d8be06f..fccb734 100644 --- a/tests/unit/test_position_conversion.py +++ b/tests/unit/test_position_conversion.py @@ -1,58 +1,41 @@ +import pytest + from malls.lsp.models import Position from malls.ts.utils import lsp_to_tree_sitter_position, tree_sitter_to_lsp_position - -def test_ascii_chars_only(): - text = b"hello world" - - # h e l l o w o r l d - # 0 1 2 3 4 5 6 7 8 9 10 - # target 'w' - position 6 - - position = Position(line=0, character=6) - - result_position = lsp_to_tree_sitter_position(text, position) - assert result_position == (0, 6) - assert tree_sitter_to_lsp_position(text, result_position) == position - - -def test_with_emoji(): - text = "a๐Ÿš€b".encode() # emoji takes 2 UTF-16 characters, so the emoji is 4 bytes - - # a ๐Ÿš€ b - # 0 1 2 3 - # target 'b' - 3 - - position = Position(line=0, character=3) - - # In bytes should be 5, 1 for 'a', 4 for emoji - - result_position = lsp_to_tree_sitter_position(text, position) - assert result_position == (0, 5) - assert tree_sitter_to_lsp_position(text, result_position) == position - - -def test_ascii_and_multibyte_chars(): - text = "รŸรง๐Ÿ".encode() - - # รŸ รง ๐Ÿ - # 0 1 2 3 - # target '๐Ÿ' - position 2 (start) - - position = Position(line=0, character=2) - - # In bytes รŸ, รง take 2 bytes and emoji 4 -> position 2+2 = 4 - - result_position = lsp_to_tree_sitter_position(text, position) - assert result_position == (0, 4) - assert tree_sitter_to_lsp_position(text, result_position) == position - - -def test_empty_string(): - text = b" " - - position = Position(line=0, character=0) +# (text, input/utf16/lsp position, output/utf8/ts position) +parameters = [ + # h e l l o w o r l d + # 0 1 2 3 4 5 6 7 8 9 10 + # target 'w' - position 6 + ("hello world".encode(), (0, 6), (0, 6)), + # emoji takes 2 UTF-16 characters, so the emoji is 4 bytes + # a ๐Ÿš€ b + # 0 1 2 3 + # target 'b' - 3 + # In bytes should be 5, 1 for 'a', 4 for emoji + ("a๐Ÿš€b".encode(), (0, 3), (0, 5)), + # รŸ รง ๐Ÿ + # 0 1 2 3 + # target '๐Ÿ' - position 2 (start) + # In bytes รŸ, รง take 2 bytes and emoji 4 -> position 2+2 = 4 + ("รŸรง๐Ÿ".encode(), (0, 2), (0, 4)), + (" ".encode(), (0, 0), (0, 0)) + ] +parameter_ids = [ + "ascii_chars_only", + "with_emoji", + "ascii_and_multibyte_chars", + "empty_string" + ] + +@pytest.mark.parametrize("text,lsp_position,ts_position", parameters, ids=parameter_ids) +def test_position_conversion( + text: bytes, + lsp_position: (int, int), + ts_position: (int, int)): + position = Position(line=lsp_position[0], character=lsp_position[1]) result_position = lsp_to_tree_sitter_position(text, position) - assert result_position == (0, 0) + assert result_position == ts_position assert tree_sitter_to_lsp_position(text, result_position) == position From 0a34b091b94e6cfe52e65d0cebb0bcb37dfa9dd0 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 14:31:23 +0200 Subject: [PATCH 25/42] refactor: Paratremize test_find_symbols_in_scope.py --- tests/unit/conftest.py | 10 ++ tests/unit/test_find_symbols_in_scope.py | 178 +++++++++-------------- tests/unit/test_visit_expr.py | 10 +- 3 files changed, 80 insertions(+), 118 deletions(-) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index f8fbbb5..34d8f94 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,6 +1,8 @@ from io import BytesIO import pytest +import tree_sitter_mal as ts_mal +from tree_sitter import Language, Parser from malls.lsp.fsm import LifecycleState @@ -13,3 +15,11 @@ def mute_ls() -> FakeLanguageServer: yield ls if ls.state.current_state != LifecycleState.EXIT: ls.m_exit() + +@pytest.fixture +def mal_language() -> Language: + return Language(ts_mal.language()) + +@pytest.fixture +def utf8_mal_parser(mal_language: Language) -> Parser: + return Parser(mal_language) diff --git a/tests/unit/test_find_symbols_in_scope.py b/tests/unit/test_find_symbols_in_scope.py index 8240380..0de5c37 100644 --- a/tests/unit/test_find_symbols_in_scope.py +++ b/tests/unit/test_find_symbols_in_scope.py @@ -1,118 +1,72 @@ -import tree_sitter_mal as ts_mal -from tree_sitter import Language, Parser +import typing +import pytest +import tree_sitter from malls.ts.utils import find_symbols_in_current_scope -MAL_LANGUAGE = Language(ts_mal.language()) -PARSER = Parser(MAL_LANGUAGE) - -def test_find_symbols_in_category_scope(mal_find_symbols_in_scope): - tree = PARSER.parse(mal_find_symbols_in_scope.read()) - - # space between category and asset (category scope) - point = (4, 0) - - # symbols (identifiers + keywords) - user_symbols = [ - "Asset1", - "Asset2", - "Asset3", - ] - keywords = ["extends", "abstract", "asset", "info"] - - # we use sets to ensure order does not matter - returned_user_symbols, returned_keywords = find_symbols_in_current_scope(tree.walk(), point) - - assert set(returned_user_symbols) == set(user_symbols) - assert set(returned_keywords) == set(keywords) - - -def test_find_symbols_in_association_scope(mal_find_symbols_in_scope): - tree = PARSER.parse(mal_find_symbols_in_scope.read()) - - # position in association scope - point = (17, 0) - - # symbols (identifiers + keywords) - user_symbols = [ - "a", - "c", - "d", - "e", - "L", - "M", - "Asset1", - "Asset2", - ] - keywords = [ - "info", - ] - - # we use sets to ensure order does not matter - returned_user_symbols, returned_keywords = find_symbols_in_current_scope(tree.walk(), point) - - assert set(returned_user_symbols) == set(user_symbols) - assert set(returned_keywords) == set(keywords) - - -def test_find_symbols_in_asset1_scope(mal_find_symbols_in_scope): - tree = PARSER.parse(mal_find_symbols_in_scope.read()) - - # position in Asset1 scope - point = (7, 10) - - # symbols (identifiers + keywords) - user_symbols = [ - "var", - "c", - "compromise", - "destroy", - ] - keywords = ["let", "info"] - - # we use sets to ensure order does not matter - returned_user_symbols, returned_keywords = find_symbols_in_current_scope(tree.walk(), point) - - assert set(returned_user_symbols) == set(user_symbols) - assert set(returned_keywords) == set(keywords) - - -def test_find_symbols_in_asset2_scope(mal_find_symbols_in_scope): - tree = PARSER.parse(mal_find_symbols_in_scope.read()) - - # position in Asset2 scope - point = (13, 10) - - # symbols (identifiers + keywords) - user_symbols = [ - "destroy", - ] - keywords = ["info", "let"] +@pytest.fixture +def find_symbols_in_scope_tree( + utf8_mal_parser: tree_sitter.Parser, + mal_find_symbols_in_scope: typing.BinaryIO) -> tree_sitter.Tree: + return utf8_mal_parser.parse(mal_find_symbols_in_scope.read()) + +@pytest.fixture +def find_symbols_in_scope_cursor( + find_symbols_in_scope_tree: tree_sitter.Tree) -> tree_sitter.TreeCursor: + return find_symbols_in_scope_tree.walk() + +# (syntax tree/ts point, expected user symbols, expected keywords) +parameters = [((4, 0), + {"Asset1", + "Asset2", + "Asset3"}, + {"extends", + "abstract", + "asset", + "info"}), + ((17, 0), + {"a", + "c", + "d", + "e", + "L", + "M", + "Asset1", + "Asset2", + }, + {"info"}), + ((7, 10), + {"var", + "c", + "compromise", + "destroy"}, + {"let", "info"}), + ((13, 10), + {"destroy"}, + {"info", "let"}), + ((1, 0), + set(), + {"category", "associations", "info"})] + +parameter_ids = ["category_scope", + "association_scope", + "asset1_scope", + "asset2_scope", + "root_node_scope"] + +@pytest.mark.parametrize( + "point,expected_user_symbols,expected_keywords", + parameters, + ids=parameter_ids) +def test_find_symbols( + point: (int, int), + expected_user_symbols: set[str], + expected_keywords: set[str], + find_symbols_in_scope_cursor: tree_sitter.TreeCursor): + + user_symbols, keywords = find_symbols_in_current_scope(find_symbols_in_scope_cursor, point) # we use sets to ensure order does not matter - returned_user_symbols, returned_keywords = find_symbols_in_current_scope(tree.walk(), point) - - assert set(returned_user_symbols) == set(user_symbols) - assert set(returned_keywords) == set(keywords) - - -def test_find_symbols_in_root_node_scope(mal_find_symbols_in_scope): - tree = PARSER.parse(mal_find_symbols_in_scope.read()) - - # position in Asset2 scope - point = (1, 0) - - # symbols (identifiers + keywords) - user_symbols = [] - keywords = [ - "category", - "associations", - "info", - ] - - # we use sets to ensure order does not matter - returned_user_symbols, returned_keywords = find_symbols_in_current_scope(tree.walk(), point) - - assert set(returned_user_symbols) == set(user_symbols) - assert set(returned_keywords) == set(keywords) + assert set(user_symbols) == expected_user_symbols + assert set(keywords) == expected_keywords diff --git a/tests/unit/test_visit_expr.py b/tests/unit/test_visit_expr.py index 52ebf7b..e752003 100644 --- a/tests/unit/test_visit_expr.py +++ b/tests/unit/test_visit_expr.py @@ -1,17 +1,15 @@ import typing import pytest -import tree_sitter_mal as ts_mal -from tree_sitter import Language, Parser, Tree, TreeCursor +from tree_sitter import Parser, Tree, TreeCursor from malls.ts.utils import visit_expr -MAL_LANGUAGE = Language(ts_mal.language()) -PARSER = Parser(MAL_LANGUAGE) @pytest.fixture -def tree(mal_visit_expr: typing.BinaryIO) -> Tree: - return PARSER.parse(mal_visit_expr.read()) +def tree(utf8_mal_parser: Parser, + mal_visit_expr: typing.BinaryIO) -> Tree: + return utf8_mal_parser.parse(mal_visit_expr.read()) @pytest.fixture def cursor(tree: Tree) -> TreeCursor: From 0f8900b6e452c0cb451b3323b01d69517d1275e0 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 14:59:28 +0200 Subject: [PATCH 26/42] refactor: Paratremize test_find_symbols_in_context_hierarchy.py --- .../test_find_symbols_in_context_hierarchy.py | 303 ++++++------------ 1 file changed, 105 insertions(+), 198 deletions(-) diff --git a/tests/unit/test_find_symbols_in_context_hierarchy.py b/tests/unit/test_find_symbols_in_context_hierarchy.py index 6b0740a..e0711fe 100644 --- a/tests/unit/test_find_symbols_in_context_hierarchy.py +++ b/tests/unit/test_find_symbols_in_context_hierarchy.py @@ -1,207 +1,114 @@ +import pytest import tree_sitter_mal as ts_mal -from tree_sitter import Language, Parser +from tree_sitter import Language, Parser, TreeCursor from malls.ts.utils import find_symbols_in_context_hierarchy MAL_LANGUAGE = Language(ts_mal.language()) PARSER = Parser(MAL_LANGUAGE) - -def test_find_symbols_in_category_hierarchy(mal_find_symbols_in_scope): - tree = PARSER.parse(mal_find_symbols_in_scope.read()) - - # space between category and asset (category scope) - point = (4, 0) - - # symbols (identifiers + keywords) - symbols = [ - ("var", -1), - ("c", -1), - ("compromise", -1), - ("destroy", -1), - ("Asset1", 0), - ("Asset2", 0), - ("Asset3", 0), - ] - keywords = [ - ("let", -1), - ("extends", 0), - ("abstract", 0), - ("asset", 0), - ("info", 1), - ("category", 1), - ("associations", 1), - ] - - # we use sets to ensure order does not matter - returned_user_symbols, returned_keywords = find_symbols_in_context_hierarchy(tree.walk(), point) - - assert len(returned_user_symbols.keys()) == len(symbols) - assert len(returned_keywords.keys()) == len(keywords) - - # check hierarchy levels are correct - for symbol, lvl in symbols: - assert symbol in returned_user_symbols - assert returned_user_symbols[symbol][1] == lvl - for keyword, lvl in keywords: - assert keyword in returned_keywords - assert returned_keywords[keyword][1] == lvl - - -def test_find_symbols_in_associations_hierarchy(mal_find_symbols_in_scope): - tree = PARSER.parse(mal_find_symbols_in_scope.read()) - - point = (17, 0) - - # symbols (identifiers + keywords) - symbols = [ - ("a", 0), - ("c", 0), - ("d", 0), - ("e", 0), - ("L", 0), - ("M", 0), - ("Asset1", 0), - ("Asset2", 0), - ] - keywords = [ - ("info", 1), - ("category", 1), - ("associations", 1), - ] - - # we use sets to ensure order does not matter - returned_user_symbols, returned_keywords = find_symbols_in_context_hierarchy(tree.walk(), point) - - assert len(returned_user_symbols.keys()) == len(symbols) - assert len(returned_keywords.keys()) == len(keywords) - - # check hierarchy levels are correct - for symbol, lvl in symbols: - assert symbol in returned_user_symbols - assert returned_user_symbols[symbol][1] == lvl - for keyword, lvl in keywords: - assert keyword in returned_keywords - assert returned_keywords[keyword][1] == lvl - - -def test_find_symbols_in_asset1_hierarchy(mal_find_symbols_in_scope): - tree = PARSER.parse(mal_find_symbols_in_scope.read()) - - point = (6, 4) - - # symbols (identifiers + keywords) - symbols = [ - ("var", 0), - ("c", 0), - ("compromise", 0), - ("destroy", 0), - ("Asset1", 1), - ("Asset2", 1), - ("Asset3", 1), - ] - keywords = [ - ("let", 0), - ("extends", 1), - ("abstract", 1), - ("asset", 1), - ("category", 2), - ("associations", 2), - ("info", 2), - ] - - # we use sets to ensure order does not matter - returned_user_symbols, returned_keywords = find_symbols_in_context_hierarchy(tree.walk(), point) - - assert len(returned_user_symbols.keys()) == len(symbols) - assert len(returned_keywords.keys()) == len(keywords) - - # check hierarchy levels are correct - for symbol, lvl in symbols: - assert symbol in returned_user_symbols - assert returned_user_symbols[symbol][1] == lvl - for keyword, lvl in keywords: - assert keyword in returned_keywords - assert returned_keywords[keyword][1] == lvl - - -def test_find_symbols_in_asset2_hierarchy(mal_find_symbols_in_scope): - tree = PARSER.parse(mal_find_symbols_in_scope.read()) - - point = (12, 4) - - # symbols (identifiers + keywords) - symbols = [ - ("destroy", 0), - ("Asset1", 1), - ("Asset2", 1), - ("Asset3", 1), - ] - keywords = [ - ("let", 0), - ("extends", 1), - ("abstract", 1), - ("asset", 1), - ("category", 2), - ("associations", 2), - ("info", 2), - ] - - # we use sets to ensure order does not matter - returned_user_symbols, returned_keywords = find_symbols_in_context_hierarchy(tree.walk(), point) - - assert len(returned_user_symbols.keys()) == len(symbols) - assert len(returned_keywords.keys()) == len(keywords) - - # check hierarchy levels are correct - for symbol, lvl in symbols: - assert symbol in returned_user_symbols - assert returned_user_symbols[symbol][1] == lvl - for keyword, lvl in keywords: - assert keyword in returned_keywords - assert returned_keywords[keyword][1] == lvl - - -def test_find_symbols_in_root_node_hierarchy(mal_find_symbols_in_scope): - tree = PARSER.parse(mal_find_symbols_in_scope.read()) - - point = (0, 0) - - # symbols (identifiers + keywords) - symbols = [ - ("var", -2), - ("compromise", -2), - ("destroy", -2), - ("Asset1", -1), - ("Asset2", -1), - ("Asset3", -1), - ("a", -1), - ("d", -1), - ("e", -1), - ("L", -1), - ("M", -1), - ("c", -1), - ] - keywords = [ - ("let", -2), - ("extends", -1), - ("abstract", -1), - ("asset", -1), - ("info", -1), - ("category", 0), - ("associations", 0), - ] - - # we use sets to ensure order does not matter - returned_user_symbols, returned_keywords = find_symbols_in_context_hierarchy(tree.walk(), point) - - assert len(returned_user_symbols.keys()) == len(symbols) - assert len(returned_keywords.keys()) == len(keywords) +# (syntax tree/ts point, expected user symbols + level, expected keywords + level) +parameters = [((4, 0), + [("var", -1), + ("c", -1), + ("compromise", -1), + ("destroy", -1), + ("Asset1", 0), + ("Asset2", 0), + ("Asset3", 0)], + [("let", -1), + ("extends", 0), + ("abstract", 0), + ("asset", 0), + ("info", 1), + ("category", 1), + ("associations", 1)]), + ((17, 0), + [("a", 0), + ("c", 0), + ("d", 0), + ("e", 0), + ("L", 0), + ("M", 0), + ("Asset1", 0), + ("Asset2", 0)], + [("info", 1), + ("category", 1), + ("associations", 1)]), + ((6, 4), + [("var", 0), + ("c", 0), + ("compromise", 0), + ("destroy", 0), + ("Asset1", 1), + ("Asset2", 1), + ("Asset3", 1)], + [("let", 0), + ("extends", 1), + ("abstract", 1), + ("asset", 1), + ("category", 2), + ("associations", 2), + ("info", 2)]), + ((12, 4), + [("destroy", 0), + ("Asset1", 1), + ("Asset2", 1), + ("Asset3", 1)], + [("let", 0), + ("extends", 1), + ("abstract", 1), + ("asset", 1), + ("category", 2), + ("associations", 2), + ("info", 2)]), + ((0, 0), + [("var", -2), + ("compromise", -2), + ("destroy", -2), + ("Asset1", -1), + ("Asset2", -1), + ("Asset3", -1), + ("a", -1), + ("d", -1), + ("e", -1), + ("L", -1), + ("M", -1), + ("c", -1)], + [("let", -2), + ("extends", -1), + ("abstract", -1), + ("asset", -1), + ("info", -1), + ("category", 0), + ("associations", 0)])] + +parameter_ids = ["category", + "associations", + "asset1", + "asset2", + "root_node"] + +pytest_plugins = ["tests.unit.test_find_symbols_in_scope"] + +@pytest.mark.parametrize( + "point,expected_symbols,expected_keywords", + parameters, + ids=parameter_ids) +def test_find_symbols_in_hierarchy( + point: (int, int), + expected_symbols: list[(str, int)], + expected_keywords: list[(str, int)], + find_symbols_in_scope_cursor: TreeCursor): + user_symbols, keywords = find_symbols_in_context_hierarchy(find_symbols_in_scope_cursor, point) + + assert len(user_symbols.keys()) == len(expected_symbols) + assert len(keywords.keys()) == len(expected_keywords) # check hierarchy levels are correct - for symbol, lvl in symbols: - assert symbol in returned_user_symbols - assert returned_user_symbols[symbol][1] == lvl - for keyword, lvl in keywords: - assert keyword in returned_keywords - assert returned_keywords[keyword][1] == lvl + for symbol, lvl in expected_symbols: + assert symbol in user_symbols + assert user_symbols[symbol][1] == lvl + for keyword, lvl in expected_keywords: + assert keyword in keywords + assert keywords[keyword][1] == lvl From 08e337487485a7aec3ada7638f7863225b2e57e5 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 15:56:30 +0200 Subject: [PATCH 27/42] fix: Incorrect contentChanges for change_with_error fixture --- tests/fixtures/lsp/publish_diagnostics.py | 6 +++--- tests/integration/test_publish_diagnostics.py | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/fixtures/lsp/publish_diagnostics.py b/tests/fixtures/lsp/publish_diagnostics.py index f28a028..7578c0e 100644 --- a/tests/fixtures/lsp/publish_diagnostics.py +++ b/tests/fixtures/lsp/publish_diagnostics.py @@ -84,10 +84,10 @@ def did_change_with_error_notification( "contentChanges": [ { "range": { - "start": {"line": 4, "character": 19}, - "end": {"line": 5, "character": 28}, + "start": {"line": 5, "character": 6}, + "end": {"line": 6, "character": 0}, }, - "text": "Bar {}\nasset Foo extends Bar {}", + "text": "FooFoo extds Foo {}\n", } ], } diff --git a/tests/integration/test_publish_diagnostics.py b/tests/integration/test_publish_diagnostics.py index 5338c35..e96bcbe 100644 --- a/tests/integration/test_publish_diagnostics.py +++ b/tests/integration/test_publish_diagnostics.py @@ -74,9 +74,6 @@ def test_diagnostics_when_opening_file_with_include_error_and_opening_bad_file( output.close() -# FIXME: -@pytest.mark.skip(("Server is not sending diagnostics for unknown reasons. " - "Only sending initalize response.")) def test_diagnostics_when_changing_file_with_error( change_file_with_error_client_messages: typing.BinaryIO): # send to server From 92e8431f042c318d609a9ee34fd4960ea29fa99c Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 16:53:34 +0200 Subject: [PATCH 28/42] fix: recursive_parsing not accepting paths not ending with slash --- src/malls/lsp/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/malls/lsp/utils.py b/src/malls/lsp/utils.py index 7216146..fbdf152 100644 --- a/src/malls/lsp/utils.py +++ b/src/malls/lsp/utils.py @@ -44,7 +44,7 @@ def recursive_parsing( while captures: # build file path - file_name = uri_prec + captures.pop(0).text.decode().strip('"') + file_name = os.path.join(uri_prec, captures.pop(0).text.decode().strip('"')) # if the file has already been processed, ignore it # (this can happen if file A was opened with a didOpen notification From 392d3fb184360e94d0280573a01b16a4ac7e26f6 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 16:54:58 +0200 Subject: [PATCH 29/42] feat: Add tests/ and tests/fixtures/mal paths as fixtures --- tests/conftest.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index d6af2f6..bd92d7e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -67,3 +67,17 @@ def template() -> str: ) setattr(module, fixture_name + "_uri", uri_fixture) + +TESTS_ROOT = Path(__file__).parent + +@pytest.fixture +def tests_root() -> Path: + return TESTS_ROOT + +@pytest.fixture +def mal_root(tests_root: Path) -> Path: + return tests_root.joinpath("fixtures", "mal") + +@pytest.fixture +def mal_root_str(mal_root: Path) -> str: + return str(mal_root) From 8633b39c4180775f69dac229ddf9b98016bca577 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 16:56:55 +0200 Subject: [PATCH 30/42] refactor: Simplify paratremization for test_find_symbol_definition_function.py --- .../test_find_symbol_definition_function.py | 181 ++++++++---------- .../test_find_symbols_in_context_hierarchy.py | 6 +- 2 files changed, 84 insertions(+), 103 deletions(-) diff --git a/tests/unit/test_find_symbol_definition_function.py b/tests/unit/test_find_symbol_definition_function.py index 5c374ed..edbdc66 100644 --- a/tests/unit/test_find_symbol_definition_function.py +++ b/tests/unit/test_find_symbol_definition_function.py @@ -1,41 +1,38 @@ -from pathlib import Path +from itertools import chain, repeat import pytest -import tree_sitter_mal as ts_mal -from tree_sitter import Language, Parser +from tree_sitter import Parser, TreeCursor from malls.lsp.classes import Document from malls.lsp.utils import recursive_parsing from malls.ts.utils import INCLUDED_FILES_QUERY, find_symbol_definition, run_query -MAL_LANGUAGE = Language(ts_mal.language()) -PARSER = Parser(MAL_LANGUAGE) -FILE_PATH = str(Path(__file__).parent.parent.resolve()) + "/fixtures/mal/" +pytest_plugins = ["tests.unit.test_find_symbols_in_scope"] - -mal_find_symbols_in_scope_points = [ - ((3, 12), (3, 0)), # category_declaration - ((11, 10), (11, 4)), # asset declaration, asset name - ((7, 10), (7, 6)), # asset variable - ((8, 10), (8, 8)), # attack step -] +mal_find_symbols_in_scope_points = zip( + [(3, 12), # category_declaration + (11, 10), # asset declaration, asset name + (7, 10), # asset variable + (8, 10), # attack step + ], + [(3, 0), + (11, 4), + (7, 6), + (8, 8)]) @pytest.mark.parametrize( - "file_name,point,expected_result", - [ - ("mal_find_symbols_in_scope", point, expected_result) - for point, expected_result in mal_find_symbols_in_scope_points - ], -) -def test_symbol_definition_withouth_building_storage(request, file_name, point, expected_result): - file = request.getfixturevalue(file_name) - tree = PARSER.parse(file.read()) - + "point,expected_result", + mal_find_symbols_in_scope_points) +def test_symbol_definition_withouth_building_storage( + request: pytest.FixtureRequest, + point: (int, int), + expected_result: (int, int), + find_symbols_in_scope_cursor: TreeCursor): # go to name of category # get the node - cursor = tree.walk() + cursor = find_symbols_in_scope_cursor while cursor.goto_first_child_for_point(point) is not None: continue @@ -48,88 +45,76 @@ def test_symbol_definition_withouth_building_storage(request, file_name, point, assert response[0].start_point == expected_result -mal_symbol_def_extended_asset_main_points = [ - ((6, 25), (2, 4)), # asset declaration, extended asset - ((9, 11), (4, 6)), # variable call -] -mal_symbol_def_variable_call_extend_chain_main_points = [ - ((9, 11), (5, 6)) # variable call, extend chain -] -symbol_def_variable_declaration_main_points = [ - ((10, 20), (5, 4)), # variable declaration - ((17, 21), (13, 4)), # variable declaration, extended asset - ((21, 36), (15, 4)), # variable declaration complex 1 - ((22, 36), (15, 4)), # variable declaration complex 2 - ((26, 6), (8, 4)), # association asset name 1 - ((27, 37), (7, 4)), # association asset name 2 - ((28, 12), (28, 4)), # association field name 1 - ((28, 32), (28, 4)), # association field name 2 - ((30, 22), (30, 4)), # link name -] -mal_symbol_def_preconditions_points = [ - ((11, 15), (5, 4)), # preconditions - ((19, 13), (14, 4)), # preconditions extended asset - ((24, 28), (16, 4)), # preconditions complex 1 - ((26, 28), (16, 4)), # preconditions complex 1 -] -mal_symbol_def_reaches_points = [ - ((13, 22), (6, 8)), # reaches - ((14, 14), (15, 6)), # reaches single attack step -] +mal_symbol_def_extended_asset_main_points = zip( + repeat("mal_symbol_def_extended_asset_main"), + [(6, 25), # asset declaration, extended asset + (9, 11)], # variable call + [(2, 4), + (4, 6)]) +mal_symbol_def_variable_call_extend_chain_main_points = zip( + repeat("mal_symbol_def_variable_call_extend_chain_main"), + [(9, 11)], # variable call, extend chain + [(5, 6)]) +symbol_def_variable_declaration_main_points = zip( + repeat("mal_symbol_def_variable_declaration_main"), + [(10, 20), # variable declaration + (17, 21), # variable declaration, extended asset + (21, 36), # variable declaration complex 1 + (22, 36), # variable declaration complex 2 + (26, 6), # association asset name 1 + (27, 37), # association asset name 2 + (28, 12), # association field name 1 + (28, 32), # association field name 2 + (30, 22)], # link name + [(5, 4), + (13, 4), + (15, 4), + (15, 4), + (8, 4), + (7, 4), + (28, 4), + (28, 4), + (30, 4)]) +mal_symbol_def_preconditions_points = zip( + repeat("mal_symbol_def_preconditions"), + [(11, 15), # preconditions + (19, 13), # preconditions extended asset + (24, 28), # preconditions complex 1 + (26, 28)], # preconditions complex 1 + [(5 , 4), + (14, 4), + (16, 4), + (16, 4)]) +mal_symbol_def_reaches_points = zip( + repeat("mal_symbol_def_reaches"), + [(13, 22), # reaches + (14, 14)], # reaches single attack step + [(6 , 8), + (15, 6)]) + +parameters = chain(mal_symbol_def_extended_asset_main_points, + mal_symbol_def_variable_call_extend_chain_main_points, + symbol_def_variable_declaration_main_points, + mal_symbol_def_preconditions_points, + mal_symbol_def_reaches_points) @pytest.mark.parametrize( - "file_name,fixture_name,point,expected_result", - [ - ( - "symbol_def_extended_asset_main.mal", - "mal_symbol_def_extended_asset_main", - point, - expected_result, - ) - for point, expected_result in mal_symbol_def_extended_asset_main_points - ] - + [ - ( - "symbol_def_variable_call_extend_chain_main.mal", - "mal_symbol_def_variable_call_extend_chain_main", - point, - expected_result, - ) - for point, expected_result in mal_symbol_def_variable_call_extend_chain_main_points - ] - + [ - ( - "symbol_def_variable_declaration_main.mal", - "mal_symbol_def_variable_declaration_main", - point, - expected_result, - ) - for point, expected_result in symbol_def_variable_declaration_main_points - ] - + [ - ("symbol_def_preconditions.mal", "mal_symbol_def_preconditions", point, expected_result) - for point, expected_result in mal_symbol_def_preconditions_points - ] - + [ - ("symbol_def_reaches.mal", "mal_symbol_def_reaches", point, expected_result) - for point, expected_result in mal_symbol_def_reaches_points - ], -) + "fixture_name,point,expected_result",parameters) def test_symbol_definition_with_storage( - request, - file_name, - fixture_name, - point, - expected_result, -): + request: pytest.FixtureRequest, + utf8_mal_parser: Parser, + mal_root_str: str, + fixture_name: str, + point: (int, int), + expected_result: (int, int)): # build the storage (mimicks the file parsing in the server) storage = {} - doc_uri = FILE_PATH + file_name + doc_uri = request.getfixturevalue(fixture_name + "_uri") file = request.getfixturevalue(fixture_name) source_encoded = file.read() - tree = PARSER.parse(source_encoded) + tree = utf8_mal_parser.parse(source_encoded) storage[doc_uri] = Document(tree, source_encoded, doc_uri) @@ -138,7 +123,7 @@ def test_symbol_definition_with_storage( captures = run_query(root_node, INCLUDED_FILES_QUERY) if "file_name" in captures: - recursive_parsing(FILE_PATH, captures["file_name"], storage, doc_uri, []) + recursive_parsing(mal_root_str, captures["file_name"], storage, doc_uri, []) ################################### diff --git a/tests/unit/test_find_symbols_in_context_hierarchy.py b/tests/unit/test_find_symbols_in_context_hierarchy.py index e0711fe..bf8b211 100644 --- a/tests/unit/test_find_symbols_in_context_hierarchy.py +++ b/tests/unit/test_find_symbols_in_context_hierarchy.py @@ -1,12 +1,8 @@ import pytest -import tree_sitter_mal as ts_mal -from tree_sitter import Language, Parser, TreeCursor +from tree_sitter import TreeCursor from malls.ts.utils import find_symbols_in_context_hierarchy -MAL_LANGUAGE = Language(ts_mal.language()) -PARSER = Parser(MAL_LANGUAGE) - # (syntax tree/ts point, expected user symbols + level, expected keywords + level) parameters = [((4, 0), [("var", -1), From eb0fb3acdc4520f54ef6d6c5806988ea9e5bb834 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:10:12 +0200 Subject: [PATCH 31/42] refactor: Simplify paratremization for test_find_meta_comment_function.py --- tests/unit/test_find_meta_comment_function.py | 60 +++++++++++-------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/tests/unit/test_find_meta_comment_function.py b/tests/unit/test_find_meta_comment_function.py index 839b69b..fdb7572 100644 --- a/tests/unit/test_find_meta_comment_function.py +++ b/tests/unit/test_find_meta_comment_function.py @@ -1,43 +1,53 @@ +import typing from pathlib import Path import pytest -import tree_sitter_mal as ts_mal -from tree_sitter import Language, Parser +from tree_sitter import Parser, Tree from malls.lsp.classes import Document from malls.lsp.utils import recursive_parsing from malls.ts.utils import INCLUDED_FILES_QUERY, find_meta_comment_function, run_query -MAL_LANGUAGE = Language(ts_mal.language()) -PARSER = Parser(MAL_LANGUAGE) -FILE_PATH = str(Path(__file__).parent.parent.resolve()) + "/fixtures/mal/" - parameters = [ - ((3, 12), [b"dev cat", b"mod cat"]), - ((8, 22), [b"dev asset", b"mod asset"]), - ((13, 13), [b"dev attack_step", b"mod attack_step"]), - ((12, 18), [b"dev asset3", b"mod asset3"]), - ((16, 12), [b"dev asset3", b"mod asset3"]), - ((19, 15), [b"dev asset4", b"mod asset4"]), - ((20, 20), [b"dev asset5", b"mod asset5"]), - ((20, 28), [b"dev attack_step_5", b"mod attack_step_5"]), - ((54, 12), [b"some info"]), - ((54, 21), [b"some info"]), - ((57, 37), [b"dev asset4", b"mod asset4"]), + ((3, 12), {b"dev cat", b"mod cat"}), + ((8, 22), {b"dev asset", b"mod asset"}), + ((13, 13), {b"dev attack_step", b"mod attack_step"}), + ((12, 18), {b"dev asset3", b"mod asset3"}), + ((16, 12), {b"dev asset3", b"mod asset3"}), + ((19, 15), {b"dev asset4", b"mod asset4"}), + ((20, 20), {b"dev asset5", b"mod asset5"}), + ((20, 28), {b"dev attack_step_5", b"mod attack_step_5"}), + ((54, 12), {b"some info"}), + ((54, 21), {b"some info"}), + ((57, 37), {b"dev asset4", b"mod asset4"}), ] +@pytest.fixture +def find_meta_comment_data(mal_find_meta_comment_function: typing.BinaryIO) -> bytes: + return mal_find_meta_comment_function.read() + +@pytest.fixture +def find_meta_comment_tree(utf8_mal_parser: Parser, + find_meta_comment_data: bytes) -> Tree: + return utf8_mal_parser.parse(find_meta_comment_data) @pytest.mark.parametrize( - "point,comments", + "point,expected_comments", parameters, ) -def test_find_meta_comment_function(mal_find_meta_comment_function, point, comments): +def test_find_meta_comment_function( + mal_root_str: str, + mal_find_meta_comment_function_uri: str, + find_meta_comment_data: bytes, + find_meta_comment_tree: Tree, + point: (int, int), + expected_comments: list[bytes]): # build the storage (mimicks the file parsing in the server) storage = {} - doc_uri = FILE_PATH + "find_meta_comment_function.mal" - source_encoded = mal_find_meta_comment_function.read() - tree = PARSER.parse(source_encoded) + doc_uri = mal_find_meta_comment_function_uri + source_encoded = find_meta_comment_data + tree = find_meta_comment_tree storage[doc_uri] = Document(tree, source_encoded, doc_uri) @@ -46,7 +56,7 @@ def test_find_meta_comment_function(mal_find_meta_comment_function, point, comme captures = run_query(root_node, INCLUDED_FILES_QUERY) if "file_name" in captures: - recursive_parsing(FILE_PATH, captures["file_name"], storage, doc_uri, []) + recursive_parsing(mal_root_str, captures["file_name"], storage, doc_uri, []) ################################### @@ -59,6 +69,6 @@ def test_find_meta_comment_function(mal_find_meta_comment_function, point, comme assert cursor.node.type == "identifier" # we use sets to ensure order does not matter - returned_comments = find_meta_comment_function(cursor.node, cursor.node.text, doc_uri, storage) + comments = find_meta_comment_function(cursor.node, cursor.node.text, doc_uri, storage) - assert set(returned_comments) == set(comments) + assert set(comments) == expected_comments From d57e78ace0d6469d38fe7f9cf5bacb0d8e566885 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:43:55 +0200 Subject: [PATCH 32/42] refactor: Paratremization for test_find_current_scope.py --- src/malls/ts/utils.py | 2 +- tests/unit/test_find_current_scope.py | 134 +++++++++----------------- 2 files changed, 44 insertions(+), 92 deletions(-) diff --git a/src/malls/ts/utils.py b/src/malls/ts/utils.py index a1f5aea..db29343 100644 --- a/src/malls/ts/utils.py +++ b/src/malls/ts/utils.py @@ -158,7 +158,7 @@ def query_and_compare_scope_pos(query_node_type: str, cursor: TreeCursor, point: return compare_points(start_point, point) -def find_current_scope(cursor: TreeCursor, point: Point): +def find_current_scope(cursor: TreeCursor, point: Point) -> Node: """ Given a cursor and a document position, return the node that "owns" the scope. Scopes are separated by curly brackets - {} diff --git a/tests/unit/test_find_current_scope.py b/tests/unit/test_find_current_scope.py index 7c15ca5..f2724db 100644 --- a/tests/unit/test_find_current_scope.py +++ b/tests/unit/test_find_current_scope.py @@ -1,95 +1,47 @@ -import tree_sitter_mal as ts_mal -from tree_sitter import Language, Parser +import typing -from malls.ts.utils import find_current_scope - -MAL_LANGUAGE = Language(ts_mal.language()) -PARSER = Parser(MAL_LANGUAGE) - - -def test_find_current_scope_on_space_between_category_and_asset(mal_find_current_scope_function): - tree = PARSER.parse(mal_find_current_scope_function.read()) - - # space between category and asset - point = (4, 0) - assert find_current_scope(tree.walk(), point).type == "category_declaration" - - -def test_find_current_scope_inside_the_asset_declaration(mal_find_current_scope_function): - tree = PARSER.parse(mal_find_current_scope_function.read()) - - # inside the asset declaration - point = (7, 13) - assert find_current_scope(tree.walk(), point).type == "asset_declaration" - - -def test_find_current_scope_inside_the_association_declaration(mal_find_current_scope_function): - tree = PARSER.parse(mal_find_current_scope_function.read()) - - # inside the association declaration - point = (17, 19) - assert find_current_scope(tree.walk(), point).type == "associations_declaration" - - -def test_find_current_scope_outside_all_components(mal_find_current_scope_function): - tree = PARSER.parse(mal_find_current_scope_function.read()) - - # outside all components - point = (1, 10) - assert find_current_scope(tree.walk(), point).type == "source_file" - - -def test_find_current_scope_on_name_of_asset(mal_find_current_scope_function): - tree = PARSER.parse(mal_find_current_scope_function.read()) - - # on the name of the asset - point = (10, 10) - assert find_current_scope(tree.walk(), point).type == "category_declaration" - - -def test_find_current_scope_on_bracket_of_asset(mal_find_current_scope_function): - tree = PARSER.parse(mal_find_current_scope_function.read()) - - # on the '{' of the asset - point = (11, 4) - assert find_current_scope(tree.walk(), point).type == "asset_declaration" - - -def test_find_current_scope_on_closing_bracket_of_asset(mal_find_current_scope_function): - tree = PARSER.parse(mal_find_current_scope_function.read()) - - # on the '}' of the asset - point = (13, 4) - assert find_current_scope(tree.walk(), point).type == "asset_declaration" - - -def test_find_current_scope_on_name_of_category(mal_find_current_scope_function): - tree = PARSER.parse(mal_find_current_scope_function.read()) - - # on the name of the category - point = (3, 10) - assert find_current_scope(tree.walk(), point).type == "source_file" - - -def test_find_current_scope_on_bracket_of_category(mal_find_current_scope_function): - tree = PARSER.parse(mal_find_current_scope_function.read()) - - # on the '{' of the category - point = (3, 17) - assert find_current_scope(tree.walk(), point).type == "category_declaration" - - -def test_find_current_scope_on_term_association(mal_find_current_scope_function): - tree = PARSER.parse(mal_find_current_scope_function.read()) - - # on the term 'associations' - point = (15, 4) - assert find_current_scope(tree.walk(), point).type == "source_file" +import pytest +from tree_sitter import Parser, Tree, TreeCursor +from malls.ts.utils import find_current_scope -def test_find_current_scope_on_bracket_of_association(mal_find_current_scope_function): - tree = PARSER.parse(mal_find_current_scope_function.read()) - # on the '{' of the association - point = (18, 0) - assert find_current_scope(tree.walk(), point).type == "associations_declaration" +@pytest.fixture +def find_current_scope_function_tree(utf8_mal_parser: Parser, + mal_find_current_scope_function: typing.BinaryIO) -> Tree: + return utf8_mal_parser.parse(mal_find_current_scope_function.read()) + +@pytest.fixture +def find_current_scope_function_cursor(find_current_scope_function_tree: Tree) -> TreeCursor: + return find_current_scope_function_tree.walk() + +parameters = [((4, 0), "category_declaration"), + ((7, 13), "asset_declaration"), + ((17, 19), "associations_declaration"), + ((1, 10), "source_file"), + ((10, 10), "category_declaration"), + ((11, 4), "asset_declaration"), + ((13, 4), "asset_declaration"), + ((3, 10), "source_file"), + ((3, 17), "category_declaration"), + ((15, 4), "source_file"), + ((18, 0), "associations_declaration")] + +parameter_ids = ["on_space_between_category_and_asset", + "inside_asset_declaration", + "inside_association_declaration", + "outside_all_components", + "on_name_of_asset", + "on_bracket_of_asset", + "on_closing_bracket_of_asset", + "on_name_of_category", + "on_bracket_of_category", + "on_term_association", + "on_bracket_of_association"] + +@pytest.mark.parametrize("point,expected_node_type", parameters, ids=parameter_ids) +def test_find_current_scope(point: (int, int), + expected_node_type: str, + find_current_scope_function_cursor: TreeCursor): + node = find_current_scope(find_current_scope_function_cursor, point) + assert node.type == expected_node_type From c4838b32d1c3a06ccc02a5217a7c0e597cfc5c09 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:57:04 +0200 Subject: [PATCH 33/42] fix: indentation adjustments for test_diagnostics_when_changing_file_with_error --- tests/fixtures/lsp/publish_diagnostics.py | 2 +- tests/integration/test_publish_diagnostics.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/fixtures/lsp/publish_diagnostics.py b/tests/fixtures/lsp/publish_diagnostics.py index 7578c0e..bca48f0 100644 --- a/tests/fixtures/lsp/publish_diagnostics.py +++ b/tests/fixtures/lsp/publish_diagnostics.py @@ -84,7 +84,7 @@ def did_change_with_error_notification( "contentChanges": [ { "range": { - "start": {"line": 5, "character": 6}, + "start": {"line": 5, "character": 10}, "end": {"line": 6, "character": 0}, }, "text": "FooFoo extds Foo {}\n", diff --git a/tests/integration/test_publish_diagnostics.py b/tests/integration/test_publish_diagnostics.py index e96bcbe..d408dba 100644 --- a/tests/integration/test_publish_diagnostics.py +++ b/tests/integration/test_publish_diagnostics.py @@ -89,7 +89,7 @@ def test_diagnostics_when_changing_file_with_error( assert len(params["diagnostics"]) == 1 start_point = params["diagnostics"][0]["range"]["start"] - assert (start_point["line"], start_point["character"]) == (5, 13) + assert (start_point["line"], start_point["character"]) == (5, 17) assert params["diagnostics"][0]["severity"] == DiagnosticSeverity.Error output.close() From 324510f90bb7726d12d21e11e34e210bd17d5721 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 19:51:49 +0200 Subject: [PATCH 34/42] test: Paratremize and use LSP fixture components for test_did_open_text_document_notification.py --- tests/conftest.py | 2 +- tests/fixtures/lsp/conftest.py | 27 ++++---- .../did_open_text_document_notification.py | 69 +++++++++++++++++++ tests/fixtures/lsp/publish_diagnostics.py | 3 +- .../mal/base_open_file_with_fake_include.mal | 11 +++ .../mal/base_open_with_included_file.mal | 9 +++ ...est_did_open_text_document_notification.py | 67 ++++-------------- 7 files changed, 121 insertions(+), 67 deletions(-) create mode 100644 tests/fixtures/lsp/did_open_text_document_notification.py create mode 100644 tests/fixtures/mal/base_open_file_with_fake_include.mal create mode 100644 tests/fixtures/mal/base_open_with_included_file.mal diff --git a/tests/conftest.py b/tests/conftest.py index bd92d7e..895a8e4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -52,7 +52,7 @@ def template() -> typing.BinaryIO: def fixture_uri(file: str): path = Path(file) file_path = path.resolve() - uri = file_path.as_uri() + uri = str(file_path.as_uri()) def template() -> str: return uri diff --git a/tests/fixtures/lsp/conftest.py b/tests/fixtures/lsp/conftest.py index aea78af..99357a0 100644 --- a/tests/fixtures/lsp/conftest.py +++ b/tests/fixtures/lsp/conftest.py @@ -270,15 +270,18 @@ def did_change_notification( return message @pytest.fixture -def did_open_base_open_notification( - client_notifications: list[dict], - client_messages: list[dict], - did_open_notification, - mal_base_open: typing.BinaryIO, - mal_base_open_uri: str): - # since did_open_notification is a dependency here - # we know the notification is the latest one - open_notification = client_notifications[-1] - text_doc_params = open_notification["params"]["textDocument"] - text_doc_params["uri"] = mal_base_open_uri - text_doc_params["text"] = mal_base_open.read().decode("utf8") +def client_initalize_procedures(initalize_request, + initalized_notification): + """ + Adds the relevant messages from client to server so that both client and server are initalized. + """ + pass + +@pytest.fixture +def client_shutdown_procedures(shutdown_request, + exit_notification): + """ + Adds the relevant messages from client to server to shut down the server. Assumes at + non-erreneous and post-initalized server state. + """ + pass diff --git a/tests/fixtures/lsp/did_open_text_document_notification.py b/tests/fixtures/lsp/did_open_text_document_notification.py new file mode 100644 index 0000000..23e681f --- /dev/null +++ b/tests/fixtures/lsp/did_open_text_document_notification.py @@ -0,0 +1,69 @@ +import typing + +import pytest + + +# So that importers are aware of the conftest fixtures +pytest_plugins = ["tests.fixtures.lsp.conftest"] + +@pytest.fixture +def did_open_base_open_notification( + client_notifications: list[dict], + client_messages: list[dict], + did_open_notification, + mal_base_open: typing.BinaryIO, + mal_base_open_uri: str): + # since did_open_notification is a dependency here + # we know the notification is the latest one + open_notification = client_notifications[-1] + text_doc_params = open_notification["params"]["textDocument"] + text_doc_params["uri"] = mal_base_open_uri + text_doc_params["text"] = mal_base_open.read().decode("utf8") + +@pytest.fixture +def base_open_client_messages(client_initalize_procedures, + did_open_base_open_notification, + client_rpc_messages: typing.BinaryIO) -> typing.BinaryIO: + return client_rpc_messages + +@pytest.fixture +def did_open_base_open_file_with_fake_include_notification( + client_notifications: list[dict], + client_messages: list[dict], + did_open_notification, + mal_base_open_file_with_fake_include: typing.BinaryIO, + mal_base_open_file_with_fake_include_uri: str): + # since did_open_notification is a dependency here + # we know the notification is the latest one + open_notification = client_notifications[-1] + text_doc_params = open_notification["params"]["textDocument"] + text_doc_params["uri"] = mal_base_open_file_with_fake_include_uri + text_doc_params["text"] = mal_base_open_file_with_fake_include.read().decode("utf8") + +@pytest.fixture +def base_open_file_with_fake_include_client_messages( + client_initalize_procedures, + did_open_base_open_file_with_fake_include_notification, + client_rpc_messages: typing.BinaryIO) -> typing.BinaryIO: + return client_rpc_messages + +@pytest.fixture +def did_open_base_open_with_included_file_notification( + client_notifications: list[dict], + client_messages: list[dict], + did_open_notification, + mal_base_open_with_included_file: typing.BinaryIO, + mal_base_open_with_included_file_uri: str): + # since did_open_notification is a dependency here + # we know the notification is the latest one + open_notification = client_notifications[-1] + text_doc_params = open_notification["params"]["textDocument"] + text_doc_params["uri"] = mal_base_open_with_included_file_uri + text_doc_params["text"] = mal_base_open_with_included_file.read().decode("utf8") + +@pytest.fixture +def base_open_with_included_file_client_messages( + client_initalize_procedures, + did_open_base_open_with_included_file_notification, + client_rpc_messages: typing.BinaryIO) -> typing.BinaryIO: + return client_rpc_messages diff --git a/tests/fixtures/lsp/publish_diagnostics.py b/tests/fixtures/lsp/publish_diagnostics.py index bca48f0..409d843 100644 --- a/tests/fixtures/lsp/publish_diagnostics.py +++ b/tests/fixtures/lsp/publish_diagnostics.py @@ -3,7 +3,8 @@ import pytest # So that importers are aware of the conftest fixtures -pytest_plugins = ["tests.fixtures.lsp.conftest"] +pytest_plugins = ["tests.fixtures.lsp.conftest", + "tests.fixtures.lsp.did_open_text_document_notification"] @pytest.fixture diff --git a/tests/fixtures/mal/base_open_file_with_fake_include.mal b/tests/fixtures/mal/base_open_file_with_fake_include.mal new file mode 100644 index 0000000..a59e499 --- /dev/null +++ b/tests/fixtures/mal/base_open_file_with_fake_include.mal @@ -0,0 +1,11 @@ +#id: "org.mal-lang.testAnalyzer" +#version:"0.0.0" + +include "random_file_that_does_not_exist.mal" +category System +{ + abstract asset Foo {} + asset Bar extends Foo {} +} + + diff --git a/tests/fixtures/mal/base_open_with_included_file.mal b/tests/fixtures/mal/base_open_with_included_file.mal new file mode 100644 index 0000000..2d006b5 --- /dev/null +++ b/tests/fixtures/mal/base_open_with_included_file.mal @@ -0,0 +1,9 @@ +#id: "org.mal-lang.testAnalyzer" +#version:"0.0.0" + +include "find_current_scope_function.mal" +category System +{ + abstract asset Foo {} + asset Bar extends Foo {} +} diff --git a/tests/integration/test_did_open_text_document_notification.py b/tests/integration/test_did_open_text_document_notification.py index a473216..204ccb2 100644 --- a/tests/integration/test_did_open_text_document_notification.py +++ b/tests/integration/test_did_open_text_document_notification.py @@ -1,67 +1,28 @@ import typing -from pathlib import Path +import pytest from tree_sitter import Tree from ..util import server_output -# calculate file path of mal files -FILE_PATH = str(Path(__file__).parent.parent.resolve()) + "/fixtures/mal/" -simplified_file_path = FILE_PATH + "main.mal" +pytest_plugins = ["tests.fixtures.lsp.did_open_text_document_notification"] +parameters = ["base_open", + "base_open_file_with_fake_include", + "base_open_with_included_file"] -def filepath_to_uri(filepath: str) -> str: - """ - Converts a native filesystem path to a file:// URI. - """ - path_obj = Path(filepath) +@pytest.mark.parametrize("file", parameters, ids=parameters) +def test_open_file(request: pytest.FixtureRequest, file: str): + file_fixture: typing.BinaryIO = request.getfixturevalue(f"{file}_client_messages") + uri_fixture: str = request.getfixturevalue(f"mal_{file}_uri") - absolute_path_obj = path_obj.resolve() - - return absolute_path_obj.as_uri() - - -def test_open_file_without_include( - writeable_fixtures_did_open_notif_in_base_open_file: typing.BinaryIO, -): - # send to server - output, ls, *_ = server_output(writeable_fixtures_did_open_notif_in_base_open_file) - - # Ensure LSP stored everything correctly - assert simplified_file_path in ls.files.keys() - assert type(ls.files[simplified_file_path].tree) is Tree - - output.close() - - -def test_open_file_with_include( - writeable_fixtures_did_open_notif_in_with_included_file: typing.BinaryIO, -): - # path for the included file - included_path_file = FILE_PATH + "find_current_scope_function.mal" - - # send to server - output, ls, *_ = server_output(writeable_fixtures_did_open_notif_in_with_included_file) - - # Ensure LSP stored everything correctly - assert len(ls.files.keys()) == 2 - assert simplified_file_path in ls.files.keys() - assert type(ls.files[simplified_file_path].tree) is Tree - assert included_path_file in ls.files.keys() - assert type(ls.files[included_path_file].tree) is Tree - - output.close() - - -def test_open_file_with_non_existant_include( - writeable_fixtures_did_open_notif_in_with_fake_include: typing.BinaryIO, -): # send to server - output, ls, *_ = server_output(writeable_fixtures_did_open_notif_in_with_fake_include) + output, ls, *_ = server_output(file_fixture) + # since Document acts inconsistent with URIs + uri_fixture = uri_fixture[len("file://"):] # Ensure LSP stored everything correctly - assert len(ls.files.keys()) == 1 - assert simplified_file_path in ls.files.keys() - assert type(ls.files[simplified_file_path].tree) is Tree + assert uri_fixture in ls.files + assert type(ls.files[uri_fixture].tree) is Tree output.close() From fdfa3261ce30a67da86db4f54b3abd787249209e Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 20:57:40 +0200 Subject: [PATCH 35/42] test: Paratremize and use LSP fixture components in test_diagnostics_are_saved.py --- .../integration/test_diagnostics_are_saved.py | 78 +++++++++---------- 1 file changed, 35 insertions(+), 43 deletions(-) diff --git a/tests/integration/test_diagnostics_are_saved.py b/tests/integration/test_diagnostics_are_saved.py index 729aa47..5e9cac3 100644 --- a/tests/integration/test_diagnostics_are_saved.py +++ b/tests/integration/test_diagnostics_are_saved.py @@ -1,53 +1,45 @@ -import typing -from pathlib import Path +import pytest from malls.lsp.enums import DiagnosticSeverity from ..util import server_output -# calculate file path of mal files -FILE_PATH = str(Path(__file__).parent.parent.resolve()) + "/fixtures/mal/" -simplified_file_path = FILE_PATH + "main.mal" -included_file_path = FILE_PATH + "file_with_error.mal" - - -def test_open_file_with_error( - open_file_with_error: typing.BinaryIO, -): - # send to server - output, ls, *_ = server_output(open_file_with_error) - - # Ensure LSP stored everything correctly - assert len(ls.diagnostics) == 1 - assert len(ls.diagnostics[simplified_file_path]) == 1 - assert ls.diagnostics[simplified_file_path][0]["severity"] == DiagnosticSeverity.Error - - output.close() - - -def test_open_file_with_include_error( - open_file_with_include_error: typing.BinaryIO, -): - # send to server - output, ls, *_ = server_output(open_file_with_include_error) - - # Ensure LSP stored everything correctly - assert len(ls.diagnostics) == 1 - assert len(ls.diagnostics[included_file_path]) == 1 - assert ls.diagnostics[included_file_path][0]["severity"] == DiagnosticSeverity.Error - - output.close() - - -def test_change_file_with_error( - change_file_with_error: typing.BinaryIO, -): - # send to server - output, ls, *_ = server_output(change_file_with_error) +pytest_plugins = ["tests.fixtures.lsp.publish_diagnostics"] + +parameters = [("erroneous_file_client_messages", + "mal_erroneous_uri", + DiagnosticSeverity.Error), + ("erroenous_include_and_file_with_error_client_messages", + "mal_file_with_error_uri", + DiagnosticSeverity.Error), + ("change_file_with_error_client_messages", + "mal_base_open_uri", + DiagnosticSeverity.Error)] +parameter_ids = ["erroneous_file", + "erroneous_include_file", + "change_file_with_error"] + +@pytest.mark.parametrize( + "messages_fixture_name,uri_fixture_name,error_level", + parameters, + ids=parameter_ids) +def test_open_file(request: pytest.FixtureRequest, + messages_fixture_name: str, + uri_fixture_name: str, + error_level: DiagnosticSeverity): + input = request.getfixturevalue(messages_fixture_name) + uri = request.getfixturevalue(uri_fixture_name) + + # since Document acts inconsistent with URIs + uri = uri[len("file://"):] + + # send to server + output, ls, *_ = server_output(input) # Ensure LSP stored everything correctly assert len(ls.diagnostics) == 1 - assert len(ls.diagnostics[simplified_file_path]) == 1 - assert ls.diagnostics[simplified_file_path][0]["severity"] == DiagnosticSeverity.Error + assert uri in ls.diagnostics + assert len(ls.diagnostics[uri]) == 1 + assert ls.diagnostics[uri][0]["severity"] == error_level output.close() From e77f7fb0002053ed5b08f81600b07bca3338d971 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 22:52:18 +0200 Subject: [PATCH 36/42] test: Paratremize and use LSP fixture components for test_did_change_text_document_notification.py --- src/malls/lsp/models.py | 17 +- tests/conftest.py | 10 + tests/fixtures/lsp/did_change.py | 132 +++++++++++++ .../did_open_text_document_notification.py | 1 - tests/fixtures/mal/change_end_of_file.mal | 10 + .../change_middle_of_file_multiple_lines.mal | 8 + .../mal/change_middle_of_file_single_line.mal | 8 + .../mal/change_middle_of_file_twice.mal | 8 + tests/fixtures/mal/change_whole_file.mal | 1 + ...t_did_change_text_document_notification.py | 181 ++++-------------- tests/unit/conftest.py | 10 - 11 files changed, 220 insertions(+), 166 deletions(-) create mode 100644 tests/fixtures/lsp/did_change.py create mode 100644 tests/fixtures/mal/change_end_of_file.mal create mode 100644 tests/fixtures/mal/change_middle_of_file_multiple_lines.mal create mode 100644 tests/fixtures/mal/change_middle_of_file_single_line.mal create mode 100644 tests/fixtures/mal/change_middle_of_file_twice.mal create mode 100644 tests/fixtures/mal/change_whole_file.mal diff --git a/src/malls/lsp/models.py b/src/malls/lsp/models.py index 1ae03bd..45db16c 100644 --- a/src/malls/lsp/models.py +++ b/src/malls/lsp/models.py @@ -2625,19 +2625,20 @@ class WholeFileChange(BaseModel): text: str -class TextDocumentContentChangeEvent(BaseModel): - """ - https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentContentChangeEvent - """ - - range: Range | None = None +class RangeFileChange(BaseModel): + range: Range - range_length: int | None = None + range_length: UInteger | None = None - text: str | WholeFileChange + text: str model_config = base_config +type TextDocumentContentChangeEvent = WholeFileChange | RangeFileChange +""" +https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentContentChangeEvent +""" + class DidChangeTextDocumentParams(BaseModel): """ diff --git a/tests/conftest.py b/tests/conftest.py index 895a8e4..a7b5252 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,8 @@ from pathlib import Path import pytest +import tree_sitter_mal as ts_mal +from tree_sitter import Language, Parser logging.getLogger().setLevel(logging.DEBUG) @@ -81,3 +83,11 @@ def mal_root(tests_root: Path) -> Path: @pytest.fixture def mal_root_str(mal_root: Path) -> str: return str(mal_root) + +@pytest.fixture +def mal_language() -> Language: + return Language(ts_mal.language()) + +@pytest.fixture +def utf8_mal_parser(mal_language: Language) -> Parser: + return Parser(mal_language) diff --git a/tests/fixtures/lsp/did_change.py b/tests/fixtures/lsp/did_change.py new file mode 100644 index 0000000..fcd2d3f --- /dev/null +++ b/tests/fixtures/lsp/did_change.py @@ -0,0 +1,132 @@ +import typing + +import pytest + + +pytest_plugins = ["tests.fixtures.lsp.conftest", + "tests.fixtures.lsp.did_open_text_document_notification"] + +@pytest.fixture +def did_change_base_open_notification( + client_notifications: list[dict], + mal_base_open_uri: str, + did_change_notification): + client_notifications[-1]["params"] = { + "textDocument": { + "uri": mal_base_open_uri, + "version": 1, + }, + } + +@pytest.fixture +def did_change_middle_of_base_open_single_line_notification( + client_notifications: list[dict], + did_change_base_open_notification): + client_notifications[-1]["params"]["contentChanges"] = [ + { + "range": { + "start": {"line": 5, "character": 10}, + "end": {"line": 6, "character": 0}, + }, + "text": "FooFoo extends Foo {}\n", + } + ] + +@pytest.fixture +def change_middle_of_file_single_line_client_messages( + client_initalize_procedures, + did_open_base_open_notification, + did_change_middle_of_base_open_single_line_notification, + client_rpc_messages) -> typing.BinaryIO: + return client_rpc_messages + +@pytest.fixture +def did_change_middle_of_base_open_multiple_lines_notification( + client_notifications: list[dict], + did_change_base_open_notification): + client_notifications[-1]["params"]["contentChanges"] = [ + { + "range": { + "start": {"line": 4, "character": 19}, + "end": {"line": 5, "character": 28}, + }, + "text": "Bar {}\n asset Foo extends Bar {}", + } + ] + +@pytest.fixture +def change_middle_of_file_multiple_lines_client_messages( + client_initalize_procedures, + did_open_base_open_notification, + did_change_middle_of_base_open_multiple_lines_notification, + client_rpc_messages) -> typing.BinaryIO: + return client_rpc_messages + +@pytest.fixture +def did_change_end_of_base_open_notification( + client_notifications: list[dict], + did_change_base_open_notification): + client_notifications[-1]["params"]["contentChanges"] = [ + { + "range": { + "start": {"line": 7, "character": 0}, + "end": {"line": 9, "character": 0}, + }, + "text": "\nassociations {\n}\n", + } + ] + +@pytest.fixture +def change_end_of_base_open_notification_client_messages( + client_initalize_procedures, + did_open_base_open_notification, + did_change_end_of_base_open_notification, + client_rpc_messages) -> typing.BinaryIO: + return client_rpc_messages + +@pytest.fixture +def did_change_middle_of_base_open_twice_notification( + client_notifications: list[dict], + did_change_base_open_notification): + client_notifications[-1]["params"]["contentChanges"] = [ + { + "range": { + "start": {"line": 4, "character": 19}, + "end": {"line": 5, "character": 28}, + }, + "text": "Bar {}\n asset Foo extends Bar {}", + }, + { + "range": { + "start": {"line": 5, "character": 10}, + "end": {"line": 5, "character": 13}, + }, + "text": "Qux", + }, + ] + +@pytest.fixture +def change_middle_of_base_open_twice_client_messages( + client_initalize_procedures, + did_open_base_open_notification, + did_change_middle_of_base_open_twice_notification, + client_rpc_messages) -> typing.BinaryIO: + return client_rpc_messages + +@pytest.fixture +def did_change_whole_base_open_notification( + client_notifications: list[dict], + did_change_base_open_notification): + client_notifications[-1]["params"]["contentChanges"] = [ + { + "text": '#id: "a.b.c"\n', + } + ] + +@pytest.fixture +def change_whole_base_open_client_messages( + client_initalize_procedures, + did_open_base_open_notification, + did_change_whole_base_open_notification, + client_rpc_messages) -> typing.BinaryIO: + return client_rpc_messages diff --git a/tests/fixtures/lsp/did_open_text_document_notification.py b/tests/fixtures/lsp/did_open_text_document_notification.py index 23e681f..1a97955 100644 --- a/tests/fixtures/lsp/did_open_text_document_notification.py +++ b/tests/fixtures/lsp/did_open_text_document_notification.py @@ -2,7 +2,6 @@ import pytest - # So that importers are aware of the conftest fixtures pytest_plugins = ["tests.fixtures.lsp.conftest"] diff --git a/tests/fixtures/mal/change_end_of_file.mal b/tests/fixtures/mal/change_end_of_file.mal new file mode 100644 index 0000000..edf743b --- /dev/null +++ b/tests/fixtures/mal/change_end_of_file.mal @@ -0,0 +1,10 @@ +#id: "org.mal-lang.testAnalyzer" +#version:"0.0.0" + +category System { + abstract asset Foo {} + asset Bar extends Foo {} +} + +associations { +} diff --git a/tests/fixtures/mal/change_middle_of_file_multiple_lines.mal b/tests/fixtures/mal/change_middle_of_file_multiple_lines.mal new file mode 100644 index 0000000..e5deb20 --- /dev/null +++ b/tests/fixtures/mal/change_middle_of_file_multiple_lines.mal @@ -0,0 +1,8 @@ +#id: "org.mal-lang.testAnalyzer" +#version:"0.0.0" + +category System { + abstract asset Bar {} + asset Foo extends Bar {} +} + diff --git a/tests/fixtures/mal/change_middle_of_file_single_line.mal b/tests/fixtures/mal/change_middle_of_file_single_line.mal new file mode 100644 index 0000000..8f2b37b --- /dev/null +++ b/tests/fixtures/mal/change_middle_of_file_single_line.mal @@ -0,0 +1,8 @@ +#id: "org.mal-lang.testAnalyzer" +#version:"0.0.0" + +category System { + abstract asset Foo {} + asset FooFoo extends Foo {} +} + diff --git a/tests/fixtures/mal/change_middle_of_file_twice.mal b/tests/fixtures/mal/change_middle_of_file_twice.mal new file mode 100644 index 0000000..76375e4 --- /dev/null +++ b/tests/fixtures/mal/change_middle_of_file_twice.mal @@ -0,0 +1,8 @@ +#id: "org.mal-lang.testAnalyzer" +#version:"0.0.0" + +category System { + abstract asset Bar {} + asset Qux extends Bar {} +} + diff --git a/tests/fixtures/mal/change_whole_file.mal b/tests/fixtures/mal/change_whole_file.mal new file mode 100644 index 0000000..9bf5aac --- /dev/null +++ b/tests/fixtures/mal/change_whole_file.mal @@ -0,0 +1 @@ +#id: "a.b.c" diff --git a/tests/integration/test_did_change_text_document_notification.py b/tests/integration/test_did_change_text_document_notification.py index 798ae0e..ff2f7fd 100644 --- a/tests/integration/test_did_change_text_document_notification.py +++ b/tests/integration/test_did_change_text_document_notification.py @@ -1,159 +1,46 @@ -import json import typing -from pathlib import Path -import tree_sitter_mal as ts_mal -from tree_sitter import Language, Parser +import pytest +from tree_sitter import Parser from ..util import server_output -MAL_LANGUAGE = Language(ts_mal.language()) -PARSER = Parser(MAL_LANGUAGE) - -# calculate file path of mal files -FILE_PATH = str(Path(__file__).parent.parent.resolve()) + "/fixtures/mal/" -simplified_file_path = FILE_PATH + "main.mal" - - -def filepath_to_uri(filepath: str) -> str: - """ - Converts a native filesystem path to a file:// URI. - """ - path_obj = Path(filepath) - - absolute_path_obj = path_obj.resolve() - - return absolute_path_obj.as_uri() - - -def write_payload(payload, file): - # get the length of the payload (+1 for the newline) - json_string = json.dumps(payload, separators=(",", ":")) # Remove extra spaces - json_payload = json_string.encode("utf-8") - payload_size = str(len(json_payload)) - - # write payload and size to file - file.write(b"Content-Length: " + payload_size.encode()) - file.write(b"\n\n" + json_string.encode()) - - -def test_change_middle_of_file_single_line(change_middle_of_file_single_line: typing.BinaryIO): - new_text = b"""#id: "org.mal-lang.testAnalyzer" -#version:"0.0.0" - -category System { -abstract asset Foo {} -asset FooFoo extends Foo {} -} - -""" - # Start: 5 line | 13-7 = 6 character - # End: 6 line | 0 character - - # send to server - output, ls, *_ = server_output(change_middle_of_file_single_line) - - # Ensure LSP stored everything correctly - assert ls.files[simplified_file_path].text == new_text - # we have to parse the file to check if the end result is the same - tree = PARSER.parse(new_text) - assert str(ls.files[simplified_file_path].tree.root_node) == str(tree.root_node) - - output.close() - - -def test_change_middle_of_file_multiple_line( - change_middle_of_file_multiple_lines: typing.BinaryIO, -): - new_text = b"""#id: "org.mal-lang.testAnalyzer" -#version:"0.0.0" - -category System { -abstract asset Bar {} -asset Foo extends Bar {} -} - -""" - # Start: 5 line | 13-7 = 6 character - # End: 6 line | 0 character - - # send to server - output, ls, *_ = server_output(change_middle_of_file_multiple_lines) - - # Ensure LSP stored everything correctly - assert ls.files[simplified_file_path].text == new_text - # we have to parse the file to check if the end result is the same - tree = PARSER.parse(new_text) - assert str(ls.files[simplified_file_path].tree.root_node) == str(tree.root_node) - - output.close() - - -def test_change_end_of_file(change_end_of_file: typing.BinaryIO): - new_text = b"""#id: "org.mal-lang.testAnalyzer" -#version:"0.0.0" - -category System { -abstract asset Foo {} -asset Bar extends Foo {} -} - -associations { -} -""" - # Start: 5 line | 13-7 = 6 character - # End: 6 line | 0 character - - # send to server - output, ls, *_ = server_output(change_end_of_file) - - # Ensure LSP stored everything correctly - assert ls.files[simplified_file_path].text == new_text - # we have to parse the file to check if the end result is the same - tree = PARSER.parse(new_text) - assert str(ls.files[simplified_file_path].tree.root_node) == str(tree.root_node) - - output.close() - - -def test_change_middle_of_file_twice(change_middle_of_file_twice: typing.BinaryIO): - # construct change notificatoin payload with correct uri - new_text = b"""#id: "org.mal-lang.testAnalyzer" -#version:"0.0.0" - -category System { -abstract asset Bar {} -asset Qux extends Bar {} -} - -""" - # Start: 5 line | 13-7 = 6 character - # End: 6 line | 0 character - - # send to server - output, ls, *_ = server_output(change_middle_of_file_twice) - - # Ensure LSP stored everything correctly - assert ls.files[simplified_file_path].text == new_text - # we have to parse the file to check if the end result is the same - tree = PARSER.parse(new_text) - assert str(ls.files[simplified_file_path].tree.root_node) == str(tree.root_node) - - output.close() - - -def test_change_whole_file(change_whole_file: typing.BinaryIO): - new_text = b"""#id: "a.b.c"\n""" - # Start: 5 line | 13-7 = 6 character - # End: 6 line | 0 character +pytest_plugins = ["tests.fixtures.lsp.did_change"] + +parameters = [("change_middle_of_file_single_line_client_messages", + "mal_change_middle_of_file_single_line"), + ("change_middle_of_file_multiple_lines_client_messages", + "mal_change_middle_of_file_multiple_lines"), + ("change_end_of_base_open_notification_client_messages", + "mal_change_end_of_file"), + ("change_middle_of_base_open_twice_client_messages", + "mal_change_middle_of_file_twice"), + ("change_whole_base_open_client_messages", + "mal_change_whole_file")] +parameter_ids = (args[0] for args in parameters) + +@pytest.mark.parametrize( + "client_messages_fixture_name,expected_file_fixture_name", + parameters, + ids=parameter_ids) +def test_changes(request: pytest.FixtureRequest, + utf8_mal_parser: Parser, + mal_base_open_uri: str, + client_messages_fixture_name: str, + expected_file_fixture_name: str): + client_messages: typing.BinaryIO = request.getfixturevalue(client_messages_fixture_name) + expected_file: typing.BinaryIO = request.getfixturevalue(expected_file_fixture_name) + + new_text = expected_file.read() + uri = mal_base_open_uri[len("file://"):] # send to server - output, ls, *_ = server_output(change_whole_file) + output, ls, *_ = server_output(client_messages) # Ensure LSP stored everything correctly - assert ls.files[simplified_file_path].text == new_text + assert ls.files[uri].text == new_text # we have to parse the file to check if the end result is the same - tree = PARSER.parse(new_text) - assert str(ls.files[simplified_file_path].tree.root_node) == str(tree.root_node) + tree = utf8_mal_parser.parse(new_text) + assert str(ls.files[uri].tree.root_node) == str(tree.root_node) output.close() diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 34d8f94..f8fbbb5 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,8 +1,6 @@ from io import BytesIO import pytest -import tree_sitter_mal as ts_mal -from tree_sitter import Language, Parser from malls.lsp.fsm import LifecycleState @@ -15,11 +13,3 @@ def mute_ls() -> FakeLanguageServer: yield ls if ls.state.current_state != LifecycleState.EXIT: ls.m_exit() - -@pytest.fixture -def mal_language() -> Language: - return Language(ts_mal.language()) - -@pytest.fixture -def utf8_mal_parser(mal_language: Language) -> Parser: - return Parser(mal_language) From 83d517a233599f98806c7e3d4b1340ec7c829ce4 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 22:52:51 +0200 Subject: [PATCH 37/42] chore: ruff format --- src/malls/lsp/models.py | 1 + tests/conftest.py | 6 + tests/fixtures/lsp/conftest.py | 54 +++-- tests/fixtures/lsp/did_change.py | 182 +++++++++------- .../did_open_text_document_notification.py | 61 +++--- .../fixtures/lsp/encoding_capability_check.py | 1 + tests/fixtures/lsp/publish_diagnostics.py | 84 +++++--- tests/integration/test_completion.py | 58 ++--- .../integration/test_diagnostics_are_saved.py | 42 ++-- ...t_did_change_text_document_notification.py | 40 ++-- ...est_did_open_text_document_notification.py | 7 +- tests/integration/test_goto_definition.py | 203 ++++++++++-------- tests/integration/test_publish_diagnostics.py | 16 +- tests/unit/test_find_current_scope.py | 64 +++--- tests/unit/test_find_meta_comment_function.py | 22 +- .../test_find_symbol_definition_function.py | 150 ++++++------- .../test_find_symbols_in_context_hierarchy.py | 183 ++++++++-------- tests/unit/test_find_symbols_in_scope.py | 89 ++++---- tests/unit/test_position_conversion.py | 47 ++-- tests/unit/test_visit_expr.py | 69 +++--- tests/util.py | 45 ++-- 21 files changed, 763 insertions(+), 661 deletions(-) diff --git a/src/malls/lsp/models.py b/src/malls/lsp/models.py index 45db16c..5f49606 100644 --- a/src/malls/lsp/models.py +++ b/src/malls/lsp/models.py @@ -2634,6 +2634,7 @@ class RangeFileChange(BaseModel): model_config = base_config + type TextDocumentContentChangeEvent = WholeFileChange | RangeFileChange """ https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentContentChangeEvent diff --git a/tests/conftest.py b/tests/conftest.py index a7b5252..76aa800 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -55,6 +55,7 @@ def fixture_uri(file: str): path = Path(file) file_path = path.resolve() uri = str(file_path.as_uri()) + def template() -> str: return uri @@ -72,22 +73,27 @@ def template() -> str: TESTS_ROOT = Path(__file__).parent + @pytest.fixture def tests_root() -> Path: return TESTS_ROOT + @pytest.fixture def mal_root(tests_root: Path) -> Path: return tests_root.joinpath("fixtures", "mal") + @pytest.fixture def mal_root_str(mal_root: Path) -> str: return str(mal_root) + @pytest.fixture def mal_language() -> Language: return Language(ts_mal.language()) + @pytest.fixture def utf8_mal_parser(mal_language: Language) -> Parser: return Parser(mal_language) diff --git a/tests/fixtures/lsp/conftest.py b/tests/fixtures/lsp/conftest.py index 99357a0..3547ec9 100644 --- a/tests/fixtures/lsp/conftest.py +++ b/tests/fixtures/lsp/conftest.py @@ -97,18 +97,14 @@ def initalize_request(client_requests: list[dict], client_messages: list[dict]) "id": len(client_requests), "method": "initialize", "params": { - "capabilities": { - "textDocument": { - "definition": { - "dynamicRegistration": False - }, - "synchronization": { - "dynamicRegistration": False - } - } - }, - "trace": "off" - }, + "capabilities": { + "textDocument": { + "definition": {"dynamicRegistration": False}, + "synchronization": {"dynamicRegistration": False}, + } + }, + "trace": "off", + }, } client_requests.append(message) client_messages.append(message) @@ -219,32 +215,30 @@ def server_rpc_messages(server_messages: list[dict]) -> typing.BinaryIO: """ return build_rpc_message_stream(server_messages, insert_header=CONTENT_TYPE_HEADER) + @pytest.fixture -def did_open_notification( - client_notifications: list[dict], - client_messages: list[dict]) -> dict: +def did_open_notification(client_notifications: list[dict], client_messages: list[dict]) -> dict: """ Defines an `textDocument/didOpen` LSP notification from client to server. Fields `uri` and `text` in `params.textDocument` must be edited. """ message = { - "jsonrpc": "2.0", - "method": "textDocument/didOpen", - "params": { - "textDocument": { - "languageId": "mal", - "version": 0, - } - }, + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "languageId": "mal", + "version": 0, } + }, + } client_notifications.append(message) client_messages.append(message) return message + @pytest.fixture -def did_change_notification( - client_notifications: list[dict], - client_messages: list[dict]) -> dict: +def did_change_notification(client_notifications: list[dict], client_messages: list[dict]) -> dict: """ Defines an `textDocument/didChange` LSP notification from client to server. Fields `uri`, `version` in `params.textDocument` and `range`, `text` in `params.contentChanges` @@ -269,17 +263,17 @@ def did_change_notification( client_messages.append(message) return message + @pytest.fixture -def client_initalize_procedures(initalize_request, - initalized_notification): +def client_initalize_procedures(initalize_request, initalized_notification): """ Adds the relevant messages from client to server so that both client and server are initalized. """ pass + @pytest.fixture -def client_shutdown_procedures(shutdown_request, - exit_notification): +def client_shutdown_procedures(shutdown_request, exit_notification): """ Adds the relevant messages from client to server to shut down the server. Assumes at non-erreneous and post-initalized server state. diff --git a/tests/fixtures/lsp/did_change.py b/tests/fixtures/lsp/did_change.py index fcd2d3f..d5064de 100644 --- a/tests/fixtures/lsp/did_change.py +++ b/tests/fixtures/lsp/did_change.py @@ -2,131 +2,147 @@ import pytest +pytest_plugins = [ + "tests.fixtures.lsp.conftest", + "tests.fixtures.lsp.did_open_text_document_notification", +] -pytest_plugins = ["tests.fixtures.lsp.conftest", - "tests.fixtures.lsp.did_open_text_document_notification"] @pytest.fixture def did_change_base_open_notification( - client_notifications: list[dict], - mal_base_open_uri: str, - did_change_notification): + client_notifications: list[dict], mal_base_open_uri: str, did_change_notification +): client_notifications[-1]["params"] = { - "textDocument": { - "uri": mal_base_open_uri, - "version": 1, - }, - } + "textDocument": { + "uri": mal_base_open_uri, + "version": 1, + }, + } + @pytest.fixture def did_change_middle_of_base_open_single_line_notification( - client_notifications: list[dict], - did_change_base_open_notification): + client_notifications: list[dict], did_change_base_open_notification +): client_notifications[-1]["params"]["contentChanges"] = [ - { - "range": { - "start": {"line": 5, "character": 10}, - "end": {"line": 6, "character": 0}, - }, - "text": "FooFoo extends Foo {}\n", - } - ] + { + "range": { + "start": {"line": 5, "character": 10}, + "end": {"line": 6, "character": 0}, + }, + "text": "FooFoo extends Foo {}\n", + } + ] + @pytest.fixture def change_middle_of_file_single_line_client_messages( - client_initalize_procedures, - did_open_base_open_notification, - did_change_middle_of_base_open_single_line_notification, - client_rpc_messages) -> typing.BinaryIO: + client_initalize_procedures, + did_open_base_open_notification, + did_change_middle_of_base_open_single_line_notification, + client_rpc_messages, +) -> typing.BinaryIO: return client_rpc_messages + @pytest.fixture def did_change_middle_of_base_open_multiple_lines_notification( - client_notifications: list[dict], - did_change_base_open_notification): + client_notifications: list[dict], did_change_base_open_notification +): client_notifications[-1]["params"]["contentChanges"] = [ - { - "range": { - "start": {"line": 4, "character": 19}, - "end": {"line": 5, "character": 28}, - }, - "text": "Bar {}\n asset Foo extends Bar {}", - } - ] + { + "range": { + "start": {"line": 4, "character": 19}, + "end": {"line": 5, "character": 28}, + }, + "text": "Bar {}\n asset Foo extends Bar {}", + } + ] + @pytest.fixture def change_middle_of_file_multiple_lines_client_messages( - client_initalize_procedures, - did_open_base_open_notification, - did_change_middle_of_base_open_multiple_lines_notification, - client_rpc_messages) -> typing.BinaryIO: + client_initalize_procedures, + did_open_base_open_notification, + did_change_middle_of_base_open_multiple_lines_notification, + client_rpc_messages, +) -> typing.BinaryIO: return client_rpc_messages + @pytest.fixture def did_change_end_of_base_open_notification( - client_notifications: list[dict], - did_change_base_open_notification): + client_notifications: list[dict], did_change_base_open_notification +): client_notifications[-1]["params"]["contentChanges"] = [ - { - "range": { - "start": {"line": 7, "character": 0}, - "end": {"line": 9, "character": 0}, - }, - "text": "\nassociations {\n}\n", - } - ] + { + "range": { + "start": {"line": 7, "character": 0}, + "end": {"line": 9, "character": 0}, + }, + "text": "\nassociations {\n}\n", + } + ] + @pytest.fixture def change_end_of_base_open_notification_client_messages( - client_initalize_procedures, - did_open_base_open_notification, - did_change_end_of_base_open_notification, - client_rpc_messages) -> typing.BinaryIO: + client_initalize_procedures, + did_open_base_open_notification, + did_change_end_of_base_open_notification, + client_rpc_messages, +) -> typing.BinaryIO: return client_rpc_messages + @pytest.fixture def did_change_middle_of_base_open_twice_notification( - client_notifications: list[dict], - did_change_base_open_notification): + client_notifications: list[dict], did_change_base_open_notification +): client_notifications[-1]["params"]["contentChanges"] = [ - { - "range": { - "start": {"line": 4, "character": 19}, - "end": {"line": 5, "character": 28}, - }, - "text": "Bar {}\n asset Foo extends Bar {}", - }, - { - "range": { - "start": {"line": 5, "character": 10}, - "end": {"line": 5, "character": 13}, - }, - "text": "Qux", - }, - ] + { + "range": { + "start": {"line": 4, "character": 19}, + "end": {"line": 5, "character": 28}, + }, + "text": "Bar {}\n asset Foo extends Bar {}", + }, + { + "range": { + "start": {"line": 5, "character": 10}, + "end": {"line": 5, "character": 13}, + }, + "text": "Qux", + }, + ] + @pytest.fixture def change_middle_of_base_open_twice_client_messages( - client_initalize_procedures, - did_open_base_open_notification, - did_change_middle_of_base_open_twice_notification, - client_rpc_messages) -> typing.BinaryIO: + client_initalize_procedures, + did_open_base_open_notification, + did_change_middle_of_base_open_twice_notification, + client_rpc_messages, +) -> typing.BinaryIO: return client_rpc_messages + @pytest.fixture def did_change_whole_base_open_notification( - client_notifications: list[dict], - did_change_base_open_notification): + client_notifications: list[dict], did_change_base_open_notification +): client_notifications[-1]["params"]["contentChanges"] = [ - { - "text": '#id: "a.b.c"\n', - } - ] + { + "text": '#id: "a.b.c"\n', + } + ] + @pytest.fixture def change_whole_base_open_client_messages( - client_initalize_procedures, - did_open_base_open_notification, - did_change_whole_base_open_notification, - client_rpc_messages) -> typing.BinaryIO: + client_initalize_procedures, + did_open_base_open_notification, + did_change_whole_base_open_notification, + client_rpc_messages, +) -> typing.BinaryIO: return client_rpc_messages diff --git a/tests/fixtures/lsp/did_open_text_document_notification.py b/tests/fixtures/lsp/did_open_text_document_notification.py index 1a97955..04f4aa8 100644 --- a/tests/fixtures/lsp/did_open_text_document_notification.py +++ b/tests/fixtures/lsp/did_open_text_document_notification.py @@ -5,13 +5,15 @@ # So that importers are aware of the conftest fixtures pytest_plugins = ["tests.fixtures.lsp.conftest"] + @pytest.fixture def did_open_base_open_notification( - client_notifications: list[dict], - client_messages: list[dict], - did_open_notification, - mal_base_open: typing.BinaryIO, - mal_base_open_uri: str): + client_notifications: list[dict], + client_messages: list[dict], + did_open_notification, + mal_base_open: typing.BinaryIO, + mal_base_open_uri: str, +): # since did_open_notification is a dependency here # we know the notification is the latest one open_notification = client_notifications[-1] @@ -19,19 +21,24 @@ def did_open_base_open_notification( text_doc_params["uri"] = mal_base_open_uri text_doc_params["text"] = mal_base_open.read().decode("utf8") + @pytest.fixture -def base_open_client_messages(client_initalize_procedures, - did_open_base_open_notification, - client_rpc_messages: typing.BinaryIO) -> typing.BinaryIO: +def base_open_client_messages( + client_initalize_procedures, + did_open_base_open_notification, + client_rpc_messages: typing.BinaryIO, +) -> typing.BinaryIO: return client_rpc_messages + @pytest.fixture def did_open_base_open_file_with_fake_include_notification( - client_notifications: list[dict], - client_messages: list[dict], - did_open_notification, - mal_base_open_file_with_fake_include: typing.BinaryIO, - mal_base_open_file_with_fake_include_uri: str): + client_notifications: list[dict], + client_messages: list[dict], + did_open_notification, + mal_base_open_file_with_fake_include: typing.BinaryIO, + mal_base_open_file_with_fake_include_uri: str, +): # since did_open_notification is a dependency here # we know the notification is the latest one open_notification = client_notifications[-1] @@ -39,20 +46,24 @@ def did_open_base_open_file_with_fake_include_notification( text_doc_params["uri"] = mal_base_open_file_with_fake_include_uri text_doc_params["text"] = mal_base_open_file_with_fake_include.read().decode("utf8") + @pytest.fixture def base_open_file_with_fake_include_client_messages( - client_initalize_procedures, - did_open_base_open_file_with_fake_include_notification, - client_rpc_messages: typing.BinaryIO) -> typing.BinaryIO: + client_initalize_procedures, + did_open_base_open_file_with_fake_include_notification, + client_rpc_messages: typing.BinaryIO, +) -> typing.BinaryIO: return client_rpc_messages + @pytest.fixture def did_open_base_open_with_included_file_notification( - client_notifications: list[dict], - client_messages: list[dict], - did_open_notification, - mal_base_open_with_included_file: typing.BinaryIO, - mal_base_open_with_included_file_uri: str): + client_notifications: list[dict], + client_messages: list[dict], + did_open_notification, + mal_base_open_with_included_file: typing.BinaryIO, + mal_base_open_with_included_file_uri: str, +): # since did_open_notification is a dependency here # we know the notification is the latest one open_notification = client_notifications[-1] @@ -60,9 +71,11 @@ def did_open_base_open_with_included_file_notification( text_doc_params["uri"] = mal_base_open_with_included_file_uri text_doc_params["text"] = mal_base_open_with_included_file.read().decode("utf8") + @pytest.fixture def base_open_with_included_file_client_messages( - client_initalize_procedures, - did_open_base_open_with_included_file_notification, - client_rpc_messages: typing.BinaryIO) -> typing.BinaryIO: + client_initalize_procedures, + did_open_base_open_with_included_file_notification, + client_rpc_messages: typing.BinaryIO, +) -> typing.BinaryIO: return client_rpc_messages diff --git a/tests/fixtures/lsp/encoding_capability_check.py b/tests/fixtures/lsp/encoding_capability_check.py index d6f0960..bac777b 100644 --- a/tests/fixtures/lsp/encoding_capability_check.py +++ b/tests/fixtures/lsp/encoding_capability_check.py @@ -5,6 +5,7 @@ # So that importers are aware of the conftest fixtures pytest_plugins = ["tests.fixtures.lsp.conftest"] + @pytest.fixture def set_trace_notification(client_notifications: list[dict], client_messages: list[dict]) -> dict: message = {"jsonrpc": "2.0", "method": "$/setTrace", "params": {"value": "messages"}} diff --git a/tests/fixtures/lsp/publish_diagnostics.py b/tests/fixtures/lsp/publish_diagnostics.py index 409d843..ce8bb3b 100644 --- a/tests/fixtures/lsp/publish_diagnostics.py +++ b/tests/fixtures/lsp/publish_diagnostics.py @@ -3,16 +3,19 @@ import pytest # So that importers are aware of the conftest fixtures -pytest_plugins = ["tests.fixtures.lsp.conftest", - "tests.fixtures.lsp.did_open_text_document_notification"] +pytest_plugins = [ + "tests.fixtures.lsp.conftest", + "tests.fixtures.lsp.did_open_text_document_notification", +] @pytest.fixture def did_open_erroneous_notification( - client_notifications: list[dict], - did_open_notification, - mal_erroneous: typing.BinaryIO, - mal_erroneous_uri: str): + client_notifications: list[dict], + did_open_notification, + mal_erroneous: typing.BinaryIO, + mal_erroneous_uri: str, +): # since did_open_notification is a dependency here # we know the notification is the latest one open_notification = client_notifications[-1] @@ -20,20 +23,24 @@ def did_open_erroneous_notification( text_doc_params["uri"] = mal_erroneous_uri text_doc_params["text"] = mal_erroneous.read().decode("utf8") + @pytest.fixture def erroneous_file_client_messages( - initalize_request, - initalized_notification, - did_open_erroneous_notification, - client_rpc_messages: typing.BinaryIO) -> typing.BinaryIO: + initalize_request, + initalized_notification, + did_open_erroneous_notification, + client_rpc_messages: typing.BinaryIO, +) -> typing.BinaryIO: return client_rpc_messages + @pytest.fixture def did_open_erroneous_include_notification( - client_notifications: list[dict], - did_open_notification, - mal_erroneous_include: typing.BinaryIO, - mal_erroneous_include_uri: str): + client_notifications: list[dict], + did_open_notification, + mal_erroneous_include: typing.BinaryIO, + mal_erroneous_include_uri: str, +): # since did_open_notification is a dependency here # we know the notification is the latest one open_notification = client_notifications[-1] @@ -44,18 +51,21 @@ def did_open_erroneous_include_notification( @pytest.fixture def erroneous_include_file_client_messages( - initalize_request, - initalized_notification, - did_open_erroneous_include_notification, - client_rpc_messages: typing.BinaryIO) -> typing.BinaryIO: + initalize_request, + initalized_notification, + did_open_erroneous_include_notification, + client_rpc_messages: typing.BinaryIO, +) -> typing.BinaryIO: return client_rpc_messages + @pytest.fixture def did_open_file_with_error_notification( - client_notifications: list[dict], - did_open_notification, - mal_file_with_error: typing.BinaryIO, - mal_file_with_error_uri: str): + client_notifications: list[dict], + did_open_notification, + mal_file_with_error: typing.BinaryIO, + mal_file_with_error_uri: str, +): # since did_open_notification is a dependency here # we know the notification is the latest one open_notification = client_notifications[-1] @@ -63,20 +73,22 @@ def did_open_file_with_error_notification( text_doc_params["uri"] = mal_file_with_error_uri text_doc_params["text"] = mal_file_with_error.read().decode("utf8") + @pytest.fixture def erroenous_include_and_file_with_error_client_messages( - initalize_request, - initalized_notification, - did_open_erroneous_notification, - did_open_file_with_error_notification, - client_rpc_messages: typing.BinaryIO) -> typing.BinaryIO: + initalize_request, + initalized_notification, + did_open_erroneous_notification, + did_open_file_with_error_notification, + client_rpc_messages: typing.BinaryIO, +) -> typing.BinaryIO: return client_rpc_messages + @pytest.fixture def did_change_with_error_notification( - client_notifications: list[dict], - did_change_notification, - mal_base_open_uri: str): + client_notifications: list[dict], did_change_notification, mal_base_open_uri: str +): client_notifications[-1]["params"] = { "textDocument": { "uri": mal_base_open_uri, @@ -93,11 +105,13 @@ def did_change_with_error_notification( ], } + @pytest.fixture def change_file_with_error_client_messages( - initalize_request, - initalized_notification, - did_open_base_open_notification, - did_change_with_error_notification, - client_rpc_messages: typing.BinaryIO) -> typing.BinaryIO: + initalize_request, + initalized_notification, + did_open_base_open_notification, + did_change_with_error_notification, + client_rpc_messages: typing.BinaryIO, +) -> typing.BinaryIO: return client_rpc_messages diff --git a/tests/integration/test_completion.py b/tests/integration/test_completion.py index 823c609..65d7297 100644 --- a/tests/integration/test_completion.py +++ b/tests/integration/test_completion.py @@ -62,21 +62,23 @@ ((0, 0), symbols_in_root_node_hierarchy), ] parameter_names = [ - "symbols_in_category_hierarchy", - "symbols_in_associations_hierarchy", - "symbols_in_asset1_hierarchy", - "symbols_in_asset2_hierarchy", - "symbols_in_root_node_hierarchy", - ] + "symbols_in_category_hierarchy", + "symbols_in_associations_hierarchy", + "symbols_in_asset1_hierarchy", + "symbols_in_asset2_hierarchy", + "symbols_in_root_node_hierarchy", +] pytest_plugins = ["tests.fixtures.mal"] + @pytest.fixture def open_completion_document_notification( - client_notifications: list[dict], - client_messages: list[dict], - mal_completion_document: io.BytesIO, - mal_completion_document_uri: str) -> dict: + client_notifications: list[dict], + client_messages: list[dict], + mal_completion_document: io.BytesIO, + mal_completion_document_uri: str, +) -> dict: """ Sends a didOpen notification bound to the MAL fixture file completion_document. """ @@ -96,11 +98,11 @@ def open_completion_document_notification( client_messages.append(message) return message + @pytest.fixture def completion_request( - client_requests: list[dict], - client_messages: list[dict], - mal_completion_document_uri: str) -> typing.Callable[[(int, int)], dict]: + client_requests: list[dict], client_messages: list[dict], mal_completion_document_uri: str +) -> typing.Callable[[(int, int)], dict]: def make(position: (int, int)): character, line = position message = { @@ -109,7 +111,7 @@ def make(position: (int, int)): "method": "textDocument/completion", "params": { "textDocument": { - "uri": mal_completion_document_uri, # find_symbols_in_scope_path + "uri": mal_completion_document_uri, # find_symbols_in_scope_path }, "position": { "line": line, @@ -120,29 +122,31 @@ def make(position: (int, int)): client_requests.append(message) client_messages.append(message) return message + return make + @pytest.fixture def completion_client_messages( - client_messages: list[dict], - initalize_request, - initalized_notification, - open_completion_document_notification, - completion_request: typing.Callable[[(int, int)], dict]) -> typing.Callable[[(int, int)], io.BytesIO]: # noqa: E501 + client_messages: list[dict], + initalize_request, + initalized_notification, + open_completion_document_notification, + completion_request: typing.Callable[[(int, int)], dict], +) -> typing.Callable[[(int, int)], io.BytesIO]: # noqa: E501 def make(position: (int, int)) -> io.BytesIO: completion_request(position) return build_rpc_message_stream(client_messages) + return make -@pytest.mark.parametrize( - "location,completion_list", - parameters, - ids=parameter_names -) + +@pytest.mark.parametrize("location,completion_list", parameters, ids=parameter_names) def test_completion( - location: (int, int), - completion_list: list[str], - completion_client_messages: typing.Callable[[(int, int)], io.BytesIO]): + location: (int, int), + completion_list: list[str], + completion_client_messages: typing.Callable[[(int, int)], io.BytesIO], +): # send to server fixture = completion_client_messages(location) output, ls, *_ = server_output(fixture) diff --git a/tests/integration/test_diagnostics_are_saved.py b/tests/integration/test_diagnostics_are_saved.py index 5e9cac3..5bfc327 100644 --- a/tests/integration/test_diagnostics_are_saved.py +++ b/tests/integration/test_diagnostics_are_saved.py @@ -6,34 +6,34 @@ pytest_plugins = ["tests.fixtures.lsp.publish_diagnostics"] -parameters = [("erroneous_file_client_messages", - "mal_erroneous_uri", - DiagnosticSeverity.Error), - ("erroenous_include_and_file_with_error_client_messages", - "mal_file_with_error_uri", - DiagnosticSeverity.Error), - ("change_file_with_error_client_messages", - "mal_base_open_uri", - DiagnosticSeverity.Error)] -parameter_ids = ["erroneous_file", - "erroneous_include_file", - "change_file_with_error"] +parameters = [ + ("erroneous_file_client_messages", "mal_erroneous_uri", DiagnosticSeverity.Error), + ( + "erroenous_include_and_file_with_error_client_messages", + "mal_file_with_error_uri", + DiagnosticSeverity.Error, + ), + ("change_file_with_error_client_messages", "mal_base_open_uri", DiagnosticSeverity.Error), +] +parameter_ids = ["erroneous_file", "erroneous_include_file", "change_file_with_error"] + @pytest.mark.parametrize( - "messages_fixture_name,uri_fixture_name,error_level", - parameters, - ids=parameter_ids) -def test_open_file(request: pytest.FixtureRequest, - messages_fixture_name: str, - uri_fixture_name: str, - error_level: DiagnosticSeverity): + "messages_fixture_name,uri_fixture_name,error_level", parameters, ids=parameter_ids +) +def test_open_file( + request: pytest.FixtureRequest, + messages_fixture_name: str, + uri_fixture_name: str, + error_level: DiagnosticSeverity, +): input = request.getfixturevalue(messages_fixture_name) uri = request.getfixturevalue(uri_fixture_name) # since Document acts inconsistent with URIs - uri = uri[len("file://"):] + uri = uri[len("file://") :] - # send to server + # send to server output, ls, *_ = server_output(input) # Ensure LSP stored everything correctly diff --git a/tests/integration/test_did_change_text_document_notification.py b/tests/integration/test_did_change_text_document_notification.py index ff2f7fd..62e2383 100644 --- a/tests/integration/test_did_change_text_document_notification.py +++ b/tests/integration/test_did_change_text_document_notification.py @@ -7,32 +7,34 @@ pytest_plugins = ["tests.fixtures.lsp.did_change"] -parameters = [("change_middle_of_file_single_line_client_messages", - "mal_change_middle_of_file_single_line"), - ("change_middle_of_file_multiple_lines_client_messages", - "mal_change_middle_of_file_multiple_lines"), - ("change_end_of_base_open_notification_client_messages", - "mal_change_end_of_file"), - ("change_middle_of_base_open_twice_client_messages", - "mal_change_middle_of_file_twice"), - ("change_whole_base_open_client_messages", - "mal_change_whole_file")] +parameters = [ + ("change_middle_of_file_single_line_client_messages", "mal_change_middle_of_file_single_line"), + ( + "change_middle_of_file_multiple_lines_client_messages", + "mal_change_middle_of_file_multiple_lines", + ), + ("change_end_of_base_open_notification_client_messages", "mal_change_end_of_file"), + ("change_middle_of_base_open_twice_client_messages", "mal_change_middle_of_file_twice"), + ("change_whole_base_open_client_messages", "mal_change_whole_file"), +] parameter_ids = (args[0] for args in parameters) + @pytest.mark.parametrize( - "client_messages_fixture_name,expected_file_fixture_name", - parameters, - ids=parameter_ids) -def test_changes(request: pytest.FixtureRequest, - utf8_mal_parser: Parser, - mal_base_open_uri: str, - client_messages_fixture_name: str, - expected_file_fixture_name: str): + "client_messages_fixture_name,expected_file_fixture_name", parameters, ids=parameter_ids +) +def test_changes( + request: pytest.FixtureRequest, + utf8_mal_parser: Parser, + mal_base_open_uri: str, + client_messages_fixture_name: str, + expected_file_fixture_name: str, +): client_messages: typing.BinaryIO = request.getfixturevalue(client_messages_fixture_name) expected_file: typing.BinaryIO = request.getfixturevalue(expected_file_fixture_name) new_text = expected_file.read() - uri = mal_base_open_uri[len("file://"):] + uri = mal_base_open_uri[len("file://") :] # send to server output, ls, *_ = server_output(client_messages) diff --git a/tests/integration/test_did_open_text_document_notification.py b/tests/integration/test_did_open_text_document_notification.py index 204ccb2..88f64d9 100644 --- a/tests/integration/test_did_open_text_document_notification.py +++ b/tests/integration/test_did_open_text_document_notification.py @@ -7,9 +7,8 @@ pytest_plugins = ["tests.fixtures.lsp.did_open_text_document_notification"] -parameters = ["base_open", - "base_open_file_with_fake_include", - "base_open_with_included_file"] +parameters = ["base_open", "base_open_file_with_fake_include", "base_open_with_included_file"] + @pytest.mark.parametrize("file", parameters, ids=parameters) def test_open_file(request: pytest.FixtureRequest, file: str): @@ -19,7 +18,7 @@ def test_open_file(request: pytest.FixtureRequest, file: str): # send to server output, ls, *_ = server_output(file_fixture) # since Document acts inconsistent with URIs - uri_fixture = uri_fixture[len("file://"):] + uri_fixture = uri_fixture[len("file://") :] # Ensure LSP stored everything correctly assert uri_fixture in ls.files diff --git a/tests/integration/test_goto_definition.py b/tests/integration/test_goto_definition.py index bbd3c50..b033f13 100644 --- a/tests/integration/test_goto_definition.py +++ b/tests/integration/test_goto_definition.py @@ -6,88 +6,112 @@ from ..util import build_rpc_message_stream, get_lsp_json, server_output # Test parameters -mal_find_symbols_in_scope_points = zip([ - (3, 12), # category_declaration, "goto_def_1" - (11, 10), # asset declaration, asset name, "goto_def_2" - (7, 10), # asset variable, "goto_def_3" - (8, 10), # attack step, "goto_def_4" - ], itertools.repeat("find_symbols_in_scope")) -mal_symbol_def_extended_asset_main_points = zip([ - (6, 25), # asset declaration, extended asset, "goto_def_5" - (9, 11), # variable call, "goto_def_6" - ], itertools.repeat("symbol_def_extended_asset_main")) -mal_symbol_def_variable_call_extend_chain_main_points = zip([ - (9, 11) # variable call, extend chain, "goto_def_7" - ], itertools.repeat("symbol_def_variable_call_extend_chain_main")) -symbol_def_variable_declaration_main_points = zip([ - (10, 20), # variable declaration, "goto_def_8" - (17, 21), # variable declaration, extended asset, "goto_def_9" - (21, 36), # variable declaration complex 1, "goto_def_10" - (22, 36), # variable declaration complex 2, "goto_def_11" - (26, 6), # association asset name 1, "goto_def_12" - (27, 37), # association asset name 2, "goto_def_13" - (28, 12), # association field name 1, "goto_def_14" - (28, 32), # association field name 2, "goto_def_15" - (30, 22), # link name, "goto_def_16" - ], itertools.repeat("symbol_def_variable_declaration_main")) -mal_symbol_def_preconditions_points = zip([ - (11, 15), # preconditions, "goto_def_17" - (19, 13), # preconditions extended asset, "goto_def_18" - (24, 28), # preconditions complex 1, "goto_def_19" - (26, 28), # preconditions complex 1, "goto_def_20" - ], itertools.repeat("symbol_def_preconditions")) +mal_find_symbols_in_scope_points = zip( + [ + (3, 12), # category_declaration, "goto_def_1" + (11, 10), # asset declaration, asset name, "goto_def_2" + (7, 10), # asset variable, "goto_def_3" + (8, 10), # attack step, "goto_def_4" + ], + itertools.repeat("find_symbols_in_scope"), +) +mal_symbol_def_extended_asset_main_points = zip( + [ + (6, 25), # asset declaration, extended asset, "goto_def_5" + (9, 11), # variable call, "goto_def_6" + ], + itertools.repeat("symbol_def_extended_asset_main"), +) +mal_symbol_def_variable_call_extend_chain_main_points = zip( + [ + (9, 11) # variable call, extend chain, "goto_def_7" + ], + itertools.repeat("symbol_def_variable_call_extend_chain_main"), +) +symbol_def_variable_declaration_main_points = zip( + [ + (10, 20), # variable declaration, "goto_def_8" + (17, 21), # variable declaration, extended asset, "goto_def_9" + (21, 36), # variable declaration complex 1, "goto_def_10" + (22, 36), # variable declaration complex 2, "goto_def_11" + (26, 6), # association asset name 1, "goto_def_12" + (27, 37), # association asset name 2, "goto_def_13" + (28, 12), # association field name 1, "goto_def_14" + (28, 32), # association field name 2, "goto_def_15" + (30, 22), # link name, "goto_def_16" + ], + itertools.repeat("symbol_def_variable_declaration_main"), +) +mal_symbol_def_preconditions_points = zip( + [ + (11, 15), # preconditions, "goto_def_17" + (19, 13), # preconditions extended asset, "goto_def_18" + (24, 28), # preconditions complex 1, "goto_def_19" + (26, 28), # preconditions complex 1, "goto_def_20" + ], + itertools.repeat("symbol_def_preconditions"), +) # this one is specifically list so that the last case can be reused in # test_goto_definition_wrong_symbol -mal_symbol_def_reaches_points = list(zip([ - (13, 22), # reaches, "goto_def_21" - (14, 14), # reaches single attack step, "goto_def_22" - (10, 7), # random non-user defined symbol, "goto_def_23" - ], itertools.repeat("symbol_def_reaches"))) +mal_symbol_def_reaches_points = list( + zip( + [ + (13, 22), # reaches, "goto_def_21" + (14, 14), # reaches single attack step, "goto_def_22" + (10, 7), # random non-user defined symbol, "goto_def_23" + ], + itertools.repeat("symbol_def_reaches"), + ) +) -goto_input_location_and_files = itertools.chain(mal_find_symbols_in_scope_points, - mal_symbol_def_extended_asset_main_points, - mal_symbol_def_variable_call_extend_chain_main_points, - symbol_def_variable_declaration_main_points, - mal_symbol_def_preconditions_points, - mal_symbol_def_reaches_points) +goto_input_location_and_files = itertools.chain( + mal_find_symbols_in_scope_points, + mal_symbol_def_extended_asset_main_points, + mal_symbol_def_variable_call_extend_chain_main_points, + symbol_def_variable_declaration_main_points, + mal_symbol_def_preconditions_points, + mal_symbol_def_reaches_points, +) goto_expected_location_and_files = [ - ((3, 0), "find_symbols_in_scope"), # "goto_def_1" - ((11, 4), "find_symbols_in_scope"), # "goto_def_2" - ((7, 6), "find_symbols_in_scope"), # "goto_def_3" - ((8, 8), "find_symbols_in_scope"), # "goto_def_4" - ((2, 4), "symbol_def_extended_asset_aux3"), # "goto_def_5" - ((4, 6), "symbol_def_extended_asset_aux3"), # "goto_def_6" - ((5, 6), "symbol_def_variable_call_extend_chain_aux2"), # "goto_def_7" - ((5, 4), "symbol_def_variable_declaration_main"), # "goto_def_8" - ((13, 4), "symbol_def_variable_declaration_main"), # "goto_def_9" - ((15, 4), "symbol_def_variable_declaration_main"), # "goto_def_10" - ((15, 4), "symbol_def_variable_declaration_main"), # "goto_def_11" - ((8, 4), "symbol_def_variable_declaration_main"), # "goto_def_12" - ((7, 4), "symbol_def_variable_declaration_main"), # "goto_def_13" - ((28, 4), "symbol_def_variable_declaration_main"), # "goto_def_14" - ((28, 4), "symbol_def_variable_declaration_main"), # "goto_def_15" - ((30, 4), "symbol_def_variable_declaration_main"), # "goto_def_16" - ((5, 4), "symbol_def_preconditions"), # "goto_def_17" - ((14, 4), "symbol_def_preconditions"), # "goto_def_18" - ((16, 4), "symbol_def_preconditions"), # "goto_def_19" - ((16, 4), "symbol_def_preconditions"), # "goto_def_20" - ((6, 8), "symbol_def_reaches"), # "goto_def_21" - ((15, 6), "symbol_def_reaches"), # "goto_def_22" + ((3, 0), "find_symbols_in_scope"), # "goto_def_1" + ((11, 4), "find_symbols_in_scope"), # "goto_def_2" + ((7, 6), "find_symbols_in_scope"), # "goto_def_3" + ((8, 8), "find_symbols_in_scope"), # "goto_def_4" + ((2, 4), "symbol_def_extended_asset_aux3"), # "goto_def_5" + ((4, 6), "symbol_def_extended_asset_aux3"), # "goto_def_6" + ((5, 6), "symbol_def_variable_call_extend_chain_aux2"), # "goto_def_7" + ((5, 4), "symbol_def_variable_declaration_main"), # "goto_def_8" + ((13, 4), "symbol_def_variable_declaration_main"), # "goto_def_9" + ((15, 4), "symbol_def_variable_declaration_main"), # "goto_def_10" + ((15, 4), "symbol_def_variable_declaration_main"), # "goto_def_11" + ((8, 4), "symbol_def_variable_declaration_main"), # "goto_def_12" + ((7, 4), "symbol_def_variable_declaration_main"), # "goto_def_13" + ((28, 4), "symbol_def_variable_declaration_main"), # "goto_def_14" + ((28, 4), "symbol_def_variable_declaration_main"), # "goto_def_15" + ((30, 4), "symbol_def_variable_declaration_main"), # "goto_def_16" + ((5, 4), "symbol_def_preconditions"), # "goto_def_17" + ((14, 4), "symbol_def_preconditions"), # "goto_def_18" + ((16, 4), "symbol_def_preconditions"), # "goto_def_19" + ((16, 4), "symbol_def_preconditions"), # "goto_def_20" + ((6, 8), "symbol_def_reaches"), # "goto_def_21" + ((15, 6), "symbol_def_reaches"), # "goto_def_22" ] # [((input location, input file), (expected location, expected file))] -find_symbols_in_scope_expected_results = list(zip(goto_input_location_and_files, - goto_expected_location_and_files)) +find_symbols_in_scope_expected_results = list( + zip(goto_input_location_and_files, goto_expected_location_and_files) +) # Fixtures type FixtureCallback[T] = typing.Callable[[str, typing.BinaryIO, (int, int)], T] + @pytest.fixture def open_document_notification( - client_notifications: list[dict], - client_messages: list[dict]) -> FixtureCallback[dict]: + client_notifications: list[dict], client_messages: list[dict] +) -> FixtureCallback[dict]: def make(uri: str, file: typing.BinaryIO, _location) -> dict: message = { "jsonrpc": "2.0", @@ -104,12 +128,14 @@ def make(uri: str, file: typing.BinaryIO, _location) -> dict: client_notifications.append(message) client_messages.append(message) return message + return make + @pytest.fixture def definition_request( - client_requests: list[dict], - client_messages: list[dict]) -> FixtureCallback[dict]: + client_requests: list[dict], client_messages: list[dict] +) -> FixtureCallback[dict]: def make(uri: str, _file, location: (int, int)) -> dict: line, char = location message = { @@ -129,20 +155,24 @@ def make(uri: str, _file, location: (int, int)) -> dict: client_requests.append(message) client_messages.append(message) return message + return make + @pytest.fixture def goto_definition_client_messages( - client_messages: list[dict], - initalize_request, - initalized_notification, - open_document_notification: FixtureCallback[dict], - definition_request: FixtureCallback[dict]) -> FixtureCallback[typing.BinaryIO]: + client_messages: list[dict], + initalize_request, + initalized_notification, + open_document_notification: FixtureCallback[dict], + definition_request: FixtureCallback[dict], +) -> FixtureCallback[typing.BinaryIO]: def make(uri: str, file: typing.BinaryIO, location: (int, int)) -> typing.BinaryIO: args = (uri, file, location) open_document_notification(*args) definition_request(*args) return build_rpc_message_stream(client_messages) + return make @@ -152,21 +182,25 @@ def parameter_id(argvalue: (((int, int), str), ((int, int), str))) -> object: inputs, outputs = argvalue (origin_line, origin_char), origin_file = inputs (expected_line, expected_char), expected_file = outputs - return (f"{origin_file}:L{origin_line},C{origin_char}" - "-" - f"{expected_file}:L{expected_line},C{expected_char}") + return ( + f"{origin_file}:L{origin_line},C{origin_char}" + "-" + f"{expected_file}:L{expected_line},C{expected_char}" + ) + # Tests @pytest.mark.parametrize( "inputs,expected_outputs", find_symbols_in_scope_expected_results, - ids=map(parameter_id, find_symbols_in_scope_expected_results) + ids=map(parameter_id, find_symbols_in_scope_expected_results), ) def test_goto_definition( - request: pytest.FixtureRequest, - inputs: ((int, int), str), - expected_outputs: ((int, int), str), - goto_definition_client_messages: FixtureCallback[typing.BinaryIO]): + request: pytest.FixtureRequest, + inputs: ((int, int), str), + expected_outputs: ((int, int), str), + goto_definition_client_messages: FixtureCallback[typing.BinaryIO], +): origin_location, origin_file = inputs expected_point, expected_file = expected_outputs @@ -193,8 +227,9 @@ def test_goto_definition( def test_goto_definition_wrong_symbol( - request: pytest.FixtureRequest, - goto_definition_client_messages: FixtureCallback[typing.BinaryIO]): + request: pytest.FixtureRequest, + goto_definition_client_messages: FixtureCallback[typing.BinaryIO], +): """ This test aims to check that the LS can handle requests for symbols which are not user-defined diff --git a/tests/integration/test_publish_diagnostics.py b/tests/integration/test_publish_diagnostics.py index d408dba..383161e 100644 --- a/tests/integration/test_publish_diagnostics.py +++ b/tests/integration/test_publish_diagnostics.py @@ -1,8 +1,6 @@ import typing from pathlib import Path -import pytest - from malls.lsp.enums import DiagnosticSeverity from ..util import get_lsp_json, server_output @@ -14,8 +12,8 @@ pytest_plugins = ["tests.fixtures.lsp.publish_diagnostics"] -def test_diagnostics_when_opening_file_with_error( - erroneous_file_client_messages: typing.BinaryIO): + +def test_diagnostics_when_opening_file_with_error(erroneous_file_client_messages: typing.BinaryIO): # send to server output, ls, *_ = server_output(erroneous_file_client_messages) @@ -37,7 +35,8 @@ def test_diagnostics_when_opening_file_with_error( def test_diagnostics_when_opening_file_with_include_error( - erroneous_include_file_client_messages: typing.BinaryIO): + erroneous_include_file_client_messages: typing.BinaryIO, +): # send to server output, ls, *_ = server_output(erroneous_include_file_client_messages) @@ -55,7 +54,8 @@ def test_diagnostics_when_opening_file_with_include_error( def test_diagnostics_when_opening_file_with_include_error_and_opening_bad_file( - erroenous_include_and_file_with_error_client_messages: typing.BinaryIO): + erroenous_include_and_file_with_error_client_messages: typing.BinaryIO, +): # send to server output, ls, *_ = server_output(erroenous_include_and_file_with_error_client_messages) @@ -74,8 +74,10 @@ def test_diagnostics_when_opening_file_with_include_error_and_opening_bad_file( output.close() + def test_diagnostics_when_changing_file_with_error( - change_file_with_error_client_messages: typing.BinaryIO): + change_file_with_error_client_messages: typing.BinaryIO, +): # send to server output, ls, *_ = server_output(change_file_with_error_client_messages) diff --git a/tests/unit/test_find_current_scope.py b/tests/unit/test_find_current_scope.py index f2724db..2c3c800 100644 --- a/tests/unit/test_find_current_scope.py +++ b/tests/unit/test_find_current_scope.py @@ -7,41 +7,49 @@ @pytest.fixture -def find_current_scope_function_tree(utf8_mal_parser: Parser, - mal_find_current_scope_function: typing.BinaryIO) -> Tree: +def find_current_scope_function_tree( + utf8_mal_parser: Parser, mal_find_current_scope_function: typing.BinaryIO +) -> Tree: return utf8_mal_parser.parse(mal_find_current_scope_function.read()) + @pytest.fixture def find_current_scope_function_cursor(find_current_scope_function_tree: Tree) -> TreeCursor: return find_current_scope_function_tree.walk() -parameters = [((4, 0), "category_declaration"), - ((7, 13), "asset_declaration"), - ((17, 19), "associations_declaration"), - ((1, 10), "source_file"), - ((10, 10), "category_declaration"), - ((11, 4), "asset_declaration"), - ((13, 4), "asset_declaration"), - ((3, 10), "source_file"), - ((3, 17), "category_declaration"), - ((15, 4), "source_file"), - ((18, 0), "associations_declaration")] - -parameter_ids = ["on_space_between_category_and_asset", - "inside_asset_declaration", - "inside_association_declaration", - "outside_all_components", - "on_name_of_asset", - "on_bracket_of_asset", - "on_closing_bracket_of_asset", - "on_name_of_category", - "on_bracket_of_category", - "on_term_association", - "on_bracket_of_association"] + +parameters = [ + ((4, 0), "category_declaration"), + ((7, 13), "asset_declaration"), + ((17, 19), "associations_declaration"), + ((1, 10), "source_file"), + ((10, 10), "category_declaration"), + ((11, 4), "asset_declaration"), + ((13, 4), "asset_declaration"), + ((3, 10), "source_file"), + ((3, 17), "category_declaration"), + ((15, 4), "source_file"), + ((18, 0), "associations_declaration"), +] + +parameter_ids = [ + "on_space_between_category_and_asset", + "inside_asset_declaration", + "inside_association_declaration", + "outside_all_components", + "on_name_of_asset", + "on_bracket_of_asset", + "on_closing_bracket_of_asset", + "on_name_of_category", + "on_bracket_of_category", + "on_term_association", + "on_bracket_of_association", +] + @pytest.mark.parametrize("point,expected_node_type", parameters, ids=parameter_ids) -def test_find_current_scope(point: (int, int), - expected_node_type: str, - find_current_scope_function_cursor: TreeCursor): +def test_find_current_scope( + point: (int, int), expected_node_type: str, find_current_scope_function_cursor: TreeCursor +): node = find_current_scope(find_current_scope_function_cursor, point) assert node.type == expected_node_type diff --git a/tests/unit/test_find_meta_comment_function.py b/tests/unit/test_find_meta_comment_function.py index fdb7572..a437945 100644 --- a/tests/unit/test_find_meta_comment_function.py +++ b/tests/unit/test_find_meta_comment_function.py @@ -1,5 +1,4 @@ import typing -from pathlib import Path import pytest from tree_sitter import Parser, Tree @@ -22,31 +21,34 @@ ((57, 37), {b"dev asset4", b"mod asset4"}), ] + @pytest.fixture def find_meta_comment_data(mal_find_meta_comment_function: typing.BinaryIO) -> bytes: return mal_find_meta_comment_function.read() + @pytest.fixture -def find_meta_comment_tree(utf8_mal_parser: Parser, - find_meta_comment_data: bytes) -> Tree: +def find_meta_comment_tree(utf8_mal_parser: Parser, find_meta_comment_data: bytes) -> Tree: return utf8_mal_parser.parse(find_meta_comment_data) + @pytest.mark.parametrize( "point,expected_comments", parameters, ) def test_find_meta_comment_function( - mal_root_str: str, - mal_find_meta_comment_function_uri: str, - find_meta_comment_data: bytes, - find_meta_comment_tree: Tree, - point: (int, int), - expected_comments: list[bytes]): + mal_root_str: str, + mal_find_meta_comment_function_uri: str, + find_meta_comment_data: bytes, + find_meta_comment_tree: Tree, + point: (int, int), + expected_comments: list[bytes], +): # build the storage (mimicks the file parsing in the server) storage = {} doc_uri = mal_find_meta_comment_function_uri - source_encoded = find_meta_comment_data + source_encoded = find_meta_comment_data tree = find_meta_comment_tree storage[doc_uri] = Document(tree, source_encoded, doc_uri) diff --git a/tests/unit/test_find_symbol_definition_function.py b/tests/unit/test_find_symbol_definition_function.py index edbdc66..44e3879 100644 --- a/tests/unit/test_find_symbol_definition_function.py +++ b/tests/unit/test_find_symbol_definition_function.py @@ -10,25 +10,23 @@ pytest_plugins = ["tests.unit.test_find_symbols_in_scope"] mal_find_symbols_in_scope_points = zip( - [(3, 12), # category_declaration - (11, 10), # asset declaration, asset name - (7, 10), # asset variable - (8, 10), # attack step - ], - [(3, 0), - (11, 4), - (7, 6), - (8, 8)]) - - -@pytest.mark.parametrize( - "point,expected_result", - mal_find_symbols_in_scope_points) + [ + (3, 12), # category_declaration + (11, 10), # asset declaration, asset name + (7, 10), # asset variable + (8, 10), # attack step + ], + [(3, 0), (11, 4), (7, 6), (8, 8)], +) + + +@pytest.mark.parametrize("point,expected_result", mal_find_symbols_in_scope_points) def test_symbol_definition_withouth_building_storage( - request: pytest.FixtureRequest, - point: (int, int), - expected_result: (int, int), - find_symbols_in_scope_cursor: TreeCursor): + request: pytest.FixtureRequest, + point: (int, int), + expected_result: (int, int), + find_symbols_in_scope_cursor: TreeCursor, +): # go to name of category # get the node @@ -46,68 +44,70 @@ def test_symbol_definition_withouth_building_storage( mal_symbol_def_extended_asset_main_points = zip( - repeat("mal_symbol_def_extended_asset_main"), - [(6, 25), # asset declaration, extended asset - (9, 11)], # variable call - [(2, 4), - (4, 6)]) + repeat("mal_symbol_def_extended_asset_main"), + [ + (6, 25), # asset declaration, extended asset + (9, 11), + ], # variable call + [(2, 4), (4, 6)], +) mal_symbol_def_variable_call_extend_chain_main_points = zip( - repeat("mal_symbol_def_variable_call_extend_chain_main"), - [(9, 11)], # variable call, extend chain - [(5, 6)]) + repeat("mal_symbol_def_variable_call_extend_chain_main"), + [(9, 11)], # variable call, extend chain + [(5, 6)], +) symbol_def_variable_declaration_main_points = zip( - repeat("mal_symbol_def_variable_declaration_main"), - [(10, 20), # variable declaration - (17, 21), # variable declaration, extended asset - (21, 36), # variable declaration complex 1 - (22, 36), # variable declaration complex 2 - (26, 6), # association asset name 1 - (27, 37), # association asset name 2 - (28, 12), # association field name 1 - (28, 32), # association field name 2 - (30, 22)], # link name - [(5, 4), - (13, 4), - (15, 4), - (15, 4), - (8, 4), - (7, 4), - (28, 4), - (28, 4), - (30, 4)]) + repeat("mal_symbol_def_variable_declaration_main"), + [ + (10, 20), # variable declaration + (17, 21), # variable declaration, extended asset + (21, 36), # variable declaration complex 1 + (22, 36), # variable declaration complex 2 + (26, 6), # association asset name 1 + (27, 37), # association asset name 2 + (28, 12), # association field name 1 + (28, 32), # association field name 2 + (30, 22), + ], # link name + [(5, 4), (13, 4), (15, 4), (15, 4), (8, 4), (7, 4), (28, 4), (28, 4), (30, 4)], +) mal_symbol_def_preconditions_points = zip( - repeat("mal_symbol_def_preconditions"), - [(11, 15), # preconditions - (19, 13), # preconditions extended asset - (24, 28), # preconditions complex 1 - (26, 28)], # preconditions complex 1 - [(5 , 4), - (14, 4), - (16, 4), - (16, 4)]) + repeat("mal_symbol_def_preconditions"), + [ + (11, 15), # preconditions + (19, 13), # preconditions extended asset + (24, 28), # preconditions complex 1 + (26, 28), + ], # preconditions complex 1 + [(5, 4), (14, 4), (16, 4), (16, 4)], +) mal_symbol_def_reaches_points = zip( - repeat("mal_symbol_def_reaches"), - [(13, 22), # reaches - (14, 14)], # reaches single attack step - [(6 , 8), - (15, 6)]) - -parameters = chain(mal_symbol_def_extended_asset_main_points, - mal_symbol_def_variable_call_extend_chain_main_points, - symbol_def_variable_declaration_main_points, - mal_symbol_def_preconditions_points, - mal_symbol_def_reaches_points) - - -@pytest.mark.parametrize( - "fixture_name,point,expected_result",parameters) + repeat("mal_symbol_def_reaches"), + [ + (13, 22), # reaches + (14, 14), + ], # reaches single attack step + [(6, 8), (15, 6)], +) + +parameters = chain( + mal_symbol_def_extended_asset_main_points, + mal_symbol_def_variable_call_extend_chain_main_points, + symbol_def_variable_declaration_main_points, + mal_symbol_def_preconditions_points, + mal_symbol_def_reaches_points, +) + + +@pytest.mark.parametrize("fixture_name,point,expected_result", parameters) def test_symbol_definition_with_storage( - request: pytest.FixtureRequest, - utf8_mal_parser: Parser, - mal_root_str: str, - fixture_name: str, - point: (int, int), - expected_result: (int, int)): + request: pytest.FixtureRequest, + utf8_mal_parser: Parser, + mal_root_str: str, + fixture_name: str, + point: (int, int), + expected_result: (int, int), +): # build the storage (mimicks the file parsing in the server) storage = {} diff --git a/tests/unit/test_find_symbols_in_context_hierarchy.py b/tests/unit/test_find_symbols_in_context_hierarchy.py index bf8b211..e9af53a 100644 --- a/tests/unit/test_find_symbols_in_context_hierarchy.py +++ b/tests/unit/test_find_symbols_in_context_hierarchy.py @@ -4,98 +4,107 @@ from malls.ts.utils import find_symbols_in_context_hierarchy # (syntax tree/ts point, expected user symbols + level, expected keywords + level) -parameters = [((4, 0), - [("var", -1), - ("c", -1), - ("compromise", -1), - ("destroy", -1), - ("Asset1", 0), - ("Asset2", 0), - ("Asset3", 0)], - [("let", -1), - ("extends", 0), - ("abstract", 0), - ("asset", 0), - ("info", 1), - ("category", 1), - ("associations", 1)]), - ((17, 0), - [("a", 0), - ("c", 0), - ("d", 0), - ("e", 0), - ("L", 0), - ("M", 0), - ("Asset1", 0), - ("Asset2", 0)], - [("info", 1), - ("category", 1), - ("associations", 1)]), - ((6, 4), - [("var", 0), - ("c", 0), - ("compromise", 0), - ("destroy", 0), - ("Asset1", 1), - ("Asset2", 1), - ("Asset3", 1)], - [("let", 0), - ("extends", 1), - ("abstract", 1), - ("asset", 1), - ("category", 2), - ("associations", 2), - ("info", 2)]), - ((12, 4), - [("destroy", 0), - ("Asset1", 1), - ("Asset2", 1), - ("Asset3", 1)], - [("let", 0), - ("extends", 1), - ("abstract", 1), - ("asset", 1), - ("category", 2), - ("associations", 2), - ("info", 2)]), - ((0, 0), - [("var", -2), - ("compromise", -2), - ("destroy", -2), - ("Asset1", -1), - ("Asset2", -1), - ("Asset3", -1), - ("a", -1), - ("d", -1), - ("e", -1), - ("L", -1), - ("M", -1), - ("c", -1)], - [("let", -2), - ("extends", -1), - ("abstract", -1), - ("asset", -1), - ("info", -1), - ("category", 0), - ("associations", 0)])] +parameters = [ + ( + (4, 0), + [ + ("var", -1), + ("c", -1), + ("compromise", -1), + ("destroy", -1), + ("Asset1", 0), + ("Asset2", 0), + ("Asset3", 0), + ], + [ + ("let", -1), + ("extends", 0), + ("abstract", 0), + ("asset", 0), + ("info", 1), + ("category", 1), + ("associations", 1), + ], + ), + ( + (17, 0), + [("a", 0), ("c", 0), ("d", 0), ("e", 0), ("L", 0), ("M", 0), ("Asset1", 0), ("Asset2", 0)], + [("info", 1), ("category", 1), ("associations", 1)], + ), + ( + (6, 4), + [ + ("var", 0), + ("c", 0), + ("compromise", 0), + ("destroy", 0), + ("Asset1", 1), + ("Asset2", 1), + ("Asset3", 1), + ], + [ + ("let", 0), + ("extends", 1), + ("abstract", 1), + ("asset", 1), + ("category", 2), + ("associations", 2), + ("info", 2), + ], + ), + ( + (12, 4), + [("destroy", 0), ("Asset1", 1), ("Asset2", 1), ("Asset3", 1)], + [ + ("let", 0), + ("extends", 1), + ("abstract", 1), + ("asset", 1), + ("category", 2), + ("associations", 2), + ("info", 2), + ], + ), + ( + (0, 0), + [ + ("var", -2), + ("compromise", -2), + ("destroy", -2), + ("Asset1", -1), + ("Asset2", -1), + ("Asset3", -1), + ("a", -1), + ("d", -1), + ("e", -1), + ("L", -1), + ("M", -1), + ("c", -1), + ], + [ + ("let", -2), + ("extends", -1), + ("abstract", -1), + ("asset", -1), + ("info", -1), + ("category", 0), + ("associations", 0), + ], + ), +] -parameter_ids = ["category", - "associations", - "asset1", - "asset2", - "root_node"] +parameter_ids = ["category", "associations", "asset1", "asset2", "root_node"] pytest_plugins = ["tests.unit.test_find_symbols_in_scope"] -@pytest.mark.parametrize( - "point,expected_symbols,expected_keywords", - parameters, - ids=parameter_ids) + +@pytest.mark.parametrize("point,expected_symbols,expected_keywords", parameters, ids=parameter_ids) def test_find_symbols_in_hierarchy( - point: (int, int), - expected_symbols: list[(str, int)], - expected_keywords: list[(str, int)], - find_symbols_in_scope_cursor: TreeCursor): + point: (int, int), + expected_symbols: list[(str, int)], + expected_keywords: list[(str, int)], + find_symbols_in_scope_cursor: TreeCursor, +): user_symbols, keywords = find_symbols_in_context_hierarchy(find_symbols_in_scope_cursor, point) assert len(user_symbols.keys()) == len(expected_symbols) diff --git a/tests/unit/test_find_symbols_in_scope.py b/tests/unit/test_find_symbols_in_scope.py index 0de5c37..35314ea 100644 --- a/tests/unit/test_find_symbols_in_scope.py +++ b/tests/unit/test_find_symbols_in_scope.py @@ -1,4 +1,5 @@ import typing + import pytest import tree_sitter @@ -7,64 +8,58 @@ @pytest.fixture def find_symbols_in_scope_tree( - utf8_mal_parser: tree_sitter.Parser, - mal_find_symbols_in_scope: typing.BinaryIO) -> tree_sitter.Tree: + utf8_mal_parser: tree_sitter.Parser, mal_find_symbols_in_scope: typing.BinaryIO +) -> tree_sitter.Tree: return utf8_mal_parser.parse(mal_find_symbols_in_scope.read()) + @pytest.fixture def find_symbols_in_scope_cursor( - find_symbols_in_scope_tree: tree_sitter.Tree) -> tree_sitter.TreeCursor: + find_symbols_in_scope_tree: tree_sitter.Tree, +) -> tree_sitter.TreeCursor: return find_symbols_in_scope_tree.walk() + # (syntax tree/ts point, expected user symbols, expected keywords) -parameters = [((4, 0), - {"Asset1", - "Asset2", - "Asset3"}, - {"extends", - "abstract", - "asset", - "info"}), - ((17, 0), - {"a", - "c", - "d", - "e", - "L", - "M", - "Asset1", - "Asset2", - }, - {"info"}), - ((7, 10), - {"var", - "c", - "compromise", - "destroy"}, - {"let", "info"}), - ((13, 10), - {"destroy"}, - {"info", "let"}), - ((1, 0), - set(), - {"category", "associations", "info"})] +parameters = [ + ((4, 0), {"Asset1", "Asset2", "Asset3"}, {"extends", "abstract", "asset", "info"}), + ( + (17, 0), + { + "a", + "c", + "d", + "e", + "L", + "M", + "Asset1", + "Asset2", + }, + {"info"}, + ), + ((7, 10), {"var", "c", "compromise", "destroy"}, {"let", "info"}), + ((13, 10), {"destroy"}, {"info", "let"}), + ((1, 0), set(), {"category", "associations", "info"}), +] + +parameter_ids = [ + "category_scope", + "association_scope", + "asset1_scope", + "asset2_scope", + "root_node_scope", +] -parameter_ids = ["category_scope", - "association_scope", - "asset1_scope", - "asset2_scope", - "root_node_scope"] @pytest.mark.parametrize( - "point,expected_user_symbols,expected_keywords", - parameters, - ids=parameter_ids) + "point,expected_user_symbols,expected_keywords", parameters, ids=parameter_ids +) def test_find_symbols( - point: (int, int), - expected_user_symbols: set[str], - expected_keywords: set[str], - find_symbols_in_scope_cursor: tree_sitter.TreeCursor): - + point: (int, int), + expected_user_symbols: set[str], + expected_keywords: set[str], + find_symbols_in_scope_cursor: tree_sitter.TreeCursor, +): user_symbols, keywords = find_symbols_in_current_scope(find_symbols_in_scope_cursor, point) # we use sets to ensure order does not matter diff --git a/tests/unit/test_position_conversion.py b/tests/unit/test_position_conversion.py index fccb734..a79100c 100644 --- a/tests/unit/test_position_conversion.py +++ b/tests/unit/test_position_conversion.py @@ -5,35 +5,28 @@ # (text, input/utf16/lsp position, output/utf8/ts position) parameters = [ - # h e l l o w o r l d - # 0 1 2 3 4 5 6 7 8 9 10 - # target 'w' - position 6 - ("hello world".encode(), (0, 6), (0, 6)), - # emoji takes 2 UTF-16 characters, so the emoji is 4 bytes - # a ๐Ÿš€ b - # 0 1 2 3 - # target 'b' - 3 - # In bytes should be 5, 1 for 'a', 4 for emoji - ("a๐Ÿš€b".encode(), (0, 3), (0, 5)), - # รŸ รง ๐Ÿ - # 0 1 2 3 - # target '๐Ÿ' - position 2 (start) - # In bytes รŸ, รง take 2 bytes and emoji 4 -> position 2+2 = 4 - ("รŸรง๐Ÿ".encode(), (0, 2), (0, 4)), - (" ".encode(), (0, 0), (0, 0)) - ] -parameter_ids = [ - "ascii_chars_only", - "with_emoji", - "ascii_and_multibyte_chars", - "empty_string" - ] + # h e l l o w o r l d + # 0 1 2 3 4 5 6 7 8 9 10 + # target 'w' - position 6 + ("hello world".encode(), (0, 6), (0, 6)), + # emoji takes 2 UTF-16 characters, so the emoji is 4 bytes + # a ๐Ÿš€ b + # 0 1 2 3 + # target 'b' - 3 + # In bytes should be 5, 1 for 'a', 4 for emoji + ("a๐Ÿš€b".encode(), (0, 3), (0, 5)), + # รŸ รง ๐Ÿ + # 0 1 2 3 + # target '๐Ÿ' - position 2 (start) + # In bytes รŸ, รง take 2 bytes and emoji 4 -> position 2+2 = 4 + ("รŸรง๐Ÿ".encode(), (0, 2), (0, 4)), + (" ".encode(), (0, 0), (0, 0)), +] +parameter_ids = ["ascii_chars_only", "with_emoji", "ascii_and_multibyte_chars", "empty_string"] + @pytest.mark.parametrize("text,lsp_position,ts_position", parameters, ids=parameter_ids) -def test_position_conversion( - text: bytes, - lsp_position: (int, int), - ts_position: (int, int)): +def test_position_conversion(text: bytes, lsp_position: (int, int), ts_position: (int, int)): position = Position(line=lsp_position[0], character=lsp_position[1]) result_position = lsp_to_tree_sitter_position(text, position) diff --git a/tests/unit/test_visit_expr.py b/tests/unit/test_visit_expr.py index e752003..7839055 100644 --- a/tests/unit/test_visit_expr.py +++ b/tests/unit/test_visit_expr.py @@ -7,62 +7,67 @@ @pytest.fixture -def tree(utf8_mal_parser: Parser, - mal_visit_expr: typing.BinaryIO) -> Tree: +def tree(utf8_mal_parser: Parser, mal_visit_expr: typing.BinaryIO) -> Tree: return utf8_mal_parser.parse(mal_visit_expr.read()) + @pytest.fixture def cursor(tree: Tree) -> TreeCursor: return tree.walk() + def goto_asset_expression(cursor: TreeCursor, point: (int, int)): while cursor.node.type != "asset_expr": cursor.goto_first_child_for_point(point) cursor.goto_first_child() + point_found_parameters = [ ((9, 11), [b"a", b"b", b"c"]), ((10, 11), [b"a", b"z", b"b", b"c"]), - ((11, 11), [ - b"a", - b"z", - b"b", - b"y", - b"c", - b"f", - b"l", - b"h", - b"n", - b"m", - b"e", - b"x", - b"u", - b"t", - ]), + ( + (11, 11), + [ + b"a", + b"z", + b"b", + b"y", + b"c", + b"f", + b"l", + b"h", + b"n", + b"m", + b"e", + b"x", + b"u", + b"t", + ], + ), ((12, 11), [b"a", b"b", b"c"]), ((13, 11), [b"a", b"b", b"c"]), ((14, 11), [b"a", b"b", b"d", b"e", b"f", b"h", b"i"]), ((15, 11), [(b"d", "asset")]), ((16, 11), [(b"g", "asset"), b"h", b"i"]), - ((17, 11), [b"w", b"x", b"y", b"z", b"a"]) + ((17, 11), [b"w", b"x", b"y", b"z", b"a"]), +] + +parameter_names = [ + "only_collects", + "simple_paranthesized", + "various_paranthesized", + "unop", + "single_binop", + "various_binop", + "single_type", + "various_type", + "variable", ] -parameter_names = ["only_collects", - "simple_paranthesized", - "various_paranthesized", - "unop", - "single_binop", - "various_binop", - "single_type", - "various_type", - "variable"] @pytest.mark.parametrize("point,expected", point_found_parameters, ids=parameter_names) -def test_visir_expr( - point: (int, int), - expected: list[bytes], - cursor: TreeCursor): +def test_visir_expr(point: (int, int), expected: list[bytes], cursor: TreeCursor): goto_asset_expression(cursor, point) found = [] visit_expr(cursor, found) diff --git a/tests/util.py b/tests/util.py index 09410fe..89dccce 100644 --- a/tests/util.py +++ b/tests/util.py @@ -17,9 +17,8 @@ def find_last_request( - requests: list[dict], - condition: typing.Callable[[dict], bool] | str, - default=None) -> dict: + requests: list[dict], condition: typing.Callable[[dict], bool] | str, default=None +) -> dict: """ Searches through the list of requests in reverse order and returns first (logically last) element fulfilling the condition. If condition is a string, then it finds the last request @@ -29,8 +28,10 @@ def find_last_request( """ if isinstance(condition, str): method_name = condition + def condition(request: dict) -> bool: request.get("method") == method_name + return next(filter(condition, reversed(requests)), default) @@ -113,9 +114,10 @@ def template() -> typing.BinaryIO: return template def fixture_uri(file_path: str | Path): - uri = uritools.uricompose(scheme="file", path=str(file_path)) - def template() -> str: - return uri + uri = uritools.uricompose(scheme="file", path=str(file_path)) + + def template() -> str: + return uri fixture_name = fixture_name_from_file( path, extension_renaming=extension_renaming, root_folder=root_folder @@ -128,10 +130,7 @@ def template() -> str: uri_fixture_name = fixture_name + "_uri" - uri_fixture = pytest.fixture( - fixture_uri(Path(path).absolute()), - name=uri_fixture_name - ) + uri_fixture = pytest.fixture(fixture_uri(Path(path).absolute()), name=uri_fixture_name) return fixture, fixture_name, uri_fixture, uri_fixture_name @@ -147,19 +146,20 @@ def load_fixture_file_into_module( Shares options with `fixture_name_from_file`. """ - fixture, fixture_name, *_ = load_file_as_fixture(path, - extension_renaming=extension_renaming, - root_folder=root_folder) + fixture, fixture_name, *_ = load_file_as_fixture( + path, extension_renaming=extension_renaming, root_folder=root_folder + ) setattr(module, fixture_name, fixture) + # NOTE: On noqa C417 # Ruff wants to use list generators instead, but that will end up creating many useless # intermediary lists which is hurtful for performance. def load_directory_files_as_fixtures( - dir_path: str | Path, - extension: str | None = None, - extension_renaming: typing.Callable[[str], str] | dict[str, str] | None = None) \ - -> [(typing.Callable, str, typing.Callable, str)]: + dir_path: str | Path, + extension: str | None = None, + extension_renaming: typing.Callable[[str], str] | dict[str, str] | None = None, +) -> [(typing.Callable, str, typing.Callable, str)]: """ Loads all file contents in a given directory, aside from .py, and their URI's as fixtures, using `load_file_as_fixture`. @@ -177,15 +177,18 @@ def load_directory_files_as_fixtures( # Filter all entries in the directory to non-python files def non_python_file(entry: os.DirEntry) -> bool: return entry.is_file() and not entry.path.endswith(".py") + file_entries = os.scandir(dir_path) non_python_file_entries = filter(non_python_file, file_entries) - files = map(lambda entry: entry.path, non_python_file_entries) # noqa C417 + files = map(lambda entry: entry.path, non_python_file_entries) # noqa C417 # Concat the file names with the directory to get relative to root path # then load the file as a fixture, getting the name and absolute path in the process - file_paths = map(lambda file: path.join(dir_path, file), files) # noqa C417 - fixture_name_paths = map(lambda path: load_file_as_fixture(path, root_folder=dir_path), # noqa C417 - file_paths) + file_paths = map(lambda file: path.join(dir_path, file), files) # noqa C417 + fixture_name_paths = map( + lambda path: load_file_as_fixture(path, root_folder=dir_path), # noqa C417 + file_paths, + ) return list(fixture_name_paths) From efbbea25ab43d5c33d3e7c8a5494e5d85e2a0929 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 22:53:41 +0200 Subject: [PATCH 38/42] chore: Disable ruff line length lint on TODOs --- tests/fixtures/lsp/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/fixtures/lsp/conftest.py b/tests/fixtures/lsp/conftest.py index 3547ec9..26468c7 100644 --- a/tests/fixtures/lsp/conftest.py +++ b/tests/fixtures/lsp/conftest.py @@ -123,14 +123,14 @@ def initalize_response( "jsonrpc": "2.0", "id": find_last_request(client_requests, "initalize", {}).get("id", 0), "result": { - # TODO: Replace with values from an actual server instance (e.g. via instance.capabilities()) + # TODO: Replace with values from an actual server instance (e.g. via instance.capabilities()) # noqa: E501 "capabilities": { "positionEncoding": "utf-16", "textDocumentSync": {"openClose": True, "change": 1}, "definitionProvider": True, "completionProvider": {}, }, - # TODO: Replace with values from an actual server instance (e.g. via instance.server_info()) + # TODO: Replace with values from an actual server instance (e.g. via instance.server_info()) # noqa: E501 "serverInfo": {"name": "mal-ls"}, }, } From 90cdb135ceb5e86858417bc7620d74db7a45a472 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 22:56:04 +0200 Subject: [PATCH 39/42] chore: Replace map with generator expressions --- tests/util.py | 423 +------------------------------------------------- 1 file changed, 3 insertions(+), 420 deletions(-) diff --git a/tests/util.py b/tests/util.py index 89dccce..299dda9 100644 --- a/tests/util.py +++ b/tests/util.py @@ -152,9 +152,6 @@ def load_fixture_file_into_module( setattr(module, fixture_name, fixture) -# NOTE: On noqa C417 -# Ruff wants to use list generators instead, but that will end up creating many useless -# intermediary lists which is hurtful for performance. def load_directory_files_as_fixtures( dir_path: str | Path, extension: str | None = None, @@ -180,15 +177,12 @@ def non_python_file(entry: os.DirEntry) -> bool: file_entries = os.scandir(dir_path) non_python_file_entries = filter(non_python_file, file_entries) - files = map(lambda entry: entry.path, non_python_file_entries) # noqa C417 + files = (entry.path for entry in non_python_file_entries) # Concat the file names with the directory to get relative to root path # then load the file as a fixture, getting the name and absolute path in the process - file_paths = map(lambda file: path.join(dir_path, file), files) # noqa C417 - fixture_name_paths = map( - lambda path: load_file_as_fixture(path, root_folder=dir_path), # noqa C417 - file_paths, - ) + file_paths = (path.join(dir_path, file) for file in files) + fixture_name_paths = (load_file_as_fixture(path, root_folder=dir_path) for path in file_paths) return list(fixture_name_paths) @@ -340,414 +334,3 @@ async def run_server(): raise e return intermediary, ls, time_out_err - - -###################### -# Pre-built payloads # -###################### - -# create fake uri for the MAL file being parsed -# (won't be used by the server, so there is no issue if the file does not actually exist) -FILE_PATH = str(Path(__file__).parent.resolve()) + "/fixtures/mal/" -main_simplified_file_path = FILE_PATH + "main.mal" -main_file_path = filepath_to_uri(main_simplified_file_path) -find_symbols_in_scope_path = filepath_to_uri(FILE_PATH + "find_symbols_in_scope.mal") - -BASE_OPEN_FILE = { - "jsonrpc": "2.0", - "method": "textDocument/didOpen", - "params": { - "textDocument": { - "uri": main_file_path, - "languageId": "mal", - "version": 0, - "text": '#id: "org.mal-lang.testAnalyzer"\n#version:"0.0.0"\n\ncategory ' - + "System {\nabstract asset Foo {}\nasset Bar extends Foo {}\n}\n\n", - } - }, -} - -OPEN_FILE_WITH_INCLUDED_FILE = { - "jsonrpc": "2.0", - "method": "textDocument/didOpen", - "params": { - "textDocument": { - "uri": main_file_path, - "languageId": "mal", - "version": 0, - "text": '#id: "org.mal-lang.testAnalyzer"\n#version:"0.0.0"\ - \ninclude "find_current_scope_function.mal"\ncategory System\ - {\nabstract asset Foo {}\nasset Bar extends Foo {}\n}\n\n', - } - }, -} - -OPEN_FILE_WITH_FAKE_INCLUDE = { - "jsonrpc": "2.0", - "method": "textDocument/didOpen", - "params": { - "textDocument": { - "uri": main_file_path, - "languageId": "mal", - "version": 0, - "text": '#id: "org.mal-lang.testAnalyzer"\n#version:"0.0.0"\ - \ninclude "random_file_that_does_not_exist.mal"\ncategory System\ - {\nabstract asset Foo {}\nasset Bar extends Foo {}\n}\n\n', - } - }, -} - -CHANGE_FILE_1 = { - "jsonrpc": "2.0", - "method": "textDocument/didChange", - "params": { - "textDocument": { - "uri": main_file_path, - "version": 1, - }, - "contentChanges": [ - { - "range": { - "start": {"line": 5, "character": 6}, - "end": {"line": 6, "character": 0}, - }, - "text": "FooFoo extends Foo {}\n", - } - ], - }, -} - -CHANGE_FILE_2 = { - "jsonrpc": "2.0", - "method": "textDocument/didChange", - "params": { - "textDocument": { - "uri": main_file_path, - "version": 1, - }, - "contentChanges": [ - { - "range": { - "start": {"line": 4, "character": 15}, - "end": {"line": 5, "character": 24}, - }, - "text": "Bar {}\nasset Foo extends Bar {}", - } - ], - }, -} - -CHANGE_FILE_3 = { - "jsonrpc": "2.0", - "method": "textDocument/didChange", - "params": { - "textDocument": { - "uri": main_file_path, - "version": 1, - }, - "contentChanges": [ - { - "range": { - "start": {"line": 7, "character": 0}, - "end": {"line": 9, "character": 0}, - }, - "text": "\nassociations {\n}\n", - } - ], - }, -} - -CHANGE_FILE_4 = { - "jsonrpc": "2.0", - "method": "textDocument/didChange", - "params": { - "textDocument": { - "uri": main_file_path, - "version": 1, - }, - "contentChanges": [ - { - "range": { - "start": {"line": 4, "character": 15}, - "end": {"line": 5, "character": 24}, - }, - "text": "Bar {}\nasset Foo extends Bar {}", - }, - { - "range": { - "start": {"line": 5, "character": 6}, - "end": {"line": 5, "character": 9}, - }, - "text": "Qux", - }, - ], - }, -} - -CHANGE_FILE_5 = { - "jsonrpc": "2.0", - "method": "textDocument/didChange", - "params": { - "textDocument": { - "uri": main_file_path, - "version": 1, - }, - "contentChanges": [ - { - "text": '#id: "a.b.c"\n', - } - ], - }, -} - - -def build_goto_definition_payload(text: str, uri: str, line: int, char: int, name: str): - open_message = { - "jsonrpc": "2.0", - "method": "textDocument/didOpen", - "params": { - "textDocument": { - "uri": main_file_path, - "languageId": "mal", - "version": 0, - "text": text, - } - }, - } - goto_message = { - "id": 1, - "jsonrpc": "2.0", - "method": "textDocument/definition", - "params": { - "textDocument": { - "uri": uri, - }, - "position": { - "line": line, - "character": char, - }, - }, - } - return ([open_message, goto_message], name) - - -mal_find_symbols_in_scope_points = [ - (3, 12, "goto_def_1"), # category_declaration - (11, 10, "goto_def_2"), # asset declaration, asset name - (7, 10, "goto_def_3"), # asset variable - (8, 10, "goto_def_4"), # attack step -] -mal_symbol_def_extended_asset_main_points = [ - (6, 25, "goto_def_5"), # asset declaration, extended asset - (9, 11, "goto_def_6"), # variable call -] -mal_symbol_def_variable_call_extend_chain_main_points = [ - (9, 11, "goto_def_7") # variable call, extend chain -] -symbol_def_variable_declaration_main_points = [ - (10, 20, "goto_def_8"), # variable declaration - (17, 21, "goto_def_9"), # variable declaration, extended asset - (21, 36, "goto_def_10"), # variable declaration complex 1 - (22, 36, "goto_def_11"), # variable declaration complex 2 - (26, 6, "goto_def_12"), # association asset name 1 - (27, 37, "goto_def_13"), # association asset name 2 - (28, 12, "goto_def_14"), # association field name 1 - (28, 32, "goto_def_15"), # association field name 2 - (30, 22, "goto_def_16"), # link name -] -mal_symbol_def_preconditions_points = [ - (11, 15, "goto_def_17"), # preconditions - (19, 13, "goto_def_18"), # preconditions extended asset - (24, 28, "goto_def_19"), # preconditions complex 1 - (26, 28, "goto_def_20"), # preconditions complex 1 -] -mal_symbol_def_reaches_points = [ - (13, 22, "goto_def_21"), # reaches - (14, 14, "goto_def_22"), # reaches single attack step - (10, 7, "goto_def_23"), # random non-user defined symbol -] -GOTO_DEFINITION_PAYLOADS = ( - [ - build_goto_definition_payload( - 'include "find_symbols_in_scope.mal"', find_symbols_in_scope_path, line, char, name - ) - for (line, char, name) in mal_find_symbols_in_scope_points - ] - + [ - build_goto_definition_payload( - 'include "symbol_def_extended_asset_main.mal"', - filepath_to_uri(FILE_PATH + "symbol_def_extended_asset_main.mal"), - line, - char, - name, - ) - for (line, char, name) in mal_symbol_def_extended_asset_main_points - ] - + [ - build_goto_definition_payload( - 'include "symbol_def_variable_call_extend_chain_main.mal"', - filepath_to_uri(FILE_PATH + "symbol_def_variable_call_extend_chain_main.mal"), - line, - char, - name, - ) - for (line, char, name) in mal_symbol_def_variable_call_extend_chain_main_points - ] - + [ - build_goto_definition_payload( - 'include "symbol_def_variable_declaration_main.mal"', - filepath_to_uri(FILE_PATH + "symbol_def_variable_declaration_main.mal"), - line, - char, - name, - ) - for (line, char, name) in symbol_def_variable_declaration_main_points - ] - + [ - build_goto_definition_payload( - 'include "symbol_def_preconditions.mal"', - filepath_to_uri(FILE_PATH + "symbol_def_preconditions.mal"), - line, - char, - name, - ) - for (line, char, name) in mal_symbol_def_preconditions_points - ] - + [ - build_goto_definition_payload( - 'include "symbol_def_reaches.mal"', - filepath_to_uri(FILE_PATH + "symbol_def_reaches.mal"), - line, - char, - name, - ) - for (line, char, name) in mal_symbol_def_reaches_points - ] -) - -OPEN_FILE_WITH_ERROR = { - "jsonrpc": "2.0", - "method": "textDocument/didOpen", - "params": { - "textDocument": { - "uri": main_file_path, - "languageId": "mal", - "version": 0, - "text": '#id: "org.mal-lang.testAnalyzer"\n#version:"0.0.0"\n\ncategory ' - + "System {\nabstract aet Foo {}\nasset Bar extends Foo {}\n}\n\n", - } - }, -} - -OPEN_FILE_WITH_INCLUDE_WITH_ERROR = { - "jsonrpc": "2.0", - "method": "textDocument/didOpen", - "params": { - "textDocument": { - "uri": main_file_path, - "languageId": "mal", - "version": 0, - "text": '#id: "org.mal-lang.testAnalyzer"\n#version:"0.0.0"\ - \ninclude "file_with_error.mal"', - } - }, -} - -file_with_error_path = filepath_to_uri(FILE_PATH + "file_with_error.mal") -OPEN_INCLUDED_FILE_WITH_ERROR = { - "jsonrpc": "2.0", - "method": "textDocument/didOpen", - "params": { - "textDocument": { - "uri": file_with_error_path, - "languageId": "mal", - "version": 0, - "text": "class Category {\n Asset1 {}\n }\n", - } - }, -} - -CHANGE_FILE_WITH_ERROR = { - "jsonrpc": "2.0", - "method": "textDocument/didChange", - "params": { - "textDocument": { - "uri": main_file_path, - "version": 1, - }, - "contentChanges": [ - { - "range": { - "start": {"line": 5, "character": 6}, - "end": {"line": 6, "character": 0}, - }, - "text": "FooFoo extds Foo {}\n", - } - ], - }, -} - - -def build_completion_payload(line, character, name): - open = { - "jsonrpc": "2.0", - "method": "textDocument/didOpen", - "params": { - "textDocument": { - "uri": find_symbols_in_scope_path, - "languageId": "mal", - "version": 0, - "text": """#id: "org.mal-lang.testAnalyzer" -#version:"0.0.0" - - category Example { - - abstract asset Asset1 - { - let var = c - | compromise - -> var.destroy - } - asset Asset2 extends Asset3 - { - | destroy - } - } - associations - { - Asset1 [a] * <-- L --> * [c] Asset2 - Asset2 [d] 1 <-- M --> 1 [e] Asset2 - } - """, - } - }, - } - - completion_list = { - "id": 1, - "jsonrpc": "2.0", - "method": "textDocument/completion", - "params": { - "textDocument": { - "uri": find_symbols_in_scope_path, - }, - "position": { - "line": line, - "character": character, - }, - }, - } - - return ([open, completion_list], name) - - -completion_items = [ - (4, 0, "completion_category"), - (18, 0, "completion_associations"), - (7, 0, "completion_asset1"), - (13, 0, "completion_asset2"), - (0, 0, "completion_root_node"), -] -COMPLETION_PAYLOADS = [ - build_completion_payload(line, char, name) for line, char, name in completion_items -] From ff6d4856a1fe6f363ef5076194b2b45bb6d7cc39 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 23:03:17 +0200 Subject: [PATCH 40/42] chore: Clean up imports and fixture locations --- tests/fixtures/lsp/goto_definition.py | 79 +++++++++++++++++++ tests/integration/test_goto_definition.py | 76 +----------------- tests/integration/test_publish_diagnostics.py | 6 -- 3 files changed, 83 insertions(+), 78 deletions(-) create mode 100644 tests/fixtures/lsp/goto_definition.py diff --git a/tests/fixtures/lsp/goto_definition.py b/tests/fixtures/lsp/goto_definition.py new file mode 100644 index 0000000..e7b974d --- /dev/null +++ b/tests/fixtures/lsp/goto_definition.py @@ -0,0 +1,79 @@ +import typing + +import pytest + +from ...util import build_rpc_message_stream + +# So that importers are aware of the conftest fixtures +pytest_plugins = ["tests.fixtures.lsp.conftest"] + + +type FixtureCallback[T] = typing.Callable[[str, typing.BinaryIO, (int, int)], T] + + +@pytest.fixture +def open_document_notification( + client_notifications: list[dict], client_messages: list[dict] +) -> FixtureCallback[dict]: + def make(uri: str, file: typing.BinaryIO, _location) -> dict: + message = { + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": uri, + "languageId": "mal", + "version": 0, + "text": file.read().decode("utf8"), + } + }, + } + client_notifications.append(message) + client_messages.append(message) + return message + + return make + + +@pytest.fixture +def definition_request( + client_requests: list[dict], client_messages: list[dict] +) -> FixtureCallback[dict]: + def make(uri: str, _file, location: (int, int)) -> dict: + line, char = location + message = { + "id": len(client_requests), + "jsonrpc": "2.0", + "method": "textDocument/definition", + "params": { + "textDocument": { + "uri": uri, + }, + "position": { + "line": line, + "character": char, + }, + }, + } + client_requests.append(message) + client_messages.append(message) + return message + + return make + + +@pytest.fixture +def goto_definition_client_messages( + client_messages: list[dict], + initalize_request, + initalized_notification, + open_document_notification: FixtureCallback[dict], + definition_request: FixtureCallback[dict], +) -> FixtureCallback[typing.BinaryIO]: + def make(uri: str, file: typing.BinaryIO, location: (int, int)) -> typing.BinaryIO: + args = (uri, file, location) + open_document_notification(*args) + definition_request(*args) + return build_rpc_message_stream(client_messages) + + return make diff --git a/tests/integration/test_goto_definition.py b/tests/integration/test_goto_definition.py index b033f13..a0971e8 100644 --- a/tests/integration/test_goto_definition.py +++ b/tests/integration/test_goto_definition.py @@ -3,7 +3,10 @@ import pytest -from ..util import build_rpc_message_stream, get_lsp_json, server_output +from ..fixtures.lsp.goto_definition import FixtureCallback +from ..util import get_lsp_json, server_output + +pytest_plugins = ["tests.fixtures.lsp.goto_definition"] # Test parameters mal_find_symbols_in_scope_points = zip( @@ -104,77 +107,6 @@ zip(goto_input_location_and_files, goto_expected_location_and_files) ) -# Fixtures -type FixtureCallback[T] = typing.Callable[[str, typing.BinaryIO, (int, int)], T] - - -@pytest.fixture -def open_document_notification( - client_notifications: list[dict], client_messages: list[dict] -) -> FixtureCallback[dict]: - def make(uri: str, file: typing.BinaryIO, _location) -> dict: - message = { - "jsonrpc": "2.0", - "method": "textDocument/didOpen", - "params": { - "textDocument": { - "uri": uri, - "languageId": "mal", - "version": 0, - "text": file.read().decode("utf8"), - } - }, - } - client_notifications.append(message) - client_messages.append(message) - return message - - return make - - -@pytest.fixture -def definition_request( - client_requests: list[dict], client_messages: list[dict] -) -> FixtureCallback[dict]: - def make(uri: str, _file, location: (int, int)) -> dict: - line, char = location - message = { - "id": len(client_requests), - "jsonrpc": "2.0", - "method": "textDocument/definition", - "params": { - "textDocument": { - "uri": uri, - }, - "position": { - "line": line, - "character": char, - }, - }, - } - client_requests.append(message) - client_messages.append(message) - return message - - return make - - -@pytest.fixture -def goto_definition_client_messages( - client_messages: list[dict], - initalize_request, - initalized_notification, - open_document_notification: FixtureCallback[dict], - definition_request: FixtureCallback[dict], -) -> FixtureCallback[typing.BinaryIO]: - def make(uri: str, file: typing.BinaryIO, location: (int, int)) -> typing.BinaryIO: - args = (uri, file, location) - open_document_notification(*args) - definition_request(*args) - return build_rpc_message_stream(client_messages) - - return make - def parameter_id(argvalue: (((int, int), str), ((int, int), str))) -> object: # Turns the combination of test parameters of find_symbols_in_scope_expected_results diff --git a/tests/integration/test_publish_diagnostics.py b/tests/integration/test_publish_diagnostics.py index 383161e..b691d96 100644 --- a/tests/integration/test_publish_diagnostics.py +++ b/tests/integration/test_publish_diagnostics.py @@ -1,15 +1,9 @@ import typing -from pathlib import Path from malls.lsp.enums import DiagnosticSeverity from ..util import get_lsp_json, server_output -# calculate file path of mal files -FILE_PATH = str(Path(__file__).parent.parent.resolve()) + "/fixtures/mal/" -simplified_file_path = FILE_PATH + "main.mal" -included_file_path = FILE_PATH + "file_with_error.mal" - pytest_plugins = ["tests.fixtures.lsp.publish_diagnostics"] From cc883cbe87f156ef858282424e4731a80d866dd6 Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Wed, 27 Aug 2025 23:52:32 +0200 Subject: [PATCH 41/42] test: Use LSP fixture components for test_trace.py --- tests/fixtures/log_trace_messages.in.lsp | 15 --- tests/fixtures/log_trace_off.in.lsp | 12 --- tests/fixtures/log_trace_verbose.in.lsp | 15 --- tests/fixtures/lsp/trace.py | 93 +++++++++++++++++++ tests/fixtures/set_trace_value.in.lsp | 15 --- tests/fixtures/set_wrong_trace_value.in.lsp | 15 --- .../did_change_notif.in.lsp | 6 -- .../writeable_fixtures/did_open_notif.in.lsp | 6 -- tests/fixtures/wrong_trace_value.in.lsp | 12 --- tests/integration/test_trace.py | 26 +++--- 10 files changed, 107 insertions(+), 108 deletions(-) delete mode 100644 tests/fixtures/log_trace_messages.in.lsp delete mode 100644 tests/fixtures/log_trace_off.in.lsp delete mode 100644 tests/fixtures/log_trace_verbose.in.lsp create mode 100644 tests/fixtures/lsp/trace.py delete mode 100644 tests/fixtures/set_trace_value.in.lsp delete mode 100644 tests/fixtures/set_wrong_trace_value.in.lsp delete mode 100644 tests/fixtures/writeable_fixtures/did_change_notif.in.lsp delete mode 100644 tests/fixtures/writeable_fixtures/did_open_notif.in.lsp delete mode 100644 tests/fixtures/wrong_trace_value.in.lsp diff --git a/tests/fixtures/log_trace_messages.in.lsp b/tests/fixtures/log_trace_messages.in.lsp deleted file mode 100644 index c26f9d3..0000000 --- a/tests/fixtures/log_trace_messages.in.lsp +++ /dev/null @@ -1,15 +0,0 @@ -Content-Length: 72 - -{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"trace":"off"}} -Content-Length: 41 - -{"jsonrpc":"2.0","method":"initialized"} -Content-Length: 71 - -{"jsonrpc": "2.0","method":"$/setTrace","params":{"value":"messages"}} -Content-Length: 45 - -{"jsonrpc":"2.0","id":2,"method":"shutdown"} -Content-Length: 34 - -{"jsonrpc":"2.0","method":"exit"} diff --git a/tests/fixtures/log_trace_off.in.lsp b/tests/fixtures/log_trace_off.in.lsp deleted file mode 100644 index 4858837..0000000 --- a/tests/fixtures/log_trace_off.in.lsp +++ /dev/null @@ -1,12 +0,0 @@ -Content-Length: 72 - -{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"trace":"off"}} -Content-Length: 41 - -{"jsonrpc":"2.0","method":"initialized"} -Content-Length: 45 - -{"jsonrpc":"2.0","id":2,"method":"shutdown"} -Content-Length: 34 - -{"jsonrpc":"2.0","method":"exit"} diff --git a/tests/fixtures/log_trace_verbose.in.lsp b/tests/fixtures/log_trace_verbose.in.lsp deleted file mode 100644 index cc7eefb..0000000 --- a/tests/fixtures/log_trace_verbose.in.lsp +++ /dev/null @@ -1,15 +0,0 @@ -Content-Length: 72 - -{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"trace":"off"}} -Content-Length: 41 - -{"jsonrpc":"2.0","method":"initialized"} -Content-Length: 70 - -{"jsonrpc": "2.0","method":"$/setTrace","params":{"value":"verbose"}} -Content-Length: 45 - -{"jsonrpc":"2.0","id":2,"method":"shutdown"} -Content-Length: 34 - -{"jsonrpc":"2.0","method":"exit"} diff --git a/tests/fixtures/lsp/trace.py b/tests/fixtures/lsp/trace.py new file mode 100644 index 0000000..8a35e34 --- /dev/null +++ b/tests/fixtures/lsp/trace.py @@ -0,0 +1,93 @@ +import typing + +import pytest + +pytest_plugins = ["tests.fixtures.lsp.conftest"] + + +@pytest.fixture +def wrong_trace_initalize_requests(client_requests: list[dict], initalize_request) -> dict: + client_requests[-1]["params"]["trace"] = "odf" + + +@pytest.fixture +def wrong_trace_value_client_messages( + wrong_trace_initalize_requests, + initalized_notification, + shutdown_request, + exit_notification, + client_rpc_messages: typing.BinaryIO, +) -> typing.BinaryIO: + return client_rpc_messages + + +@pytest.fixture +def set_trace_notification(client_notifications: list[dict], client_messages: list[dict]) -> dict: + """ + Sends a $/setTrace notification from the client to the server. + `value` must be set in params. + """ + message = {"jsonrpc": "2.0", "method": "$/setTrace", "params": {}} + client_notifications.append(message) + client_messages.append(message) + return message + + +@pytest.fixture +def set_trace_verbose_notification(client_notifications: list[dict], set_trace_notification): + client_notifications[-1]["params"]["value"] = "verbose" + + +@pytest.fixture +def set_trace_verbose_client_messages( + client_initalize_procedures, + set_trace_verbose_notification, + client_shutdown_procedures, + client_rpc_messages: typing.BinaryIO, +) -> typing.BinaryIO: + return client_rpc_messages + + +@pytest.fixture +def set_trace_messages_notification(client_notifications: list[dict], set_trace_notification): + client_notifications[-1]["params"]["value"] = "messages" + + +@pytest.fixture +def set_trace_messages_client_messages( + client_initalize_procedures, + set_trace_messages_notification, + client_shutdown_procedures, + client_rpc_messages: typing.BinaryIO, +) -> typing.BinaryIO: + return client_rpc_messages + + +@pytest.fixture +def set_trace_off_notification(client_notifications: list[dict], set_trace_notification): + client_notifications[-1]["params"]["value"] = "off" + + +@pytest.fixture +def set_trace_off_client_messages( + client_initalize_procedures, + set_trace_off_notification, + client_shutdown_procedures, + client_rpc_messages: typing.BinaryIO, +) -> typing.BinaryIO: + return client_rpc_messages + + +@pytest.fixture +def set_trace_wrong_notification(client_notifications: list[dict], set_trace_notification): + client_notifications[-1]["params"]["value"] = "vxrbxsx" + + +@pytest.fixture +def set_trace_wrong_client_messages( + client_initalize_procedures, + set_trace_wrong_notification, + client_shutdown_procedures, + client_rpc_messages: typing.BinaryIO, +) -> typing.BinaryIO: + return client_rpc_messages diff --git a/tests/fixtures/set_trace_value.in.lsp b/tests/fixtures/set_trace_value.in.lsp deleted file mode 100644 index cc7eefb..0000000 --- a/tests/fixtures/set_trace_value.in.lsp +++ /dev/null @@ -1,15 +0,0 @@ -Content-Length: 72 - -{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"trace":"off"}} -Content-Length: 41 - -{"jsonrpc":"2.0","method":"initialized"} -Content-Length: 70 - -{"jsonrpc": "2.0","method":"$/setTrace","params":{"value":"verbose"}} -Content-Length: 45 - -{"jsonrpc":"2.0","id":2,"method":"shutdown"} -Content-Length: 34 - -{"jsonrpc":"2.0","method":"exit"} diff --git a/tests/fixtures/set_wrong_trace_value.in.lsp b/tests/fixtures/set_wrong_trace_value.in.lsp deleted file mode 100644 index 2c2d8cf..0000000 --- a/tests/fixtures/set_wrong_trace_value.in.lsp +++ /dev/null @@ -1,15 +0,0 @@ -Content-Length: 72 - -{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"trace":"off"}} -Content-Length: 41 - -{"jsonrpc":"2.0","method":"initialized"} -Content-Length: 70 - -{"jsonrpc": "2.0","method":"$/setTrace","params":{"value":"vxrbxsx"}} -Content-Length: 45 - -{"jsonrpc":"2.0","id":2,"method":"shutdown"} -Content-Length: 34 - -{"jsonrpc":"2.0","method":"exit"} diff --git a/tests/fixtures/writeable_fixtures/did_change_notif.in.lsp b/tests/fixtures/writeable_fixtures/did_change_notif.in.lsp deleted file mode 100644 index 10dcfe4..0000000 --- a/tests/fixtures/writeable_fixtures/did_change_notif.in.lsp +++ /dev/null @@ -1,6 +0,0 @@ -Content-Length: 197 - -{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{"textDocument":{"definition":{"dynamicRegistration":false},"synchronization":{"dynamicRegistration":false}}},"trace":"off"}} -Content-Length: 41 - -{"jsonrpc":"2.0","method":"initialized"} diff --git a/tests/fixtures/writeable_fixtures/did_open_notif.in.lsp b/tests/fixtures/writeable_fixtures/did_open_notif.in.lsp deleted file mode 100644 index 10dcfe4..0000000 --- a/tests/fixtures/writeable_fixtures/did_open_notif.in.lsp +++ /dev/null @@ -1,6 +0,0 @@ -Content-Length: 197 - -{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{"textDocument":{"definition":{"dynamicRegistration":false},"synchronization":{"dynamicRegistration":false}}},"trace":"off"}} -Content-Length: 41 - -{"jsonrpc":"2.0","method":"initialized"} diff --git a/tests/fixtures/wrong_trace_value.in.lsp b/tests/fixtures/wrong_trace_value.in.lsp deleted file mode 100644 index 977a922..0000000 --- a/tests/fixtures/wrong_trace_value.in.lsp +++ /dev/null @@ -1,12 +0,0 @@ -Content-Length: 72 - -{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"trace":"odf"}} -Content-Length: 41 - -{"jsonrpc":"2.0","method":"initialized"} -Content-Length: 45 - -{"jsonrpc":"2.0","id":2,"method":"shutdown"} -Content-Length: 34 - -{"jsonrpc":"2.0","method":"exit"} diff --git a/tests/integration/test_trace.py b/tests/integration/test_trace.py index 08158e0..c83fcd7 100644 --- a/tests/integration/test_trace.py +++ b/tests/integration/test_trace.py @@ -4,9 +4,11 @@ from ..util import get_lsp_json, server_output +pytest_plugins = ["tests.fixtures.lsp.trace"] -def test_wrong_trace_value_in_initialization(wrong_trace_value_in: typing.BinaryIO): - output, ls, *_ = server_output(wrong_trace_value_in) + +def test_wrong_trace_value_in_initialization(wrong_trace_value_client_messages: typing.BinaryIO): + output, ls, *_ = server_output(wrong_trace_value_client_messages) # the test and server share the same buffer, # so we must reset the cursor @@ -23,8 +25,8 @@ def test_wrong_trace_value_in_initialization(wrong_trace_value_in: typing.Binary output.close() -def test_set_trace_correctly(set_trace_value_in: typing.BinaryIO): - output, ls, *_ = server_output(set_trace_value_in) +def test_set_trace_correctly(set_trace_verbose_client_messages: typing.BinaryIO): + output, ls, *_ = server_output(set_trace_verbose_client_messages) # ensure ls has trace value correctly set assert ls.trace_value == TraceValue.Verbose @@ -32,8 +34,8 @@ def test_set_trace_correctly(set_trace_value_in: typing.BinaryIO): output.close() -def test_set_trace_incorrectly(set_wrong_trace_value_in: typing.BinaryIO): - output, ls, *_ = server_output(set_wrong_trace_value_in) +def test_set_trace_incorrectly(set_trace_wrong_client_messages: typing.BinaryIO): + output, ls, *_ = server_output(set_trace_wrong_client_messages) # ensure ls has trace value correctly set assert ls.trace_value == TraceValue.Off @@ -41,8 +43,8 @@ def test_set_trace_incorrectly(set_wrong_trace_value_in: typing.BinaryIO): output.close() -def test_log_trace_messages(log_trace_messages_in: typing.BinaryIO): - output, ls, *_ = server_output(log_trace_messages_in) +def test_log_trace_messages(set_trace_messages_client_messages: typing.BinaryIO): + output, ls, *_ = server_output(set_trace_messages_client_messages) # the test and server share the same buffer, # so we must reset the cursor @@ -62,8 +64,8 @@ def test_log_trace_messages(log_trace_messages_in: typing.BinaryIO): output.close() -def test_log_trace_verbose(log_trace_verbose_in: typing.BinaryIO): - output, ls, *_ = server_output(log_trace_verbose_in) +def test_log_trace_verbose(set_trace_verbose_client_messages: typing.BinaryIO): + output, ls, *_ = server_output(set_trace_verbose_client_messages) # the test and server share the same buffer, # so we must reset the cursor @@ -83,8 +85,8 @@ def test_log_trace_verbose(log_trace_verbose_in: typing.BinaryIO): output.close() -def test_log_trace_off(log_trace_off_in: typing.BinaryIO): - output, ls, *_ = server_output(log_trace_off_in) +def test_log_trace_off(set_trace_off_client_messages: typing.BinaryIO): + output, ls, *_ = server_output(set_trace_off_client_messages) # the test and server share the same buffer, # so we must reset the cursor From 02f371a43216b968b1ba9d39a5439fefd8cb2c4e Mon Sep 17 00:00:00 2001 From: Tobiky <12644606+Tobiky@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:20:40 +0200 Subject: [PATCH 42/42] docs: Explain structure, purpose, and systems of tests --- docs/TESTS.md | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 docs/TESTS.md diff --git a/docs/TESTS.md b/docs/TESTS.md new file mode 100644 index 0000000..c1d5e8e --- /dev/null +++ b/docs/TESTS.md @@ -0,0 +1,99 @@ +# Tests + +The tests are split up into three sections; fixtures, integration tests, and unit tests. Fixtures +are then split further, into LSP fixtures and MAL fixtures. + +## Integration tests + +These tests are gennerally tests that take some kind of recorded or constructed LSP message stream, +input it into the server, and inspects the output or the state of the server. + +Many of these tests are paratremized with fixture names and have the fixtures dynamically requested +since the underlying testing logic is the same (such as files or message streams). This also makes +it more maintainable and overall easier to understand, though it may be a bit more confusing +connecting the parameters and their purpose. + +## Unit tests + +Unit tests are dedicated to testing small units, therefore the name, and is no different here. +Things like individual algorithms and helper functions are tested here. + +## LSP Fixtures + +The LSP fixtures have a component system to them. Since the LSP is built on JSON RPC, most of these +fixtures are an individual JSON RPC message from client to server or vice versa. For example: + +`tests/fixtures/lsp/conftest.py` +```py +@pytest.fixture +def initalize_request(client_requests: list[dict], client_messages: list[dict]) -> dict: + """ + Defines an `initalize` LSP request from client to server. + """ + message = { + "jsonrpc": "2.0", + "id": len(client_requests), + "method": "initialize", + "params": { + "capabilities": { + "textDocument": { + "definition": {"dynamicRegistration": False}, + "synchronization": {"dynamicRegistration": False}, + } + }, + "trace": "off", + }, + } + client_requests.append(message) + client_messages.append(message) + return message +``` + +The most common of these are available in the shared `conftest.py` of the LSP fixture folder. +However, sometimes these fixtures need to be specialized or edited, which can be done simply +(and by recommendation) by requesting the fixture and editing the last message: + +`tests/fixtures/lsp/trace.py` +```py +@pytest.fixture +def set_trace_notification(client_notifications: list[dict], client_messages: list[dict]) -> dict: + """ + Sends a $/setTrace notification from the client to the server. + `value` must be set in params. + """ + message = {"jsonrpc": "2.0", "method": "$/setTrace", "params": {}} + client_notifications.append(message) + client_messages.append(message) + return message + + +@pytest.fixture +def set_trace_verbose_notification(client_notifications: list[dict], set_trace_notification): + client_notifications[-1]["params"]["value"] = "verbose" +``` + +They are then built together by simply requesting the fixtures in order and the specialized +`client_rpc_messages` that builds them into an actual JSON RPC message stream: + +`tests/fixtures/lsp/trace.py` +```py +@pytest.fixture +def set_trace_wrong_client_messages( + client_initalize_procedures, + set_trace_wrong_notification, + client_shutdown_procedures, + client_rpc_messages: typing.BinaryIO, +) -> typing.BinaryIO: + return client_rpc_messages +``` + +## MAL Fixtures + +The MAL files located in `tests/fixtures/mal/` are automatically loaded by the root `conftest.py`. +To request one of the, simply specifiy the file name without the extension and the prefix `mal_`. +For example, for a file `tests/fixtures/mal/hello_world.mal`, it can be requested as +`mal_hello_world`. The files are opened as read-only binary, so decoding will have to be done as +extra setup by tests/fixtures if necessary. These should never be changed to be writable since +fixture data should never be edited. If anything needs to be added, create a new fixture that +either creates a temporary file that it modifies and hands over or load the file into memory and +modify the memory.