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. diff --git a/src/malls/lsp/models.py b/src/malls/lsp/models.py index 1ae03bd..5f49606 100644 --- a/src/malls/lsp/models.py +++ b/src/malls/lsp/models.py @@ -2625,20 +2625,22 @@ 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): """ https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#didChangeTextDocumentParams 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 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/conftest.py b/tests/conftest.py index 8659657..76aa800 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,35 +2,22 @@ import os import sys import typing -from io import BytesIO +from pathlib import Path import pytest - -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, - 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, -) +import tree_sitter_mal as ts_mal +from tree_sitter import Language, Parser 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 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 @@ -47,64 +34,66 @@ # 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 = path.resolve() + uri = str(file_path.as_uri()) + + 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) + +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/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/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/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/base_protocol.py b/tests/fixtures/lsp/base_protocol.py new file mode 100644 index 0000000..4160832 --- /dev/null +++ b/tests/fixtures/lsp/base_protocol.py @@ -0,0 +1,95 @@ +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/lsp/conftest.py b/tests/fixtures/lsp/conftest.py new file mode 100644 index 0000000..26468c7 --- /dev/null +++ b/tests/fixtures/lsp/conftest.py @@ -0,0 +1,281 @@ +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": { + "capabilities": { + "textDocument": { + "definition": {"dynamicRegistration": False}, + "synchronization": {"dynamicRegistration": False}, + } + }, + "trace": "off", + }, + } + 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()) # 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()) # noqa: E501 + "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 + + +@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 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_change.py b/tests/fixtures/lsp/did_change.py new file mode 100644 index 0000000..d5064de --- /dev/null +++ b/tests/fixtures/lsp/did_change.py @@ -0,0 +1,148 @@ +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 new file mode 100644 index 0000000..04f4aa8 --- /dev/null +++ b/tests/fixtures/lsp/did_open_text_document_notification.py @@ -0,0 +1,81 @@ +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/encoding_capability_check.py b/tests/fixtures/lsp/encoding_capability_check.py new file mode 100644 index 0000000..bac777b --- /dev/null +++ b/tests/fixtures/lsp/encoding_capability_check.py @@ -0,0 +1,35 @@ +import io + +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: + 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: + """ + client server + -------------------------- + initialize + initalized + $/setTrace + shutdown + exit + """ + return client_rpc_messages 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/fixtures/lsp/publish_diagnostics.py b/tests/fixtures/lsp/publish_diagnostics.py new file mode 100644 index 0000000..ce8bb3b --- /dev/null +++ b/tests/fixtures/lsp/publish_diagnostics.py @@ -0,0 +1,117 @@ +import typing + +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.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": 5, "character": 10}, + "end": {"line": 6, "character": 0}, + }, + "text": "FooFoo extds Foo {}\n", + } + ], + } + + +@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/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/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/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/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/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/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/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 } 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/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_base_protocol.py b/tests/integration/test_base_protocol.py index 7c6b3ba..f182417 100644 --- a/tests/integration/test_base_protocol.py +++ b/tests/integration/test_base_protocol.py @@ -1,84 +1,40 @@ -import asyncio -import io -import logging import typing 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__) +pytest_plugins = ["tests.fixtures.lsp.base_protocol"] -# wait for most 5s (arbitrary) -MAX_TIMEOUT = 2 +def test_correct_base_lifecycle( + init_exit_client_messages: typing.BinaryIO, init_exit_server_messages: typing.BinaryIO +): + output, *_ = server_output(init_exit_client_messages) -class SteppedBytesIO(io.BytesIO): - """ - SteppedBytesIO provide a way to stop the closing of the IO N-1 times, closing on the Nth time. - """ - - 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, + init_shutdown_client_messages: typing.BinaryIO, ): - output, ls, *_ = server_output(pre_initialized_shutdown_in) + 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 diff --git a/tests/integration/test_completion.py b/tests/integration/test_completion.py index 068f83d..65d7297 100644 --- a/tests/integration/test_completion.py +++ b/tests/integration/test_completion.py @@ -1,13 +1,15 @@ -import logging +import io +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 + +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/" @@ -53,30 +55,107 @@ ] 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], -) -def test_completion(request, fixture_name, completion_list): +@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], +): # 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) diff --git a/tests/integration/test_diagnostics_are_saved.py b/tests/integration/test_diagnostics_are_saved.py index 8071dab..5bfc327 100644 --- a/tests/integration/test_diagnostics_are_saved.py +++ b/tests/integration/test_diagnostics_are_saved.py @@ -1,56 +1,45 @@ -import logging -import typing -from pathlib import Path +import pytest from malls.lsp.enums import DiagnosticSeverity 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" -included_file_path = FILE_PATH + "file_with_error.mal" - - -def test_open_file_with_error( - open_file_with_error: typing.BinaryIO, +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, ): - # 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 + input = request.getfixturevalue(messages_fixture_name) + uri = request.getfixturevalue(uri_fixture_name) - output.close() + # since Document acts inconsistent with URIs + uri = uri[len("file://") :] - -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) + 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() diff --git a/tests/integration/test_did_change_text_document_notification.py b/tests/integration/test_did_change_text_document_notification.py index 7316aba..62e2383 100644 --- a/tests/integration/test_did_change_text_document_notification.py +++ b/tests/integration/test_did_change_text_document_notification.py @@ -1,161 +1,48 @@ -import json -import logging 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 -log = logging.getLogger(__name__) -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, +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, ): - 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() - + client_messages: typing.BinaryIO = request.getfixturevalue(client_messages_fixture_name) + expected_file: typing.BinaryIO = request.getfixturevalue(expected_file_fixture_name) -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 + 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/integration/test_did_open_text_document_notification.py b/tests/integration/test_did_open_text_document_notification.py index b4f10d0..88f64d9 100644 --- a/tests/integration/test_did_open_text_document_notification.py +++ b/tests/integration/test_did_open_text_document_notification.py @@ -1,70 +1,27 @@ -import logging import typing -from pathlib import Path +import pytest from tree_sitter import Tree from ..util import server_output -log = logging.getLogger(__name__) +pytest_plugins = ["tests.fixtures.lsp.did_open_text_document_notification"] -# calculate file path of mal files -FILE_PATH = str(Path(__file__).parent.parent.resolve()) + "/fixtures/mal/" -simplified_file_path = FILE_PATH + "main.mal" +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() diff --git a/tests/integration/test_encoding_capability.py b/tests/integration/test_encoding_capability.py index 0c5f236..cb6778c 100644 --- a/tests/integration/test_encoding_capability.py +++ b/tests/integration/test_encoding_capability.py @@ -4,9 +4,12 @@ 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 diff --git a/tests/integration/test_find_current_scope.py b/tests/integration/test_find_current_scope.py deleted file mode 100644 index 7c15ca5..0000000 --- a/tests/integration/test_find_current_scope.py +++ /dev/null @@ -1,95 +0,0 @@ -import tree_sitter_mal as ts_mal -from tree_sitter import Language, Parser - -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" - - -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" diff --git a/tests/integration/test_find_symbol_definition_function.py b/tests/integration/test_find_symbol_definition_function.py deleted file mode 100644 index b33f4d2..0000000 --- a/tests/integration/test_find_symbol_definition_function.py +++ /dev/null @@ -1,158 +0,0 @@ -import logging -from pathlib import Path - -import pytest -import tree_sitter_mal as ts_mal -from tree_sitter import Language, Parser - -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 - -log = logging.getLogger(__name__) -MAL_LANGUAGE = Language(ts_mal.language()) -PARSER = Parser(MAL_LANGUAGE) -FILE_PATH = str(Path(__file__).parent.parent.resolve()) + "/fixtures/mal/" - - -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 -] - - -@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()) - - # go to name of category - - # get the node - cursor = tree.walk() - while cursor.goto_first_child_for_point(point) is not None: - continue - - # confirm it's an identifier - assert cursor.node.type == "identifier" - - response = find_symbol_definition(cursor.node, cursor.node.text) - - # ensure position is start of category declaration - 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 -] - - -@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 - ], -) -def test_symbol_definition_with_storage( - request, - file_name, - fixture_name, - point, - expected_result, -): - # build the storage (mimicks the file parsing in the server) - storage = {} - - doc_uri = FILE_PATH + file_name - file = request.getfixturevalue(fixture_name) - source_encoded = file.read() - tree = PARSER.parse(source_encoded) - - storage[doc_uri] = Document(tree, source_encoded, doc_uri) - - # obtain the included files - root_node = tree.root_node - - captures = run_query(root_node, INCLUDED_FILES_QUERY) - if "file_name" in captures: - recursive_parsing(FILE_PATH, captures["file_name"], storage, doc_uri, []) - - ################################### - - # get the node - cursor = tree.walk() - while cursor.goto_first_child_for_point(point) is not None: - continue - - # confirm it's an identifier - assert cursor.node.type == "identifier" - - # we use sets to ensure order does not matter - response = find_symbol_definition(cursor.node, cursor.node.text, doc_uri, storage) - - assert response[0].start_point == expected_result diff --git a/tests/integration/test_find_symbols_in_context_hierarchy.py b/tests/integration/test_find_symbols_in_context_hierarchy.py deleted file mode 100644 index 6b0740a..0000000 --- a/tests/integration/test_find_symbols_in_context_hierarchy.py +++ /dev/null @@ -1,207 +0,0 @@ -import tree_sitter_mal as ts_mal -from tree_sitter import Language, Parser - -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) - - # 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 diff --git a/tests/integration/test_find_symbols_in_scope.py b/tests/integration/test_find_symbols_in_scope.py deleted file mode 100644 index 8240380..0000000 --- a/tests/integration/test_find_symbols_in_scope.py +++ /dev/null @@ -1,118 +0,0 @@ -import tree_sitter_mal as ts_mal -from tree_sitter import Language, Parser - -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"] - - # 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) diff --git a/tests/integration/test_goto_definition.py b/tests/integration/test_goto_definition.py index 477081a..a0971e8 100644 --- a/tests/integration/test_goto_definition.py +++ b/tests/integration/test_goto_definition.py @@ -1,67 +1,147 @@ -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 ..fixtures.lsp.goto_definition import FixtureCallback 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/" +pytest_plugins = ["tests.fixtures.lsp.goto_definition"] -simplified_file_path = FILE_PATH + "main.mal" +# 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"), + ) +) -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"), +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) +) + +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 +153,30 @@ 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) diff --git a/tests/integration/test_position_conversion.py b/tests/integration/test_position_conversion.py deleted file mode 100644 index d8be06f..0000000 --- a/tests/integration/test_position_conversion.py +++ /dev/null @@ -1,58 +0,0 @@ -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) - - result_position = lsp_to_tree_sitter_position(text, position) - assert result_position == (0, 0) - assert tree_sitter_to_lsp_position(text, result_position) == position diff --git a/tests/integration/test_publish_diagnostics.py b/tests/integration/test_publish_diagnostics.py index afdae2a..b691d96 100644 --- a/tests/integration/test_publish_diagnostics.py +++ b/tests/integration/test_publish_diagnostics.py @@ -1,24 +1,15 @@ -import logging import typing -from pathlib import Path from malls.lsp.enums import DiagnosticSeverity from ..util import get_lsp_json, server_output -log = logging.getLogger(__name__) +pytest_plugins = ["tests.fixtures.lsp.publish_diagnostics"] -# 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_diagnostics_when_opening_file_with_error( - open_file_with_error: typing.BinaryIO, -): +def test_diagnostics_when_opening_file_with_error(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 +22,17 @@ 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 +48,16 @@ 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 @@ -80,23 +70,22 @@ def test_diagnostics_when_opening_file_with_include_error_and_opening_bad_file( 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 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() diff --git a/tests/integration/test_trace.py b/tests/integration/test_trace.py index 6015f17..c83fcd7 100644 --- a/tests/integration/test_trace.py +++ b/tests/integration/test_trace.py @@ -1,15 +1,14 @@ -import logging import typing from malls.lsp.enums import ErrorCodes, TraceValue from ..util import get_lsp_json, server_output -log = logging.getLogger(__name__) +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 @@ -26,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 @@ -35,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 @@ -44,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 @@ -65,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 @@ -86,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 diff --git a/tests/integration/test_visit_expr.py b/tests/integration/test_visit_expr.py deleted file mode 100644 index 28b6eb4..0000000 --- a/tests/integration/test_visit_expr.py +++ /dev/null @@ -1,178 +0,0 @@ -import logging - -import tree_sitter_mal as ts_mal -from tree_sitter import Language, Parser - -from malls.ts.utils import visit_expr - -log = logging.getLogger(__name__) -MAL_LANGUAGE = Language(ts_mal.language()) -PARSER = Parser(MAL_LANGUAGE) - - -def test_visit_expr_only_collects(mal_visit_expr): - tree = PARSER.parse(mal_visit_expr.read()) - cursor = tree.walk() - - point = (9, 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_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 == [ - 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", - ] - - -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() - 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"] diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..26f348e --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,35 @@ +""" +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/unit/test_find_current_scope.py b/tests/unit/test_find_current_scope.py new file mode 100644 index 0000000..2c3c800 --- /dev/null +++ b/tests/unit/test_find_current_scope.py @@ -0,0 +1,55 @@ +import typing + +import pytest +from tree_sitter import Parser, Tree, TreeCursor + +from malls.ts.utils import find_current_scope + + +@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 diff --git a/tests/unit/test_find_meta_comment_function.py b/tests/unit/test_find_meta_comment_function.py index 839b69b..a437945 100644 --- a/tests/unit/test_find_meta_comment_function.py +++ b/tests/unit/test_find_meta_comment_function.py @@ -1,43 +1,55 @@ -from pathlib import Path +import typing 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 +58,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 +71,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 diff --git a/tests/unit/test_find_symbol_definition_function.py b/tests/unit/test_find_symbol_definition_function.py new file mode 100644 index 0000000..44e3879 --- /dev/null +++ b/tests/unit/test_find_symbol_definition_function.py @@ -0,0 +1,141 @@ +from itertools import chain, repeat + +import pytest +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 + +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) +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 = find_symbols_in_scope_cursor + while cursor.goto_first_child_for_point(point) is not None: + continue + + # confirm it's an identifier + assert cursor.node.type == "identifier" + + response = find_symbol_definition(cursor.node, cursor.node.text) + + # ensure position is start of category declaration + assert response[0].start_point == expected_result + + +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("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), +): + # build the storage (mimicks the file parsing in the server) + storage = {} + + doc_uri = request.getfixturevalue(fixture_name + "_uri") + file = request.getfixturevalue(fixture_name) + source_encoded = file.read() + tree = utf8_mal_parser.parse(source_encoded) + + storage[doc_uri] = Document(tree, source_encoded, doc_uri) + + # obtain the included files + root_node = tree.root_node + + captures = run_query(root_node, INCLUDED_FILES_QUERY) + if "file_name" in captures: + recursive_parsing(mal_root_str, captures["file_name"], storage, doc_uri, []) + + ################################### + + # get the node + cursor = tree.walk() + while cursor.goto_first_child_for_point(point) is not None: + continue + + # confirm it's an identifier + assert cursor.node.type == "identifier" + + # we use sets to ensure order does not matter + response = find_symbol_definition(cursor.node, cursor.node.text, doc_uri, storage) + + assert response[0].start_point == expected_result diff --git a/tests/unit/test_find_symbols_in_context_hierarchy.py b/tests/unit/test_find_symbols_in_context_hierarchy.py new file mode 100644 index 0000000..e9af53a --- /dev/null +++ b/tests/unit/test_find_symbols_in_context_hierarchy.py @@ -0,0 +1,119 @@ +import pytest +from tree_sitter import TreeCursor + +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), + ], + ), +] + +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 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 diff --git a/tests/unit/test_find_symbols_in_scope.py b/tests/unit/test_find_symbols_in_scope.py new file mode 100644 index 0000000..35314ea --- /dev/null +++ b/tests/unit/test_find_symbols_in_scope.py @@ -0,0 +1,67 @@ +import typing + +import pytest +import tree_sitter + +from malls.ts.utils import find_symbols_in_current_scope + + +@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 + assert set(user_symbols) == expected_user_symbols + assert set(keywords) == expected_keywords diff --git a/tests/unit/test_position_conversion.py b/tests/unit/test_position_conversion.py new file mode 100644 index 0000000..a79100c --- /dev/null +++ b/tests/unit/test_position_conversion.py @@ -0,0 +1,34 @@ +import pytest + +from malls.lsp.models import Position +from malls.ts.utils import lsp_to_tree_sitter_position, tree_sitter_to_lsp_position + +# (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 == ts_position + assert tree_sitter_to_lsp_position(text, result_position) == position diff --git a/tests/unit/test_visit_expr.py b/tests/unit/test_visit_expr.py new file mode 100644 index 0000000..7839055 --- /dev/null +++ b/tests/unit/test_visit_expr.py @@ -0,0 +1,74 @@ +import typing + +import pytest +from tree_sitter import Parser, Tree, TreeCursor + +from malls.ts.utils import visit_expr + + +@pytest.fixture +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", + ], + ), + ((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 == expected diff --git a/tests/util.py b/tests/util.py index 6c8264d..299dda9 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,9 +1,14 @@ import asyncio +import glob import io import json +import os import typing +from os import path from pathlib import Path +import pytest +import uritools from pylsp_jsonrpc.endpoint import Endpoint from pylsp_jsonrpc.exceptions import JsonRpcException from pylsp_jsonrpc.streams import JsonRpcStreamReader @@ -11,6 +16,216 @@ from malls.mal_lsp import MALLSPServer +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. 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) + + +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 isinstance(file_name, 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 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): + """ + 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`. + """ + + # 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 + + def fixture_uri(file_path: str | Path): + 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 + ) + + fixture = pytest.fixture( + open_fixture_file(path), + name=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( + 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) + + +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`. + """ + 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: + # 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 = (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 = (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) + + +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: @@ -119,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 -]