Skip to content

Commit c94da49

Browse files
committed
Merge branch 'topic/default-config-file' into 'master'
Add tests for the concept of the base configuration See merge request eng/ide/ada_language_server!1849
2 parents 24d78a6 + 12fa036 commit c94da49

File tree

6 files changed

+310
-7
lines changed

6 files changed

+310
-7
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"$schema": "../../../integration/vscode/ada/schemas/als-settings-schema.json",
3+
"scenarioVariables": {
4+
"Var": "value-from-config-file"
5+
}
6+
}

testsuite/ada_lsp/config_base/pkg.ads

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- We need a source file in the project to trigger indexing
2+
package Pkg is
3+
pragma Pure;
4+
end Pkg;

testsuite/ada_lsp/config_base/prj.gpr

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
project Prj is
2+
for Object_Dir use external("Var", "value-from-prj");
3+
end Prj;

testsuite/ada_lsp/config_base/test.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
"""
2+
The goal of these tests is to check that ALS remembers a base configuration to which it
3+
can revert settings when receiving null values in didChangeConfiguration notifiactions.
4+
5+
It's important to check that the base configuration can be obtained both when there are
6+
config files (.als.json or User config file), and when there aren't - i.e. when settings
7+
are sent with the initialize request, or with the first didChangeConfiguration
8+
notification.
9+
10+
To observe the active configuration, we use a project file where the object directory is
11+
defined via an external scenario variable and we use the als-object-dir command to query
12+
that.
13+
14+
We also need a way to wait until the project has finished loading in order to query its
15+
object dir. To do that we add at least one source file and await the end of indexing.
16+
17+
The scenarios we want to test:
18+
19+
1. There is a config file. Then the initialize request also provides settings, but the
20+
base config should be the config file.
21+
22+
2. There are no config files, and the initialize request contains settings with non-null
23+
values that should be the base config.
24+
25+
3. There are no config files, and the initialize request contains settings with all null
26+
values. This is the case when no ada.* settings are set in VS Code. The config at
27+
initialize should be considered the base config.
28+
29+
4. There are no config files. The initialize request doesn't contain any settings. The
30+
first didChangeConfig notification should be considered as the base config.
31+
"""
32+
33+
import os
34+
from lsprotocol.types import ClientCapabilities, InitializeParams
35+
from drivers.pylsp import (
36+
URI,
37+
ALSClientServerConfig,
38+
ALSLanguageClient,
39+
ALSSettings,
40+
assertEqual,
41+
awaitIndexingEnd,
42+
test,
43+
)
44+
45+
46+
@test(
47+
config=ALSClientServerConfig(
48+
server_command=[
49+
os.environ.get("ALS", "ada_language_server"),
50+
"--config",
51+
"my_config.json",
52+
],
53+
),
54+
initialize=False,
55+
)
56+
async def test1(lsp: ALSLanguageClient) -> None:
57+
# Even if we send settings in the initialize request, the config file should
58+
# constitue the base config.
59+
settings: ALSSettings = {
60+
"scenarioVariables": {"Var": "value-from-init"},
61+
}
62+
await lsp.initialize_session(
63+
InitializeParams(
64+
ClientCapabilities(),
65+
root_uri=URI(os.getcwd()),
66+
initialization_options={"ada": settings},
67+
)
68+
)
69+
# Because no project file was set, we need a didOpen to load the project
70+
lsp.didOpenVirtual()
71+
await awaitIndexingEnd(lsp)
72+
assertEqual(await lsp.getObjDirBasename(), "value-from-init")
73+
74+
# Now let's change the settings
75+
lsp.didChangeConfig({"scenarioVariables": {"Var": "new-value"}})
76+
await awaitIndexingEnd(lsp)
77+
assertEqual(await lsp.getObjDirBasename(), "new-value")
78+
79+
# Now we send a null value to revert to the base config which should be the config
80+
# file, not the initialize request.
81+
lsp.didChangeConfig({"scenarioVariables": None})
82+
await awaitIndexingEnd(lsp)
83+
assertEqual(await lsp.getObjDirBasename(), "value-from-config-file")
84+
85+
86+
@test(initialize=False)
87+
async def test2(lsp: ALSLanguageClient) -> None:
88+
# In this scenario there are no config files. The settings in the initialize request
89+
# should constitute the base config.
90+
settings: ALSSettings = {
91+
"scenarioVariables": {"Var": "value-from-init"},
92+
}
93+
await lsp.initialize_session(
94+
InitializeParams(
95+
ClientCapabilities(),
96+
root_uri=URI(os.getcwd()),
97+
initialization_options={"ada": settings},
98+
)
99+
)
100+
# Because no project file was set, we need a didOpen to load the project
101+
lsp.didOpenVirtual()
102+
await awaitIndexingEnd(lsp)
103+
assertEqual(await lsp.getObjDirBasename(), "value-from-init")
104+
105+
# Now let's change the settings and revert back to see if we revert to the right
106+
# value.
107+
lsp.didChangeConfig({"scenarioVariables": {"Var": "new-value"}})
108+
await awaitIndexingEnd(lsp)
109+
assertEqual(await lsp.getObjDirBasename(), "new-value")
110+
111+
lsp.didChangeConfig({"scenarioVariables": None})
112+
await awaitIndexingEnd(lsp)
113+
assertEqual(await lsp.getObjDirBasename(), "value-from-init")
114+
115+
116+
@test(initialize=False)
117+
async def test3(lsp: ALSLanguageClient) -> None:
118+
# This scenario reproduces what happens in VS Code when there are no ada.* settings
119+
#
120+
# The settings in the initialize request are null
121+
settings: ALSSettings = {
122+
"scenarioVariables": None,
123+
}
124+
await lsp.initialize_session(
125+
InitializeParams(
126+
ClientCapabilities(),
127+
root_uri=URI(os.getcwd()),
128+
initialization_options={"ada": settings},
129+
)
130+
)
131+
# Because no project file was set, we need a didOpen to load the project
132+
lsp.didOpenVirtual()
133+
await awaitIndexingEnd(lsp)
134+
# No value was provided for the scenario variable, so we should get the default
135+
# value defined in the project.
136+
assertEqual(await lsp.getObjDirBasename(), "value-from-prj")
137+
138+
# Now let's change the settings and revert back to see if we revert to the right
139+
# value.
140+
lsp.didChangeConfig({"scenarioVariables": {"Var": "new-value"}})
141+
await awaitIndexingEnd(lsp)
142+
assertEqual(await lsp.getObjDirBasename(), "new-value")
143+
144+
lsp.didChangeConfig({"scenarioVariables": None})
145+
await awaitIndexingEnd(lsp)
146+
assertEqual(await lsp.getObjDirBasename(), "value-from-prj")
147+
148+
149+
@test(initialize=False)
150+
async def test4(lsp: ALSLanguageClient) -> None:
151+
# There is no config file
152+
await lsp.initialize_session(
153+
InitializeParams(
154+
ClientCapabilities(),
155+
root_uri=URI(os.getcwd()),
156+
# initialize has no settings
157+
)
158+
)
159+
# Because no project file was set, we need a didOpen to load the project
160+
lsp.didOpenVirtual()
161+
await awaitIndexingEnd(lsp)
162+
# No value was provided for the scenario variable, so we should get the default
163+
# value defined in the project.
164+
assertEqual(await lsp.getObjDirBasename(), "value-from-prj")
165+
166+
# Now let's send the first didChangeConfiguration. This should constitute the base
167+
# config.
168+
lsp.didChangeConfig(
169+
{"scenarioVariables": {"Var": "value-from-first-config-change"}}
170+
)
171+
await awaitIndexingEnd(lsp)
172+
assertEqual(await lsp.getObjDirBasename(), "value-from-first-config-change")
173+
174+
# Now we change to another value, and revert with a null value.
175+
lsp.didChangeConfig({"scenarioVariables": {"Var": "new-value"}})
176+
await awaitIndexingEnd(lsp)
177+
assertEqual(await lsp.getObjDirBasename(), "new-value")
178+
179+
lsp.didChangeConfig({"scenarioVariables": None})
180+
await awaitIndexingEnd(lsp)
181+
assertEqual(await lsp.getObjDirBasename(), "value-from-first-config-change")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
driver: pylsp

testsuite/drivers/pylsp.py

Lines changed: 115 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,16 @@
1111
import urllib
1212
import urllib.parse
1313
from pathlib import Path
14-
from typing import Any, Awaitable, Callable, Sequence, Type
14+
from typing import (
15+
Any,
16+
Awaitable,
17+
Callable,
18+
Literal,
19+
Sequence,
20+
Type,
21+
TypedDict,
22+
)
23+
import uuid
1524
import warnings
1625

1726
import attrs
@@ -36,6 +45,7 @@
3645
CallHierarchyItem,
3746
CallHierarchyPrepareParams,
3847
ClientCapabilities,
48+
DidChangeConfigurationParams,
3949
DidOpenTextDocumentParams,
4050
ExecuteCommandParams,
4151
InitializeParams,
@@ -201,8 +211,45 @@ def connection_made(self, transport: asyncio.Transport): # type: ignore
201211
return super().connection_made(transport)
202212

203213

214+
class OnTypeFormattingSetting(TypedDict):
215+
indentOnly: bool
216+
217+
218+
class ALSSettings(
219+
TypedDict,
220+
# This indicates that the dictionary keys can be omitted, they are not required to
221+
# appear
222+
total=False,
223+
):
224+
"""This class helps create a dictionary of ALS settings. It has to be manually
225+
updated when new settings are added.
226+
227+
So if you see a missing setting that you need, consider adding it.
228+
"""
229+
230+
defaultCharset: str | None
231+
displayMethodAncestryOnNavigation: bool | None
232+
documentationStyle: Literal["gnat", "leading"] | None
233+
enableDiagnostics: bool | None
234+
enableIndexing: bool | None
235+
foldComments: bool | None
236+
followSymlinks: bool | None
237+
insertWithClauses: bool | None
238+
logThreshold: int | None
239+
namedNotationThreshold: int | None
240+
onTypeFormatting: OnTypeFormattingSetting | None
241+
projectDiagnostics: bool | None
242+
projectFile: str | None
243+
relocateBuildTree: str | None
244+
renameInComments: bool | None
245+
rootDir: str | None
246+
scenarioVariables: dict[str, str] | None
247+
useCompletionSnippets: bool | None
248+
useGnatformat: bool | None
249+
250+
204251
class ALSLanguageClient(LanguageClient):
205-
"""This class provides methods to communicate with a language server."""
252+
"""This class provides methods to communicate with the Ada Language Server."""
206253

207254
def __init__(
208255
self,
@@ -215,6 +262,63 @@ def __init__(
215262

216263
super().__init__(*args, configuration=configuration, **kwargs)
217264

265+
async def getObjectDir(self) -> str | None:
266+
"""Send the "als-object-dir" command to obtain the object dir of the currently
267+
loaded project.
268+
"""
269+
return await self.workspace_execute_command_async(
270+
ExecuteCommandParams("als-object-dir")
271+
)
272+
273+
async def getObjDirBasename(self) -> str | None:
274+
"""Send the "als-object-dir" command to obtain the object dir of the currently
275+
loaded project, and return its basename.
276+
"""
277+
obj_dir = await self.getObjectDir()
278+
if obj_dir is not None:
279+
return os.path.basename(obj_dir)
280+
else:
281+
return None
282+
283+
def didChangeConfig(self, settings: ALSSettings) -> None:
284+
"""Send a workspace/didChangeConfiguration notification with as set of ALS
285+
settings.
286+
"""
287+
self.workspace_did_change_configuration(
288+
DidChangeConfigurationParams(settings={"ada": settings})
289+
)
290+
291+
def didOpenVirtual(
292+
self, uri: str | None = None, language_id="ada", version=0, text: str = ""
293+
) -> str:
294+
"""Send a didOpen notification for a file that doesn't exist on disk.
295+
296+
If the `uri` parameter is omitted, a random one is generated
297+
automatically with a `.ads` extension.
298+
299+
:param uri: the URI of the file. If None, that will be automatically
300+
generated and returned as a result of the call.
301+
:param language_id: the language_id parameter of the LSP notification.
302+
:param version: the version parameter of the LSP notification. Defaults
303+
to 0.
304+
:param text: the text parameter of the LSP notification.
305+
306+
:return: the URI of the document
307+
"""
308+
if uri is None:
309+
path = str(uuid.uuid4())
310+
if language_id == "ada":
311+
path += ".ads"
312+
else:
313+
path += "." + language_id
314+
uri = URI(path)
315+
316+
self.text_document_did_open(
317+
DidOpenTextDocumentParams(TextDocumentItem(uri, language_id, version, text))
318+
)
319+
320+
return uri
321+
218322

219323
def als_client_factory() -> ALSLanguageClient:
220324
"""This function is an ugly copy-paste of pytest_lsp.make_test_lsp_client. It is
@@ -647,13 +751,16 @@ def to_str(item: tuple[str, int]):
647751

648752
async def awaitIndexingEnd(lsp: LanguageClient):
649753
"""Wait until the ALS finishes indexing."""
754+
LOG.info("Awaiting indexing start and end")
755+
650756
indexing_progress = None
651757
while indexing_progress is None:
652758
await asyncio.sleep(0.2)
653-
LOG.info(
654-
"Awaiting indexing progress - lsp.progress_reports = %s",
655-
lsp.progress_reports,
656-
)
759+
if args.verbose >= 2:
760+
LOG.debug(
761+
"Awaiting indexing progress - lsp.progress_reports = %s",
762+
lsp.progress_reports,
763+
)
657764
indexing_progress = next(
658765
(prog for prog in lsp.progress_reports if "indexing" in str(prog)),
659766
None,
@@ -664,7 +771,8 @@ async def awaitIndexingEnd(lsp: LanguageClient):
664771
last_progress = lsp.progress_reports[indexing_progress][-1]
665772
while not isinstance(last_progress, WorkDoneProgressEnd):
666773
await asyncio.sleep(0.2)
667-
LOG.info("Waiting for indexing end - last_progress = %s", last_progress)
774+
if args.verbose >= 2:
775+
LOG.debug("Waiting for indexing end - last_progress = %s", last_progress)
668776
last_progress = lsp.progress_reports[indexing_progress][-1]
669777

670778
LOG.info("Received indexing end message")

0 commit comments

Comments
 (0)