Skip to content

Commit c5eead8

Browse files
committed
python test driver: on Windows, spawn a separate Python
This is needed so we can properly remove the test dir once the test is done, without risking that the Python interpreter has a handle on files there. Slightly improve the semantics for the wait factor. Improve error reporting slightly. Fix creation of URIs with respect to drive letters. Follow-up on #1462
1 parent 88e5b2d commit c5eead8

File tree

4 files changed

+83
-34
lines changed

4 files changed

+83
-34
lines changed

testsuite/drivers/lsp_ada_requests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# TODO: use a library such as pytest-lsp to support most requests
99

1010

11-
def initialize(workspacefolder="."):
11+
def initialize(workspacefolder=URI(".")):
1212
"""This mimics what vs code sends at the moment"""
1313
return LSPMessage(
1414
{

testsuite/drivers/lsp_python_driver.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,13 @@
7373
DEBUG_MODE = False
7474

7575

76-
def set_debug_mode(mode : bool = True):
76+
def set_debug_mode(mode: bool = True):
7777
"""Set the debug mode."""
7878
global DEBUG_MODE
7979
DEBUG_MODE = mode
8080

8181

82-
def set_wait_factor(factor : float):
82+
def set_wait_factor(factor: float):
8383
"""Set the wait factor."""
8484
global RLIMIT_SECONDS
8585
global RESPONSE_TIMEOUT
@@ -93,7 +93,7 @@ def __init__(
9393
self,
9494
cl: str | list[str] | None = "ada_language_server",
9595
working_dir: str = ".",
96-
env: dict | None = None
96+
env: dict | None = None,
9797
):
9898
"""Launch an LSP server and provide a way to send messages to it.
9999
cl is the command line to launch the LSP server.
@@ -119,8 +119,9 @@ def __init__(
119119
self.wd = working_dir
120120

121121
# Kill the server when we reach this time
122-
self.kill_me_at = time.time() + RLIMIT_SECONDS * (
123-
1 + os.environ.get("ALS_WAIT_FACTOR", 0))
122+
self.kill_me_at = time.time() + int(
123+
RLIMIT_SECONDS * os.environ.get("ALS_WAIT_FACTOR", 1.0)
124+
)
124125

125126
# This contains either None or a timestamp. If a timestamp,
126127
# then the process should be killed after 2 seconds after this.
@@ -148,7 +149,6 @@ def __init__(
148149
if DEBUG_MODE:
149150
self.debug_here()
150151

151-
152152
def kill_task(self):
153153
while True:
154154
time.sleep(0.2)
@@ -178,7 +178,7 @@ def receive_task(self):
178178
break
179179
if not header.startswith("Content-Length:"):
180180
continue
181-
length = int(header[len("Content-Length:"):])
181+
length = int(header[len("Content-Length:") :])
182182

183183
# TODO: Add support for "Content-Type" header
184184

@@ -199,9 +199,7 @@ def receive_task(self):
199199

200200
time.sleep(0.01)
201201

202-
def send(
203-
self, message: LSPMessage, expect_response=True
204-
) -> LSPResponse | None:
202+
def send(self, message: LSPMessage, expect_response=True) -> LSPResponse | None:
205203
"""Send a message to the LSP server.
206204
If expect_response is True, wait for a response for at most timeout seconds.
207205
Return the response if any, or None if no response
@@ -257,12 +255,13 @@ def shutdown(self):
257255
# If errors were found, capture a replay file.
258256
# Compute the replay dir based on this file
259257
replay_dir = os.path.join(
260-
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
261-
"replays"
258+
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "replays"
262259
)
263260
# Create the directory if it doesn't exist
264261
os.makedirs(replay_dir, exist_ok=True)
265-
replay_file = os.path.join(replay_dir, os.path.basename(self.wd) + "_replay.txt")
262+
replay_file = os.path.join(
263+
replay_dir, os.path.basename(self.wd) + "_replay.txt"
264+
)
266265
self.errors.append(f"Replay file written to {replay_file}")
267266

268267
# Write a "replay.txt" replay file

testsuite/drivers/lsp_types.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def assertLocationsList(self, expected: list[(str, int)]):
127127
with line_number being 1-based.
128128
"""
129129
if not isinstance(self.from_dict, list):
130-
raise AssertionError("The response does not contain a list")
130+
raise AssertionError(f"Expected a list, but received: {self.from_dict}")
131131

132132
# Extract the locations from the response
133133
locations = []
@@ -161,5 +161,10 @@ def URI(filename: str) -> str:
161161
"""Return a URI for the given filename."""
162162
# Get the absolute path for filename
163163
abs_path = os.path.abspath(filename)
164+
# Replace the backslashes by slashes
165+
abs_path = abs_path.replace("\\", "/")
166+
# Replace "C:" by "/C%3A" for Windows paths
167+
if abs_path[1] == ":":
168+
abs_path = "/" + abs_path[0] + "%3A" + abs_path[2:]
164169
# Convert it to a URI
165170
return "file://" + abs_path

testsuite/drivers/python_driver.py

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import importlib.util
22
import inspect
33
import os
4+
import sys
45

56
from drivers import ALSTestDriver
67
from drivers.lsp_python_driver import set_debug_mode, set_wait_factor
@@ -31,25 +32,69 @@ def run(self):
3132

3233
# If there is a "test.py", evaluate it
3334
if os.path.exists(os.path.join(wd, "test.py")):
34-
# Load test.py as a module
35-
python_file = os.path.join(wd, "test.py")
36-
spec = importlib.util.spec_from_file_location("module.name", python_file)
37-
module = importlib.util.module_from_spec(spec)
38-
spec.loader.exec_module(module)
39-
40-
# Look for functions with the decorator @simple_test and run them
41-
errors = [
42-
f"no function with @simple_test or @complex_test found in {python_file}"
35+
# Spawn a separate executable for the test. The reason
36+
# for this is that we want to be able to remove the
37+
# test directory after the test is done, but we can't
38+
# do that under Windows if this Python driver still has
39+
# a handle on the module file.
40+
cmd = [
41+
sys.executable,
42+
__file__,
43+
os.path.join(wd, "test.py"),
44+
self.env.als,
45+
self.env.als_home,
46+
str(self.env.wait_factor),
4347
]
44-
45-
for _, obj in inspect.getmembers(module):
46-
if inspect.isfunction(obj) and (
47-
hasattr(obj, "simple_test") or hasattr(obj, "complex_test")
48-
):
49-
errors = obj(wd)
50-
51-
if len(errors) > 0:
52-
self.result.log += "\n".join(errors)
53-
raise TestAbortWithFailure("Test returned errors")
48+
self.shell(
49+
cmd,
50+
cwd=wd,
51+
env={
52+
"PYTHONPATH": os.path.dirname(os.path.dirname(__file__)),
53+
},
54+
ignore_environ=False,
55+
)
5456
else:
5557
raise TestAbortWithFailure("No test.py found in %s" % wd)
58+
59+
60+
def run_a_module(
61+
test_py_path: str, als: str, als_home: str, wait_factor: float, debug: bool
62+
):
63+
if debug:
64+
set_debug_mode(True)
65+
66+
set_wait_factor(wait_factor)
67+
68+
wd = os.path.dirname(test_py_path)
69+
70+
# Load test.py as a module
71+
spec = importlib.util.spec_from_file_location("module.name", test_py_path)
72+
module = importlib.util.module_from_spec(spec)
73+
spec.loader.exec_module(module)
74+
75+
# Look for functions with the decorator @simple_test and run them
76+
errors = [f"no function with @simple_test or @complex_test found in {test_py_path}"]
77+
78+
for _, obj in inspect.getmembers(module):
79+
if inspect.isfunction(obj) and (
80+
hasattr(obj, "simple_test") or hasattr(obj, "complex_test")
81+
):
82+
errors = obj(wd)
83+
84+
if len(errors) > 0:
85+
print("\n".join(errors))
86+
sys.exit(1)
87+
sys.exit(0)
88+
89+
90+
if __name__ == "__main__":
91+
import argparse
92+
93+
parser = argparse.ArgumentParser(description="Run a Python test driver")
94+
parser.add_argument("test", help="The test to load")
95+
parser.add_argument("als", help="The ALS program to run")
96+
parser.add_argument("als_home", help="The ALS home directory")
97+
parser.add_argument("wait_factor", type=float, help="The wait factor")
98+
parser.add_argument("--debug", action="store_true", help="Enable debug mode")
99+
args = parser.parse_args()
100+
run_a_module(args.test, args.als, args.als_home, args.wait_factor, args.debug)

0 commit comments

Comments
 (0)