Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
68a3533
feat: Add test util to get fixture name based on file path
Tobiky Aug 19, 2025
745e582
feat: Add test util to load fixture file as pytest fixture
Tobiky Aug 19, 2025
dfbe86b
feat: LSP requests as fixture components and accompanying utils
Tobiky Aug 22, 2025
fa8b39a
test: Use LSP fixture components for encoding_capability_check
Tobiky Aug 22, 2025
97b90f4
refactor: Minimize fixture dependency
Tobiky Aug 22, 2025
086e991
test: Use LSP fixture components for base_protocol
Tobiky Aug 22, 2025
765e9c4
chore: format and lint
Tobiky Aug 22, 2025
fc4e752
docs: Add fixture doc comment for encoding capability
Tobiky Aug 22, 2025
04b8072
refactor: Split load fixture into module to load fixture function
Tobiky Aug 23, 2025
5c31db0
feat: Test util func to load dir files and uri's as fixtures
Tobiky Aug 23, 2025
f3a36fb
feat: Add option to use string/method name for find_last_request
Tobiky Aug 25, 2025
5c20caa
feat: Create URI fixtures for fixture files
Tobiky Aug 25, 2025
f42ff6a
test: Use LSP fixture components for test_completion
Tobiky Aug 25, 2025
fe2c278
fix: Use isinstance instead of keyword is
Tobiky Aug 25, 2025
02c80b3
fix: Add missing quotes around meta tag info
Tobiky Aug 26, 2025
d3e02d7
feat!: Change URI fixtures to use Path.as_uri
Tobiky Aug 26, 2025
adf9586
test: Use LSP component fixtures for test_goto_definition
Tobiky Aug 26, 2025
317a291
refactor: Use paratremization in test_visit_expr
Tobiky Aug 26, 2025
b7fd583
misc: Move test_visir_expr to unit tests
Tobiky Aug 26, 2025
8cb0546
refactor: Move LSP fixture components to LSP fixtures subfolder
Tobiky Aug 26, 2025
449f215
feat: initalize fixture capabilities, add didChange/didOpen base_open…
Tobiky Aug 27, 2025
692486d
test!: Use LSP fixture components for test_publish_diagnostics
Tobiky Aug 27, 2025
f9f3c6f
refactor: Move unit tests files to unit test folder
Tobiky Aug 27, 2025
0c1fc75
test: Paratremize test_position_conversion.py
Tobiky Aug 27, 2025
0a34b09
refactor: Paratremize test_find_symbols_in_scope.py
Tobiky Aug 27, 2025
0f8900b
refactor: Paratremize test_find_symbols_in_context_hierarchy.py
Tobiky Aug 27, 2025
08e3374
fix: Incorrect contentChanges for change_with_error fixture
Tobiky Aug 27, 2025
92e8431
fix: recursive_parsing not accepting paths not ending with slash
Tobiky Aug 27, 2025
392d3fb
feat: Add tests/ and tests/fixtures/mal paths as fixtures
Tobiky Aug 27, 2025
8633b39
refactor: Simplify paratremization for test_find_symbol_definition_fu…
Tobiky Aug 27, 2025
eb0fb3a
refactor: Simplify paratremization for test_find_meta_comment_functio…
Tobiky Aug 27, 2025
d57e78a
refactor: Paratremization for test_find_current_scope.py
Tobiky Aug 27, 2025
c4838b3
fix: indentation adjustments for test_diagnostics_when_changing_file_…
Tobiky Aug 27, 2025
324510f
test: Paratremize and use LSP fixture components for test_did_open_te…
Tobiky Aug 27, 2025
fdfa326
test: Paratremize and use LSP fixture components in test_diagnostics_…
Tobiky Aug 27, 2025
e77f7fb
test: Paratremize and use LSP fixture components for test_did_change_…
Tobiky Aug 27, 2025
83d517a
chore: ruff format
Tobiky Aug 27, 2025
efbbea2
chore: Disable ruff line length lint on TODOs
Tobiky Aug 27, 2025
90cdb13
chore: Replace map with generator expressions
Tobiky Aug 27, 2025
ff6d485
chore: Clean up imports and fixture locations
Tobiky Aug 27, 2025
cc883cb
test: Use LSP fixture components for test_trace.py
Tobiky Aug 27, 2025
02f371a
docs: Explain structure, purpose, and systems of tests
Tobiky Aug 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions docs/TESTS.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 10 additions & 8 deletions src/malls/lsp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/malls/lsp/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/malls/ts/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 - {}
Expand Down
131 changes: 60 additions & 71 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
15 changes: 0 additions & 15 deletions tests/fixtures/encoding_capability_check.in.lsp

This file was deleted.

8 changes: 0 additions & 8 deletions tests/fixtures/init_exit.in.lsp

This file was deleted.

7 changes: 0 additions & 7 deletions tests/fixtures/init_exit.out.lsp

This file was deleted.

15 changes: 0 additions & 15 deletions tests/fixtures/log_trace_messages.in.lsp

This file was deleted.

12 changes: 0 additions & 12 deletions tests/fixtures/log_trace_off.in.lsp

This file was deleted.

15 changes: 0 additions & 15 deletions tests/fixtures/log_trace_verbose.in.lsp

This file was deleted.

Loading
Loading