Skip to content

Commit

Permalink
Add tests for the lsp integration
Browse files Browse the repository at this point in the history
Ensure that autocompletion works fine for simple things
  • Loading branch information
Théophane Hufschmitt committed Jun 21, 2024
1 parent a32698b commit 86b627b
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 0 deletions.
21 changes: 21 additions & 0 deletions project.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,27 @@ organist.OrganistExpression
touch $out
"%,
},

lsp = {
name = "organist-lsp-integration",
version = "0.0",
env.buildInputs = {
nls = import_nix "nixpkgs#nls",
python3 = import_nix "nixpkgs#python3",
pygls = import_nix "nixpkgs#python3Packages.pygls",
pytest = import_nix "nixpkgs#python3Packages.pytest",
pytest-asyncio = import_nix "nixpkgs#python3Packages.pytest-asyncio",
},
env = {
src = import_nix "self",
phases = ["unpackPhase", "testPhase", "installPhase"],
testPhase = nix-s%"
cd tests/lsp
pytest | tee $out
"%,
installPhase = "touch $out",
},
},
},

flake.checks = import "tests/main.ncl",
Expand Down
1 change: 1 addition & 0 deletions tests/lsp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__
21 changes: 21 additions & 0 deletions tests/lsp/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import testlib
import asyncio
import pytest
import pytest_asyncio
from lsprotocol import types as lsp

@pytest_asyncio.fixture
async def client():
# Setup
client = testlib.LanguageClient("organist-test-suite", "v1")
await client.start_io("nls")
response = await client.initialize_async(
lsp.InitializeParams(
capabilities=lsp.ClientCapabilities(),
root_uri="."
)
)
assert response is not None
client.initialized(lsp.InitializedParams())
return client

1 change: 1 addition & 0 deletions tests/lsp/template/nickel.lock.ncl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ organist = import "../../../lib/organist.ncl" }
109 changes: 109 additions & 0 deletions tests/lsp/test_completion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import pytest
import pytest_asyncio
from lsprotocol import types as lsp
from testlib import LanguageClient, open_file

async def complete(client: LanguageClient, file_uri: str, pos: lsp.Position):
"""
Trigger an autocompletion in the given file at the given position
"""
results = await client.text_document_completion_async(
params=lsp.CompletionParams(
text_document=lsp.TextDocumentIdentifier(file_uri),
position=pos,
)
)
assert results is not None

if isinstance(results, lsp.CompletionList):
items = results.items
else:
items = results
return items

@pytest.mark.asyncio
async def test_completion_at_toplevel(client):
"""
Test that getting an autocompletion at toplevel shows the available fields
"""

test_file = 'template/project.ncl'
with open('../../templates/default/project.ncl') as template_file:
test_file_content = template_file.read()

test_uri = open_file(client, test_file, test_file_content)

completion_items = await complete(
client,
test_uri,
lsp.Position(line=12, character=0) # Empty line in the `config` record
)

labels = [item.label for item in completion_items]
assert "files" in labels
files_item = [item for item in completion_items if item.label == "files"][0]
assert files_item.documentation.value != ""

@pytest.mark.asyncio
async def test_completion_sub_field(client: LanguageClient):
"""
Test that completing on an option shows the available sub-options
"""
test_file = 'template/projectxx.ncl'
test_file_content = """
let inputs = import "./nickel.lock.ncl" in
let organist = inputs.organist in
organist.OrganistExpression
& {
Schema,
config | Schema = {
files.foo.c
},
}
| organist.modules.T
"""
test_uri = open_file(client, test_file, test_file_content)
completion_items = await complete(
client,
test_uri,
lsp.Position(line=8, character=17) # The `c` in `files.foo.c`
)

labels = [item.label for item in completion_items]
assert "content" in labels
content_item = [item for item in completion_items if item.label == "content"][0]
assert content_item.documentation.value != ""

@pytest.mark.asyncio
async def test_completion_with_custom_module(client: LanguageClient):
"""
Test that completing takes into account extra modules
"""
test_file = 'template/projectxx.ncl'
test_file_content = """
let inputs = import "./nickel.lock.ncl" in
let organist = inputs.organist in
organist.OrganistExpression & organist.tools.direnv
& {
Schema,
config | Schema = {
},
}
| organist.modules.T
"""
test_uri = open_file(client, test_file, test_file_content)
completion_items = await complete(
client,
test_uri,
lsp.Position(line=8, character=0) # Empty line in the `config` record
)

labels = [item.label for item in completion_items]
assert "direnv" in labels

## No documentation for direnv yet
# content_item = [item for item in completion_items if item.label == "direnv"][0]
# assert content_item.documentation.value != ""
90 changes: 90 additions & 0 deletions tests/lsp/test_hover.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import pytest
import pytest_asyncio
from lsprotocol import types as lsp
from testlib import LanguageClient, open_file
from dataclasses import dataclass
from typing import Callable, List

async def hover(client: LanguageClient, file_uri: str, pos: lsp.Position):
"""
Trigger a hover in the given file at the given position
"""
results = await client.text_document_hover_async(
params=lsp.HoverParams(
text_document=lsp.TextDocumentIdentifier(file_uri),
position=pos,
)
)
return results

@dataclass
class HoverTest:
file: str
position: lsp.Position
checks: Callable[[lsp.Hover], List[bool]]


@pytest.mark.asyncio
async def test_hover_on_option(client: LanguageClient):
"""
Test that hovering over an option shows the right thing™
"""
test_file = 'template/projectxx.ncl'
test_file_content = """
let inputs = import "./nickel.lock.ncl" in
let organist = inputs.organist in
organist.OrganistExpression & organist.tools.direnv
& {
Schema,
config | Schema = {
files."foo.ncl".content = "1",
shells = organist.shells.Bash,
},
}
| organist.modules.T
"""
test_uri = open_file(client, test_file, test_file_content)

tests = [
HoverTest(
file=test_uri,
position=lsp.Position(line=8, character=11), # `files`
checks= lambda hover_info: [
lsp.MarkedString_Type1(language='nickel', value='Files') in hover_info.contents,
# Test that the contents contain a plain string (the documentation), and that it's non empty
next(content for content in hover_info.contents if type(content) == type("")) != "",
]
),
HoverTest(
file=test_uri,
position=lsp.Position(line=8, character=28), # `content`
checks= lambda hover_info: [
lsp.MarkedString_Type1(language='nickel', value='nix.derivation.NullOr nix.derivation.NixString') in hover_info.contents,
# Test that the contents contain a plain string (the documentation), and that it's non empty
next(content for content in hover_info.contents if type(content) == type("")) != "",
]
),
HoverTest(
file=test_uri,
position=lsp.Position(line=10, character=11), # `shells( =)`
checks= lambda hover_info: [
lsp.MarkedString_Type1(language='nickel', value='OrganistShells') in hover_info.contents,
# Test that the contents contain a plain string (the documentation), and that it's non empty
next(content for content in hover_info.contents if type(content) == type("")) != "",
]
),
]

for test in tests:
hover_info = await hover(
client,
test.file,
test.position,
)
print(hover_info.contents)
for check in test.checks(hover_info):
assert check


33 changes: 33 additions & 0 deletions tests/lsp/testlib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from pygls.lsp.client import BaseLanguageClient
from typing import Optional
import os
from lsprotocol import types as lsp

class LanguageClient(BaseLanguageClient):
pass

def open_file(client: LanguageClient, file_path: str, file_content: Optional[str] = None):
"""
Open the given file in the LSP.
If `file_content` is non `None`, then it will be used as the content sent to the LSP.
Otherwise, the actual file content will be read from disk.
"""
file_uri = f"file://{os.path.abspath(file_path)}"
actual_file_content = file_content
if file_content is None:
with open(file_path) as content:
actual_file_content = content.read()

client.text_document_did_open(
lsp.DidOpenTextDocumentParams(
text_document=lsp.TextDocumentItem(
uri=file_uri,
language_id="nickel",
version=1,
text=actual_file_content
)
)
)
return file_uri

0 comments on commit 86b627b

Please sign in to comment.