diff --git a/README.md b/README.md index 4c9bb8a..097a497 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,11 @@ A Language Server Protocol (LSP) server to provide language support for the [SPI - **Diagnostics**: Reports the location of syntax errors and warnings. - **Signature help**: Shows parameter hints as instructions are entered. -- **Hover**: Shows documentation and values on hover. +- **Hover**: Shows documentation and assigned values on hover. - **Completion**: Provides suggestions for opcodes, labels, and variables. -- **Renaming**: Allows renaming labels and variables. +- **Renaming**: Renames matching labels or variables. - **Go to definition**: Jumps to the definition of a label, memory address, or variable. +- **Semantic highlighting**: Color codes variables, constants, instructions, etc. based on program semantics. ------ diff --git a/tests/conftest.py b/tests/conftest.py index bf9d2e6..0b0631a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,10 @@ from __future__ import annotations +from dataclasses import dataclass from pathlib import Path -from typing import TypedDict import lsprotocol.types as lsp +import pytest import pytest_lsp from pytest_lsp import ClientServerConfig, LanguageClient @@ -28,10 +29,16 @@ async def client(request, lsp_client: LanguageClient): await lsp_client.shutdown_session() -class TestCase(TypedDict): +@dataclass +class TestCase: """The inputs and outputs of a test case.""" __test__ = False name: str """The name used to identify the test case.""" + + +def parametrize_cases(test_cases: list[TestCase]): + """A decorator to parametrize a test function with test cases.""" + return pytest.mark.parametrize("test_case", test_cases, ids=lambda x: x.name) diff --git a/tests/server_tests/test_completion.py b/tests/server_tests/test_completion.py index 941fbfe..db5aa9e 100644 --- a/tests/server_tests/test_completion.py +++ b/tests/server_tests/test_completion.py @@ -1,12 +1,15 @@ from __future__ import annotations +from dataclasses import dataclass + import lsprotocol.types as lsp import pytest from pytest_lsp import LanguageClient -from ..conftest import PATCH_DIR, TestCase +from ..conftest import PATCH_DIR, TestCase, parametrize_cases +@dataclass class CompletionTestCase(TestCase): """A dictionary to track an expected completion result.""" @@ -17,62 +20,62 @@ class CompletionTestCase(TestCase): uri: str -COMPLETIONS: list[CompletionTestCase] = [ - { - "name": "APOUT", - "label": "APOUT", - "detail": "(variable) APOUT: Literal[33]", - "kind": lsp.CompletionItemKind.Variable, - "doc_contains": None, - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, - { - "name": "REG0", - "label": "REG0", - "detail": "(constant) REG0: Literal[32]", - "kind": lsp.CompletionItemKind.Constant, - "doc_contains": None, - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, - { - "name": "CHO RDA", - "label": "CHO RDA", - "detail": "(opcode)", - "kind": lsp.CompletionItemKind.Function, - "doc_contains": "`CHO RDA, N, C, D`", - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, - { - "name": "EQU", - "label": "EQU", - "detail": "(assembler)", - "kind": lsp.CompletionItemKind.Operator, - "doc_contains": "**`EQU`** allows one to define symbolic operands", - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, +TEST_CASES: list[CompletionTestCase] = [ + CompletionTestCase( + name="variable", + label="APOUT", + detail="(variable) APOUT: Literal[33]", + kind=lsp.CompletionItemKind.Variable, + doc_contains=None, + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), + CompletionTestCase( + name="constant", + label="REG0", + detail="(constant) REG0: Literal[32]", + kind=lsp.CompletionItemKind.Constant, + doc_contains=None, + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), + CompletionTestCase( + name="multi-word opcode", + label="CHO RDA", + detail="(opcode)", + kind=lsp.CompletionItemKind.Function, + doc_contains="`CHO RDA, N, C, D`", + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), + CompletionTestCase( + name="assembler", + label="EQU", + detail="(assembler)", + kind=lsp.CompletionItemKind.Operator, + doc_contains="**`EQU`** allows one to define symbolic operands", + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), ] -@pytest.mark.parametrize("test_case", COMPLETIONS, ids=lambda x: x["name"]) +@parametrize_cases(TEST_CASES) @pytest.mark.asyncio() async def test_completions(test_case: CompletionTestCase, client: LanguageClient): """Test that expected completions are shown with details and documentation.""" results = await client.text_document_completion_async( params=lsp.CompletionParams( position=lsp.Position(line=0, character=0), - text_document=lsp.TextDocumentIdentifier(uri=test_case["uri"]), + text_document=lsp.TextDocumentIdentifier(uri=test_case.uri), ) ) assert results is not None, "Expected completions" - matches = [item for item in results.items if item.label == test_case["label"]] + matches = [item for item in results.items if item.label == test_case.label] assert ( len(matches) == 1 - ), f"Expected 1 matching label `{test_case['label']}`, got {len(matches)}." + ), f"Expected 1 matching label `{test_case.label}`, got {len(matches)}." match = matches[0] - assert match.detail == test_case["detail"] - assert match.kind == test_case["kind"] - if test_case["doc_contains"] is not None: - assert test_case["doc_contains"] in str(match.documentation) + assert match.detail == test_case.detail + assert match.kind == test_case.kind + if test_case.doc_contains is not None: + assert test_case.doc_contains in str(match.documentation) diff --git a/tests/server_tests/test_definition.py b/tests/server_tests/test_definition.py index 5126ce7..460c65f 100644 --- a/tests/server_tests/test_definition.py +++ b/tests/server_tests/test_definition.py @@ -1,12 +1,15 @@ from __future__ import annotations +from dataclasses import dataclass + import lsprotocol.types as lsp import pytest from pytest_lsp import LanguageClient -from ..conftest import PATCH_DIR, TestCase +from ..conftest import PATCH_DIR, TestCase, parametrize_cases +@dataclass class DefinitionTestCase(TestCase): """A dictionary track where a symbol is referenced and defined.""" @@ -15,72 +18,67 @@ class DefinitionTestCase(TestCase): uri: str -DEFINITIONS: list[DefinitionTestCase] = [ - { - # Variable - "name": "apout", - "referenced": lsp.Position(line=57, character=7), - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - "defined": lsp.Location( +TEST_CASES: list[DefinitionTestCase] = [ + DefinitionTestCase( + name="apout", + referenced=lsp.Position(line=57, character=7), + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + defined=lsp.Location( uri=f"file:///{PATCH_DIR / 'Basic.spn'}", range=lsp.Range( start=lsp.Position(line=23, character=4), end=lsp.Position(line=23, character=9), ), ), - }, - { - # Memory - "name": "lap2a", - "referenced": lsp.Position(line=72, character=7), - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - "defined": lsp.Location( + ), + DefinitionTestCase( + name="lap2a", + referenced=lsp.Position(line=72, character=7), + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + defined=lsp.Location( uri=f"file:///{PATCH_DIR / 'Basic.spn'}", range=lsp.Range( start=lsp.Position(line=16, character=4), end=lsp.Position(line=16, character=9), ), ), - }, - { - # Memory. Note that this has an address modifier, but still points to the - # original definition. - "name": "lap2a#", - "referenced": lsp.Position(line=71, character=7), - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - "defined": lsp.Location( + ), + DefinitionTestCase( + name="lap2a#", + referenced=lsp.Position(line=71, character=7), + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + defined=lsp.Location( uri=f"file:///{PATCH_DIR / 'Basic.spn'}", range=lsp.Range( start=lsp.Position(line=16, character=4), end=lsp.Position(line=16, character=9), ), ), - }, - { - # Label - "name": "endclr", - "referenced": lsp.Position(line=37, character=9), - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - "defined": lsp.Location( + ), + DefinitionTestCase( + name="endclr", + referenced=lsp.Position(line=37, character=9), + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + defined=lsp.Location( uri=f"file:///{PATCH_DIR / 'Basic.spn'}", range=lsp.Range( start=lsp.Position(line=41, character=0), end=lsp.Position(line=41, character=6), ), ), - }, + ), ] @pytest.mark.asyncio() -@pytest.mark.parametrize("test_case", DEFINITIONS, ids=lambda x: x["name"]) +@parametrize_cases(TEST_CASES) async def test_definition(test_case: DefinitionTestCase, client: LanguageClient): """Test that the definition location of different assignments is correct.""" result = await client.text_document_definition_async( params=lsp.DefinitionParams( - position=test_case["referenced"], - text_document=lsp.TextDocumentIdentifier(uri=test_case["uri"]), + position=test_case.referenced, + text_document=lsp.TextDocumentIdentifier(uri=test_case.uri), ) ) - assert result == test_case["defined"] + assert result == test_case.defined diff --git a/tests/server_tests/test_diagnostics.py b/tests/server_tests/test_diagnostics.py index 16610e8..af8b3ff 100644 --- a/tests/server_tests/test_diagnostics.py +++ b/tests/server_tests/test_diagnostics.py @@ -1,70 +1,97 @@ +from __future__ import annotations + +from dataclasses import dataclass + import lsprotocol.types as lsp import pytest from pytest_lsp import LanguageClient +from ..conftest import TestCase, parametrize_cases -@pytest.mark.asyncio() -async def test_diagnostic_parsing_errors(client: LanguageClient): - """Test that parsing errors and warnings are correctly reported by the server.""" - source_with_errors = """ -; Undefined symbol a -SOF 0,a -; Label REG0 re-defined -REG0 EQU 4 +@dataclass +class DiagnosticTestCase(TestCase): + """A dictionary to record prepare rename results for a symbol.""" + + source: str + expected: list[lsp.Diagnostic] -; Register out of range -MULX 100 -""" +TEST_CASES: list[DiagnosticTestCase] = [ + DiagnosticTestCase( + name="undefined label", + source="""SOF 0, a\n""", + expected=[ + lsp.Diagnostic( + range=lsp.Range( + start=lsp.Position(line=0, character=7), + end=lsp.Position(line=0, character=7), + ), + message="Undefined label a", + severity=lsp.DiagnosticSeverity.Error, + source="SPINAsm", + ), + ], + ), + DiagnosticTestCase( + name="redefined constant", + source="""REG0 EQU 4\n""", + expected=[ + lsp.Diagnostic( + range=lsp.Range( + start=lsp.Position(line=0, character=9), + end=lsp.Position(line=0, character=9), + ), + message="Label REG0 re-defined", + severity=lsp.DiagnosticSeverity.Warning, + source="SPINAsm", + ), + ], + ), + DiagnosticTestCase( + name="out of range", + source="""MULX 100\n""", + expected=[ + lsp.Diagnostic( + range=lsp.Range( + start=lsp.Position(line=0, character=0), + end=lsp.Position(line=0, character=0), + ), + message="Register 0x64 out of range for MULX", + severity=lsp.DiagnosticSeverity.Error, + source="SPINAsm", + ), + ], + ), +] + + +@parametrize_cases(TEST_CASES) +@pytest.mark.asyncio() +async def test_diagnostic_parsing_errors( + test_case: DiagnosticTestCase, client: LanguageClient +): + """Test that parsing errors and warnings are correctly reported by the server.""" # We need a URI to associate with the source, but it doesn't need to be a real file. test_uri = "dummy_uri" + client.text_document_did_open( lsp.DidOpenTextDocumentParams( text_document=lsp.TextDocumentItem( uri=test_uri, language_id="spinasm", version=1, - text=source_with_errors, + text=test_case.source, ) ) ) await client.wait_for_notification(lsp.TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS) - expected = [ - lsp.Diagnostic( - range=lsp.Range( - start=lsp.Position(line=2, character=6), - end=lsp.Position(line=2, character=6), - ), - message="Undefined label a", - severity=lsp.DiagnosticSeverity.Error, - source="SPINAsm", - ), - lsp.Diagnostic( - range=lsp.Range( - start=lsp.Position(line=5, character=9), - end=lsp.Position(line=5, character=9), - ), - message="Label REG0 re-defined", - severity=lsp.DiagnosticSeverity.Warning, - source="SPINAsm", - ), - lsp.Diagnostic( - range=lsp.Range( - start=lsp.Position(line=8, character=0), - end=lsp.Position(line=8, character=0), - ), - message="Register 0x64 out of range for MULX", - severity=lsp.DiagnosticSeverity.Error, - source="SPINAsm", - ), - ] - returned = client.diagnostics[test_uri] - extra = len(returned) - len(expected) - assert extra == 0, f"Expected {len(expected)} diagnostics, got {len(returned)}." + assert len(returned) == len( + test_case.expected + ), "Expected number of diagnostics does not match" - for i, diag in enumerate(expected): - assert diag == returned[i], f"Diagnostic {i} does not match expected" + for expected, actual in zip(test_case.expected, returned): + assert actual == expected, "Diagnostic does not match expected" diff --git a/tests/server_tests/test_hover.py b/tests/server_tests/test_hover.py index 49ce7dc..a0ac7cf 100644 --- a/tests/server_tests/test_hover.py +++ b/tests/server_tests/test_hover.py @@ -1,12 +1,15 @@ from __future__ import annotations +from dataclasses import dataclass + import lsprotocol.types as lsp import pytest from pytest_lsp import LanguageClient -from ..conftest import PATCH_DIR, TestCase +from ..conftest import PATCH_DIR, TestCase, parametrize_cases +@dataclass class HoverTestCase(TestCase): """A dictionary to record hover information for a symbol.""" @@ -15,79 +18,76 @@ class HoverTestCase(TestCase): uri: str -HOVERS: list[HoverTestCase] = [ - { - "name": "mem", - "position": lsp.Position(line=8, character=0), - "contains": "`MEM`", - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, - { - "name": "skp", - "position": lsp.Position(line=37, character=2), - "contains": "`SKP CMASK, N`", - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, - { - "name": "endclr", - "position": lsp.Position(line=37, character=13), - "contains": "(label) ENDCLR: Offset[4]", - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, - { - "name": "mono", - "position": lsp.Position(line=47, character=5), - "contains": "(variable) MONO: Literal[32]", - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, - { - "name": "reg0", - "position": lsp.Position(line=22, character=9), - "contains": "(constant) REG0: Literal[32]", - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, - { - "name": "lap2b#", - "position": lsp.Position(line=73, character=4), - "contains": "(variable) LAP2B#: Literal[9802]", - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, - { - # CHO RDA, hovering over CHO - "name": "CHO_rda", - "position": lsp.Position(line=85, character=0), - "contains": "`CHO RDA, N, C, D`", - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, - { - # CHO RDA, hovering over RDA - "name": "cho_RDA", - "position": lsp.Position(line=85, character=4), - "contains": "`CHO RDA, N, C, D`", - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, - { - # Hovering over an int, which should return no hover info - "name": "None", - "position": lsp.Position(line=8, character=8), - "contains": None, - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, +TEST_CASES: list[HoverTestCase] = [ + HoverTestCase( + name="mem", + position=lsp.Position(line=8, character=0), + contains="`MEM`", + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), + HoverTestCase( + name="skp", + position=lsp.Position(line=37, character=2), + contains="`SKP CMASK, N`", + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), + HoverTestCase( + name="endclr", + position=lsp.Position(line=37, character=13), + contains="(label) ENDCLR: Offset[4]", + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), + HoverTestCase( + name="mono", + position=lsp.Position(line=47, character=5), + contains="(variable) MONO: Literal[32]", + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), + HoverTestCase( + name="reg0", + position=lsp.Position(line=22, character=9), + contains="(constant) REG0: Literal[32]", + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), + HoverTestCase( + name="lap2b#", + position=lsp.Position(line=73, character=4), + contains="(variable) LAP2B#: Literal[9802]", + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), + HoverTestCase( + name="CHO_rda", + position=lsp.Position(line=85, character=0), + contains="`CHO RDA, N, C, D`", + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), + HoverTestCase( + name="cho_RDA", + position=lsp.Position(line=85, character=4), + contains="`CHO RDA, N, C, D`", + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), + HoverTestCase( + name="None", + position=lsp.Position(line=8, character=8), + contains=None, + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), ] -@pytest.mark.parametrize("test_case", HOVERS, ids=lambda x: x["name"]) +@parametrize_cases(TEST_CASES) @pytest.mark.asyncio() -async def test_hover(test_case: dict, client: LanguageClient): +async def test_hover(test_case: HoverTestCase, client: LanguageClient): result = await client.text_document_hover_async( params=lsp.CompletionParams( - position=test_case["position"], - text_document=lsp.TextDocumentIdentifier(uri=test_case["uri"]), + position=test_case.position, + text_document=lsp.TextDocumentIdentifier(uri=test_case.uri), ) ) - if test_case["contains"] is None: + if test_case.contains is None: assert result is None, "Expected no hover result" else: - msg = f"Hover does not contain `{test_case['contains']}`" - assert test_case["contains"] in result.contents.value, msg + msg = f"Hover does not contain `{test_case.contains}`" + assert test_case.contains in result.contents.value, msg diff --git a/tests/server_tests/test_prepare_rename.py b/tests/server_tests/test_prepare_rename.py index 9206c7a..ea524fb 100644 --- a/tests/server_tests/test_prepare_rename.py +++ b/tests/server_tests/test_prepare_rename.py @@ -1,12 +1,15 @@ from __future__ import annotations +from dataclasses import dataclass + import lsprotocol.types as lsp import pytest from pytest_lsp import LanguageClient -from ..conftest import PATCH_DIR, TestCase +from ..conftest import PATCH_DIR, TestCase, parametrize_cases +@dataclass class PrepareRenameTestCase(TestCase): """A dictionary to record prepare rename results for a symbol.""" @@ -16,53 +19,53 @@ class PrepareRenameTestCase(TestCase): uri: str -PREPARE_RENAMES: list[PrepareRenameTestCase] = [ - { - "name": "mem", - "position": lsp.Position(line=8, character=0), - "result": None, - "message": "Can't rename non-user defined token MEM.", - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, - { - "name": "reg0", - "position": lsp.Position(line=22, character=10), - "result": None, - "message": "Can't rename non-user defined token REG0.", - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, - { - "name": "ap1", - "position": lsp.Position(line=8, character=4), - "result": lsp.PrepareRenameResult_Type2(default_behavior=True), - "message": None, - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, - { - "name": "endclr", - "position": lsp.Position(line=37, character=10), - "result": lsp.PrepareRenameResult_Type2(default_behavior=True), - "message": None, - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, +TEST_CASES: list[PrepareRenameTestCase] = [ + PrepareRenameTestCase( + name="mem", + position=lsp.Position(line=8, character=0), + result=None, + message="Can't rename non-user defined token MEM.", + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), + PrepareRenameTestCase( + name="reg0", + position=lsp.Position(line=22, character=10), + result=None, + message="Can't rename non-user defined token REG0.", + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), + PrepareRenameTestCase( + name="ap1", + position=lsp.Position(line=8, character=4), + result=lsp.PrepareRenameResult_Type2(default_behavior=True), + message=None, + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), + PrepareRenameTestCase( + name="endclr", + position=lsp.Position(line=37, character=10), + result=lsp.PrepareRenameResult_Type2(default_behavior=True), + message=None, + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), ] -@pytest.mark.parametrize("test_case", PREPARE_RENAMES, ids=lambda x: x["name"]) +@parametrize_cases(TEST_CASES) @pytest.mark.asyncio() async def test_prepare_rename(test_case: PrepareRenameTestCase, client: LanguageClient): """Test that prepare rename prevents renaming non-user defined tokens.""" result = await client.text_document_prepare_rename_async( params=lsp.PrepareRenameParams( - position=test_case["position"], - text_document=lsp.TextDocumentIdentifier(uri=test_case["uri"]), + position=test_case.position, + text_document=lsp.TextDocumentIdentifier(uri=test_case.uri), ) ) - assert result == test_case["result"] + assert result == test_case.result - if test_case["message"]: - assert test_case["message"] in client.log_messages[0].message + if test_case.message: + assert test_case.message in client.log_messages[0].message assert client.log_messages[0].type == lsp.MessageType.Info else: assert not client.log_messages diff --git a/tests/server_tests/test_reference.py b/tests/server_tests/test_reference.py index 080028e..12fe6e8 100644 --- a/tests/server_tests/test_reference.py +++ b/tests/server_tests/test_reference.py @@ -1,12 +1,15 @@ from __future__ import annotations +from dataclasses import dataclass + import lsprotocol.types as lsp import pytest from pytest_lsp import LanguageClient -from ..conftest import PATCH_DIR, TestCase +from ..conftest import PATCH_DIR, TestCase, parametrize_cases +@dataclass class ReferenceTestCase(TestCase): """A dictionary to record reference locations for a symbol.""" @@ -15,13 +18,12 @@ class ReferenceTestCase(TestCase): uri: str -REFERENCES: list[ReferenceTestCase] = [ - { - # Variable - "name": "apout", - "position": lsp.Position(line=23, character=4), - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - "references": [ +TEST_CASES: list[ReferenceTestCase] = [ + ReferenceTestCase( + name="apout", + position=lsp.Position(line=23, character=4), + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + references=[ lsp.Location( uri=f"file:///{PATCH_DIR / 'Basic.spn'}", range=lsp.Range( @@ -51,12 +53,12 @@ class ReferenceTestCase(TestCase): ), ), ], - }, - { - "name": "ap1", - "position": lsp.Position(line=8, character=4), - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - "references": [ + ), + ReferenceTestCase( + name="ap1", + position=lsp.Position(line=8, character=4), + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + references=[ lsp.Location( uri=f"file:///{PATCH_DIR / 'Basic.spn'}", range=lsp.Range( @@ -79,20 +81,20 @@ class ReferenceTestCase(TestCase): ), ), ], - }, + ), ] -@pytest.mark.parametrize("test_case", REFERENCES, ids=lambda x: x["name"]) +@parametrize_cases(TEST_CASES) @pytest.mark.asyncio() async def test_references(test_case: ReferenceTestCase, client: LanguageClient): """Test that references to a symbol are correctly found.""" result = await client.text_document_references_async( params=lsp.ReferenceParams( context=lsp.ReferenceContext(include_declaration=False), - position=test_case["position"], - text_document=lsp.TextDocumentIdentifier(uri=test_case["uri"]), + position=test_case.position, + text_document=lsp.TextDocumentIdentifier(uri=test_case.uri), ) ) - assert result == test_case["references"] + assert result == test_case.references diff --git a/tests/server_tests/test_rename.py b/tests/server_tests/test_rename.py index 3c65ffa..c657af5 100644 --- a/tests/server_tests/test_rename.py +++ b/tests/server_tests/test_rename.py @@ -1,12 +1,15 @@ from __future__ import annotations +from dataclasses import dataclass + import lsprotocol.types as lsp import pytest from pytest_lsp import LanguageClient -from ..conftest import PATCH_DIR, TestCase +from ..conftest import PATCH_DIR, TestCase, parametrize_cases +@dataclass class RenameTestCase(TestCase): """A dictionary to record rename results for a symbol.""" @@ -16,13 +19,13 @@ class RenameTestCase(TestCase): uri: str -RENAMES: list[RenameTestCase] = [ - { - "name": "ap1", - "rename_to": "FOO", - "position": lsp.Position(line=8, character=4), - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - "changes": [ +TEST_CASES: list[RenameTestCase] = [ + RenameTestCase( + name="ap1", + rename_to="FOO", + position=lsp.Position(line=8, character=4), + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + changes=[ lsp.TextEdit( range=lsp.Range(start=lsp.Position(8, 4), end=lsp.Position(8, 7)), new_text="FOO", @@ -37,13 +40,13 @@ class RenameTestCase(TestCase): new_text="FOO", ), ], - }, - { - "name": "endclr", - "rename_to": "END", - "position": lsp.Position(line=41, character=0), - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - "changes": [ + ), + RenameTestCase( + name="endclr", + rename_to="END", + position=lsp.Position(line=41, character=0), + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + changes=[ lsp.TextEdit( range=lsp.Range(start=lsp.Position(37, 8), end=lsp.Position(37, 14)), new_text="END", @@ -53,13 +56,13 @@ class RenameTestCase(TestCase): new_text="END", ), ], - }, - { - "name": "lap1a#", - "rename_to": "FOO", - "position": lsp.Position(line=61, character=4), - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - "changes": [ + ), + RenameTestCase( + name="lap1a#", + rename_to="FOO", + position=lsp.Position(line=61, character=4), + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + changes=[ # Renaming `lap1a#` should also rename `lap1a` lsp.TextEdit( range=lsp.Range(start=lsp.Position(12, 4), end=lsp.Position(12, 9)), @@ -74,20 +77,20 @@ class RenameTestCase(TestCase): new_text="FOO", ), ], - }, + ), ] -@pytest.mark.parametrize("test_case", RENAMES, ids=lambda x: x["name"]) +@parametrize_cases(TEST_CASES) @pytest.mark.asyncio() async def test_rename(test_case: RenameTestCase, client: LanguageClient): """Test that renaming a symbol suggests the correct edits.""" result = await client.text_document_rename_async( params=lsp.RenameParams( - position=test_case["position"], - new_name=test_case["rename_to"], - text_document=lsp.TextDocumentIdentifier(uri=test_case["uri"]), + position=test_case.position, + new_name=test_case.rename_to, + text_document=lsp.TextDocumentIdentifier(uri=test_case.uri), ) ) - assert result.changes[test_case["uri"]] == test_case["changes"] + assert result.changes[test_case.uri] == test_case.changes diff --git a/tests/server_tests/test_signature_help.py b/tests/server_tests/test_signature_help.py index b752e46..014a4e2 100644 --- a/tests/server_tests/test_signature_help.py +++ b/tests/server_tests/test_signature_help.py @@ -1,12 +1,15 @@ from __future__ import annotations +from dataclasses import dataclass + import lsprotocol.types as lsp import pytest from pytest_lsp import LanguageClient -from ..conftest import PATCH_DIR, TestCase +from ..conftest import PATCH_DIR, TestCase, parametrize_cases +@dataclass class SignatureHelpTestCase(TestCase): """A dictionary to record signature help information for at a position.""" @@ -17,73 +20,67 @@ class SignatureHelpTestCase(TestCase): uri: str -SIGNATURE_HELPS: list[SignatureHelpTestCase] = [ - { - # No opcode on this line, so the signature help should be None - "name": "no_opcode", - "position": lsp.Position(line=8, character=3), - "active_parameter": None, - "doc_contains": None, - "param_contains": None, - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, - { - "name": "skp_first_arg", - "position": lsp.Position(line=37, character=4), - "active_parameter": 0, - "doc_contains": "**`SKP CMASK, N`** allows conditional program execution", - "param_contains": "CMASK: Binary | Hex ($00-$1F) | Symbolic", - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, - { - "name": "skp_second_arg", - "position": lsp.Position(line=37, character=8), - "active_parameter": 1, - "doc_contains": "**`SKP CMASK, N`** allows conditional program execution", - "param_contains": "N: Decimal (1-63) | Label", - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, - { - # You should still get the last argument even if you're well beyond it - "name": "skp_out_of_bounds", - "position": lsp.Position(line=37, character=45), - "active_parameter": 1, - "doc_contains": "**`SKP CMASK, N`** allows conditional program execution", - "param_contains": "N: Decimal (1-63) | Label", - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, - { - # The "first" argument of CHO RDA should be N, not RDA - "name": "cho_rda", - "position": lsp.Position(line=85, character=8), - "active_parameter": 0, - "doc_contains": "**`CHO RDA, N, C, D`**, like the `RDA` instruction", - "param_contains": "N: LFO select: SIN0,SIN1,RMP0,RMP1", - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, - { - # Triggering signature help before finishing the opcode should return None - "name": "cho_rda_unfinished", - "position": lsp.Position(line=85, character=0), - "active_parameter": None, - "doc_contains": None, - "param_contains": None, - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, - { - # Triggering signature help after finishing, before the comma in a multi-word - # instruction should return none - "name": "cho_rda_before_comma", - "position": lsp.Position(line=85, character=7), - "active_parameter": None, - "doc_contains": None, - "param_contains": None, - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - }, +TEST_CASES: list[SignatureHelpTestCase] = [ + SignatureHelpTestCase( + name="no_opcode", + position=lsp.Position(line=8, character=3), + active_parameter=None, + doc_contains=None, + param_contains=None, + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), + SignatureHelpTestCase( + name="skp_first_arg", + position=lsp.Position(line=37, character=4), + active_parameter=0, + doc_contains="**`SKP CMASK, N`** allows conditional program execution", + param_contains="CMASK: Binary | Hex ($00-$1F) | Symbolic", + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), + SignatureHelpTestCase( + name="skp_second_arg", + position=lsp.Position(line=37, character=8), + active_parameter=1, + doc_contains="**`SKP CMASK, N`** allows conditional program execution", + param_contains="N: Decimal (1-63) | Label", + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), + SignatureHelpTestCase( + name="skp_out_of_bounds", + position=lsp.Position(line=37, character=45), + active_parameter=1, + doc_contains="**`SKP CMASK, N`** allows conditional program execution", + param_contains="N: Decimal (1-63) | Label", + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), + SignatureHelpTestCase( + name="cho_rda", + position=lsp.Position(line=85, character=8), + active_parameter=0, + doc_contains="**`CHO RDA, N, C, D`**, like the `RDA` instruction", + param_contains="N: LFO select: SIN0,SIN1,RMP0,RMP1", + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), + SignatureHelpTestCase( + name="cho_rda_unfinished", + position=lsp.Position(line=85, character=0), + active_parameter=None, + doc_contains=None, + param_contains=None, + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), + SignatureHelpTestCase( + name="cho_rda_before_comma", + position=lsp.Position(line=85, character=7), + active_parameter=None, + doc_contains=None, + param_contains=None, + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + ), ] -@pytest.mark.parametrize("test_case", SIGNATURE_HELPS, ids=lambda x: x["name"]) +@parametrize_cases(TEST_CASES) @pytest.mark.asyncio() async def test_signature_help(test_case: SignatureHelpTestCase, client: LanguageClient): result = await client.text_document_signature_help_async( @@ -92,18 +89,18 @@ async def test_signature_help(test_case: SignatureHelpTestCase, client: Language trigger_kind=lsp.SignatureHelpTriggerKind.TriggerCharacter, is_retrigger=False, ), - position=test_case["position"], - text_document=lsp.TextDocumentIdentifier(uri=test_case["uri"]), + position=test_case.position, + text_document=lsp.TextDocumentIdentifier(uri=test_case.uri), ) ) - if test_case["active_parameter"] is None: + if test_case.active_parameter is None: assert not result return sig: lsp.SignatureInformation = result.signatures[result.active_signature] param: lsp.ParameterInformation = sig.parameters[result.active_parameter] - assert test_case["active_parameter"] == result.active_parameter - assert test_case["doc_contains"] in str(sig.documentation) - assert test_case["param_contains"] in param.label + assert test_case.active_parameter == result.active_parameter + assert test_case.doc_contains in str(sig.documentation) + assert test_case.param_contains in param.label diff --git a/tests/server_tests/test_symbol_definition.py b/tests/server_tests/test_symbol_definition.py index 1e469df..0b1270a 100644 --- a/tests/server_tests/test_symbol_definition.py +++ b/tests/server_tests/test_symbol_definition.py @@ -1,12 +1,15 @@ from __future__ import annotations +from dataclasses import dataclass + import lsprotocol.types as lsp import pytest from pytest_lsp import LanguageClient -from ..conftest import PATCH_DIR, TestCase +from ..conftest import PATCH_DIR, TestCase, parametrize_cases +@dataclass class SymbolDefinitionTestCase(TestCase): """A dictionary to record definition locations for a symbol.""" @@ -15,41 +18,38 @@ class SymbolDefinitionTestCase(TestCase): uri: str -SYMBOL_DEFINITIONS: list[SymbolDefinitionTestCase] = [ - { - # Variable - "name": "apout", - "kind": lsp.SymbolKind.Variable, - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - "range": lsp.Range( +TEST_CASES: list[SymbolDefinitionTestCase] = [ + SymbolDefinitionTestCase( + name="apout", + kind=lsp.SymbolKind.Variable, + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + range=lsp.Range( start=lsp.Position(line=23, character=4), end=lsp.Position(line=23, character=9), ), - }, - { - # Memory - "name": "lap2a", - "kind": lsp.SymbolKind.Variable, - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - "range": lsp.Range( + ), + SymbolDefinitionTestCase( + name="lap2a", + kind=lsp.SymbolKind.Variable, + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + range=lsp.Range( start=lsp.Position(line=16, character=4), end=lsp.Position(line=16, character=9), ), - }, - { - # Label - "name": "endclr", - "kind": lsp.SymbolKind.Module, - "uri": f"file:///{PATCH_DIR / 'Basic.spn'}", - "range": lsp.Range( + ), + SymbolDefinitionTestCase( + name="endclr", + kind=lsp.SymbolKind.Module, + uri=f"file:///{PATCH_DIR / 'Basic.spn'}", + range=lsp.Range( start=lsp.Position(line=41, character=0), end=lsp.Position(line=41, character=6), ), - }, + ), ] -@pytest.mark.parametrize("test_case", SYMBOL_DEFINITIONS, ids=lambda x: x["name"]) +@parametrize_cases(TEST_CASES) @pytest.mark.asyncio() async def test_symbol_definitions( test_case: SymbolDefinitionTestCase, client: LanguageClient @@ -57,13 +57,13 @@ async def test_symbol_definitions( """Test that the definitions of all symbols in the document are returned.""" result = await client.text_document_document_symbol_async( params=lsp.DocumentSymbolParams( - text_document=lsp.TextDocumentIdentifier(uri=test_case["uri"]), + text_document=lsp.TextDocumentIdentifier(uri=test_case.uri), ) ) - matching = [item for item in result if item.name == test_case["name"].upper()] - assert matching, f"Symbol {test_case['name'].upper()} not in document symbols" + matching = [item for item in result if item.name == test_case.name.upper()] + assert matching, f"Symbol {test_case.name.upper()} not in document symbols" item = matching[0] - assert item.kind == test_case["kind"] - assert item.range == test_case["range"] + assert item.kind == test_case.kind + assert item.range == test_case.range