Skip to content

Commit 86b627b

Browse files
author
Théophane Hufschmitt
committed
Add tests for the lsp integration
Ensure that autocompletion works fine for simple things
1 parent a32698b commit 86b627b

File tree

7 files changed

+276
-0
lines changed

7 files changed

+276
-0
lines changed

Diff for: project.ncl

+21
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,27 @@ organist.OrganistExpression
7070
touch $out
7171
"%,
7272
},
73+
74+
lsp = {
75+
name = "organist-lsp-integration",
76+
version = "0.0",
77+
env.buildInputs = {
78+
nls = import_nix "nixpkgs#nls",
79+
python3 = import_nix "nixpkgs#python3",
80+
pygls = import_nix "nixpkgs#python3Packages.pygls",
81+
pytest = import_nix "nixpkgs#python3Packages.pytest",
82+
pytest-asyncio = import_nix "nixpkgs#python3Packages.pytest-asyncio",
83+
},
84+
env = {
85+
src = import_nix "self",
86+
phases = ["unpackPhase", "testPhase", "installPhase"],
87+
testPhase = nix-s%"
88+
cd tests/lsp
89+
pytest | tee $out
90+
"%,
91+
installPhase = "touch $out",
92+
},
93+
},
7394
},
7495

7596
flake.checks = import "tests/main.ncl",

Diff for: tests/lsp/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__pycache__

Diff for: tests/lsp/conftest.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import testlib
2+
import asyncio
3+
import pytest
4+
import pytest_asyncio
5+
from lsprotocol import types as lsp
6+
7+
@pytest_asyncio.fixture
8+
async def client():
9+
# Setup
10+
client = testlib.LanguageClient("organist-test-suite", "v1")
11+
await client.start_io("nls")
12+
response = await client.initialize_async(
13+
lsp.InitializeParams(
14+
capabilities=lsp.ClientCapabilities(),
15+
root_uri="."
16+
)
17+
)
18+
assert response is not None
19+
client.initialized(lsp.InitializedParams())
20+
return client
21+

Diff for: tests/lsp/template/nickel.lock.ncl

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ organist = import "../../../lib/organist.ncl" }

Diff for: tests/lsp/test_completion.py

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import pytest
2+
import pytest_asyncio
3+
from lsprotocol import types as lsp
4+
from testlib import LanguageClient, open_file
5+
6+
async def complete(client: LanguageClient, file_uri: str, pos: lsp.Position):
7+
"""
8+
Trigger an autocompletion in the given file at the given position
9+
"""
10+
results = await client.text_document_completion_async(
11+
params=lsp.CompletionParams(
12+
text_document=lsp.TextDocumentIdentifier(file_uri),
13+
position=pos,
14+
)
15+
)
16+
assert results is not None
17+
18+
if isinstance(results, lsp.CompletionList):
19+
items = results.items
20+
else:
21+
items = results
22+
return items
23+
24+
@pytest.mark.asyncio
25+
async def test_completion_at_toplevel(client):
26+
"""
27+
Test that getting an autocompletion at toplevel shows the available fields
28+
"""
29+
30+
test_file = 'template/project.ncl'
31+
with open('../../templates/default/project.ncl') as template_file:
32+
test_file_content = template_file.read()
33+
34+
test_uri = open_file(client, test_file, test_file_content)
35+
36+
completion_items = await complete(
37+
client,
38+
test_uri,
39+
lsp.Position(line=12, character=0) # Empty line in the `config` record
40+
)
41+
42+
labels = [item.label for item in completion_items]
43+
assert "files" in labels
44+
files_item = [item for item in completion_items if item.label == "files"][0]
45+
assert files_item.documentation.value != ""
46+
47+
@pytest.mark.asyncio
48+
async def test_completion_sub_field(client: LanguageClient):
49+
"""
50+
Test that completing on an option shows the available sub-options
51+
"""
52+
test_file = 'template/projectxx.ncl'
53+
test_file_content = """
54+
let inputs = import "./nickel.lock.ncl" in
55+
let organist = inputs.organist in
56+
57+
organist.OrganistExpression
58+
& {
59+
Schema,
60+
config | Schema = {
61+
files.foo.c
62+
},
63+
}
64+
| organist.modules.T
65+
"""
66+
test_uri = open_file(client, test_file, test_file_content)
67+
completion_items = await complete(
68+
client,
69+
test_uri,
70+
lsp.Position(line=8, character=17) # The `c` in `files.foo.c`
71+
)
72+
73+
labels = [item.label for item in completion_items]
74+
assert "content" in labels
75+
content_item = [item for item in completion_items if item.label == "content"][0]
76+
assert content_item.documentation.value != ""
77+
78+
@pytest.mark.asyncio
79+
async def test_completion_with_custom_module(client: LanguageClient):
80+
"""
81+
Test that completing takes into account extra modules
82+
"""
83+
test_file = 'template/projectxx.ncl'
84+
test_file_content = """
85+
let inputs = import "./nickel.lock.ncl" in
86+
let organist = inputs.organist in
87+
88+
organist.OrganistExpression & organist.tools.direnv
89+
& {
90+
Schema,
91+
config | Schema = {
92+
93+
},
94+
}
95+
| organist.modules.T
96+
"""
97+
test_uri = open_file(client, test_file, test_file_content)
98+
completion_items = await complete(
99+
client,
100+
test_uri,
101+
lsp.Position(line=8, character=0) # Empty line in the `config` record
102+
)
103+
104+
labels = [item.label for item in completion_items]
105+
assert "direnv" in labels
106+
107+
## No documentation for direnv yet
108+
# content_item = [item for item in completion_items if item.label == "direnv"][0]
109+
# assert content_item.documentation.value != ""

Diff for: tests/lsp/test_hover.py

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import pytest
2+
import pytest_asyncio
3+
from lsprotocol import types as lsp
4+
from testlib import LanguageClient, open_file
5+
from dataclasses import dataclass
6+
from typing import Callable, List
7+
8+
async def hover(client: LanguageClient, file_uri: str, pos: lsp.Position):
9+
"""
10+
Trigger a hover in the given file at the given position
11+
"""
12+
results = await client.text_document_hover_async(
13+
params=lsp.HoverParams(
14+
text_document=lsp.TextDocumentIdentifier(file_uri),
15+
position=pos,
16+
)
17+
)
18+
return results
19+
20+
@dataclass
21+
class HoverTest:
22+
file: str
23+
position: lsp.Position
24+
checks: Callable[[lsp.Hover], List[bool]]
25+
26+
27+
@pytest.mark.asyncio
28+
async def test_hover_on_option(client: LanguageClient):
29+
"""
30+
Test that hovering over an option shows the right thing™
31+
"""
32+
test_file = 'template/projectxx.ncl'
33+
test_file_content = """
34+
let inputs = import "./nickel.lock.ncl" in
35+
let organist = inputs.organist in
36+
37+
organist.OrganistExpression & organist.tools.direnv
38+
& {
39+
Schema,
40+
config | Schema = {
41+
files."foo.ncl".content = "1",
42+
43+
shells = organist.shells.Bash,
44+
},
45+
}
46+
| organist.modules.T
47+
"""
48+
test_uri = open_file(client, test_file, test_file_content)
49+
50+
tests = [
51+
HoverTest(
52+
file=test_uri,
53+
position=lsp.Position(line=8, character=11), # `files`
54+
checks= lambda hover_info: [
55+
lsp.MarkedString_Type1(language='nickel', value='Files') in hover_info.contents,
56+
# Test that the contents contain a plain string (the documentation), and that it's non empty
57+
next(content for content in hover_info.contents if type(content) == type("")) != "",
58+
]
59+
),
60+
HoverTest(
61+
file=test_uri,
62+
position=lsp.Position(line=8, character=28), # `content`
63+
checks= lambda hover_info: [
64+
lsp.MarkedString_Type1(language='nickel', value='nix.derivation.NullOr nix.derivation.NixString') in hover_info.contents,
65+
# Test that the contents contain a plain string (the documentation), and that it's non empty
66+
next(content for content in hover_info.contents if type(content) == type("")) != "",
67+
]
68+
),
69+
HoverTest(
70+
file=test_uri,
71+
position=lsp.Position(line=10, character=11), # `shells( =)`
72+
checks= lambda hover_info: [
73+
lsp.MarkedString_Type1(language='nickel', value='OrganistShells') in hover_info.contents,
74+
# Test that the contents contain a plain string (the documentation), and that it's non empty
75+
next(content for content in hover_info.contents if type(content) == type("")) != "",
76+
]
77+
),
78+
]
79+
80+
for test in tests:
81+
hover_info = await hover(
82+
client,
83+
test.file,
84+
test.position,
85+
)
86+
print(hover_info.contents)
87+
for check in test.checks(hover_info):
88+
assert check
89+
90+

Diff for: tests/lsp/testlib.py

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from pygls.lsp.client import BaseLanguageClient
2+
from typing import Optional
3+
import os
4+
from lsprotocol import types as lsp
5+
6+
class LanguageClient(BaseLanguageClient):
7+
pass
8+
9+
def open_file(client: LanguageClient, file_path: str, file_content: Optional[str] = None):
10+
"""
11+
Open the given file in the LSP.
12+
13+
If `file_content` is non `None`, then it will be used as the content sent to the LSP.
14+
Otherwise, the actual file content will be read from disk.
15+
"""
16+
file_uri = f"file://{os.path.abspath(file_path)}"
17+
actual_file_content = file_content
18+
if file_content is None:
19+
with open(file_path) as content:
20+
actual_file_content = content.read()
21+
22+
client.text_document_did_open(
23+
lsp.DidOpenTextDocumentParams(
24+
text_document=lsp.TextDocumentItem(
25+
uri=file_uri,
26+
language_id="nickel",
27+
version=1,
28+
text=actual_file_content
29+
)
30+
)
31+
)
32+
return file_uri
33+

0 commit comments

Comments
 (0)