1
Fork 0
glsl_analyzer/tests/test_lsp.py

260 lines
7.6 KiB
Python

import os
import pathlib
import re
import typing
import lsprotocol.types as lspt
import pytest
import pytest_subtests
import pytest_lsp
from testing_utils import (
FileToTest,
OpenedFile,
)
import lsp_testing_input
import expected_hover
import expected_completion
@pytest_lsp.fixture(config=pytest_lsp.ClientServerConfig(
server_command=["glsl_analyzer", "--stdio", "--clientProcessId", str(os.getpid())]
))
async def client(lsp_client: pytest_lsp.LanguageClient):
"""A fixtre that carries a fake client."""
params = lspt.InitializeParams(
capabilities=lspt.ClientCapabilities(),
process_id=os.getpid()
)
await lsp_client.initialize_session(params)
yield
await lsp_client.shutdown_session()
@pytest.fixture(params=lsp_testing_input.files_to_test)
def opened_file(client: pytest_lsp.LanguageClient, request):
file: FileToTest = request.param
fullpath = lsp_testing_input.base_directory / file.path
with open(fullpath, "r") as file_handle:
source = file_handle.read()
this_doc = lspt.TextDocumentItem(
uri=f"file://{fullpath}",
language_id="glsl",
version=0,
text=source
)
this_doc_id = lspt.TextDocumentIdentifier(
uri=this_doc.uri
)
client.text_document_did_open(
lspt.DidOpenTextDocumentParams(this_doc)
)
yield OpenedFile(file, this_doc_id)
client.text_document_did_close(
lspt.DidCloseTextDocumentParams(this_doc_id)
)
def to_zero_based_position(line: int, column: int) -> lspt.Position:
return lspt.Position(line - 1, column -1)
def none_guard(
expected: typing.Any|None, result: typing.Any|None,
expect_fail: bool, expect_fail_reason: str, context: str
) -> bool:
"""Check if either `expected` or `result` is `None`.
If so, validate expectations.
If this returns `True`, both `expected` and `result` are `None` and
no further assertions should be made.
If this returns `False`, both `expected` and `result` have values
that should be validated further.
Otherwise, the function will exit through `assert` or `xfail`.
"""
if expected is None or result is None:
if not expect_fail:
assert result == expected
return True
else:
assert result != expected, \
f"Expected to fail because: {expect_fail_reason}. Passed instead."
pytest.xfail(context) # raises exception
return False
def strip_until_naked(formatted_text: str) -> str:
strippables = ("```glsl", "```", "\n", " ")
post_strip = formatted_text
while True:
pre_strip = post_strip
# This is probably inefficient, but I don't care.
# Keep strippin'.
for markdown_garbage in strippables:
post_strip = post_strip \
.removeprefix(markdown_garbage) \
.removesuffix(markdown_garbage)
if pre_strip == post_strip:
break
return post_strip
@pytest.mark.asyncio
async def test_hover(
client: pytest_lsp.LanguageClient,
opened_file: OpenedFile,
subtests: pytest_subtests.SubTests
):
for args in opened_file.file.hover_test_args:
line, column, expected, \
expect_fail, expect_fail_reason = args
file_location = f"{opened_file}:{line}:{column}"
def expect_generic(expected: str, result: lspt.Hover):
result: str = strip_until_naked(result.contents.value)
if not expect_fail:
assert result == expected
else: # expect fail
assert result != expected, \
f"Expected to fail because: {expect_fail_reason}. Passed instead."
pytest.xfail(file_location)
def expect_function(expected: str|set[str], result: lspt.Hover):
def strip_md(mdtext: lspt.MarkupContent) -> list[str]:
text = strip_until_naked(mdtext.value)
entries = text.split(sep="\n")
return entries
result: list[str] = strip_md(result.contents)
expected_set = {expected} if isinstance(expected, str) else expected
result_set = set(result)
has_no_duplicates = len(result_set) == len(result)
is_expected_overload_set = expected_set == result_set
if not expect_fail:
assert has_no_duplicates and is_expected_overload_set
else:
assert not (has_no_duplicates and is_expected_overload_set), \
f"Expected to fail because: {expect_fail_reason}. Passed instead."
pytest.xfail(file_location)
with subtests.test(msg=file_location):
result = await client.text_document_hover_async(
params=lspt.HoverParams(
text_document=opened_file.identifier,
position=to_zero_based_position(line, column)
)
)
if none_guard(expected, result, expect_fail, expect_fail_reason, file_location):
continue
match (expected):
case expected_hover.Generic():
expect_generic(expected.expected, result)
case expected_hover.FunctionIdent():
expect_function(expected.expected, result)
case _:
assert False, \
"Unknown hover target type. Forgot to add a handler here?"
@pytest.mark.asyncio
async def test_completion(
client: pytest_lsp.LanguageClient,
opened_file: OpenedFile,
subtests: pytest_subtests.SubTests
):
for args in opened_file.file.completion_test_args:
line, column, expected, \
expect_fail, expect_fail_reason = args
file_location = f"{opened_file}:{line}:{column}"
def expect_generic(expected: tuple[str], result: list[lspt.CompletionItem]):
labels = tuple(item.label for item in result)
expected_set = set(expected)
labels_set = set(labels)
def unique_if_present_in(label: str, collection):
return sum(label == item for item in collection) <= 1
all_expected_are_present = expected_set.issubset(labels_set)
all_expected_are_unique = all(unique_if_present_in(expected_label, labels) for expected_label in expected)
if not expect_fail:
# Check for uniqueness separately from presence,
# so that assert fails would point to the right reason.
assert all_expected_are_unique and all_expected_are_present
else: # expect fail
assert not (all_expected_are_unique and all_expected_are_present), \
f"Expected to fail because: {expect_fail_reason}. Passed instead."
pytest.xfail(file_location)
with subtests.test(msg=file_location):
result = await client.text_document_completion_async(
lspt.CompletionParams(
text_document=opened_file.identifier,
position=to_zero_based_position(line, column),
context=lspt.CompletionContext(
lspt.CompletionTriggerKind.Invoked
)
)
)
if none_guard(expected, result, expect_fail, expect_fail_reason, file_location):
continue
match (expected):
case expected_completion.Generic():
expect_generic(expected.expected, result)
case _:
assert False, \
"Unknown completion target type. Forgot to add a handler here?"