11import logging
22import os
3+ import re
34import subprocess
45import sys
56import tempfile
67import time
8+ import shutil
79import json
810from itertools import chain
911from pathlib import Path
12+ from typing import Tuple
1013
1114from utils import Plugin , configure_git , enumerate_plugins
1215
2225pip_opts = ["-qq" ]
2326
2427
25- def prepare_env (p : Plugin , directory : Path , env : dict , workflow : str ) -> bool :
26- """Returns whether we can run at all. Raises error if preparing failed."""
27- subprocess .check_call (["python3" , "-m" , "venv" , "--clear" , directory ])
28- os .environ ["PATH" ] += f":{ directory } "
28+ def prepare_env (p : Plugin , workflow : str ) -> Tuple [dict , tempfile .TemporaryDirectory ]:
29+ """Returns the environment and the temporary directory object."""
30+ vdir = None
31+ env = os .environ .copy ()
32+ directory = p .path / ".venv"
33+
34+ if p .framework != "uv" :
35+ # Create a temporary directory for virtualenv
36+ vdir = tempfile .TemporaryDirectory ()
37+ directory = Path (vdir .name )
38+ bin_path = directory / "bin"
39+
40+ env .update (
41+ {
42+ # Need to customize PATH so lightningd can find the correct python3
43+ "PATH" : f"{ bin_path } :{ os .environ ['PATH' ]} " ,
44+ # Some plugins require a valid locale to be set
45+ "LC_ALL" : "C.UTF-8" ,
46+ "LANG" : "C.UTF-8" ,
47+ }
48+ )
2949
30- if p .framework == "pip" :
31- return prepare_env_pip (p , directory , workflow )
32- elif p .framework == "poetry" :
33- return prepare_env_poetry (p , directory , workflow )
34- elif p .framework == "generic" :
35- return prepare_generic (p , directory , env , workflow )
36- else :
37- raise ValueError (f"Unknown framework { p .framework } " )
50+ # Create the virtualenv
51+ subprocess .check_call (["python3" , "-m" , "venv" , "--clear" , str (directory )])
52+
53+ if p .framework == "pip" :
54+ if not prepare_env_pip (p , directory , workflow ):
55+ raise ValueError (f"Failed to prepare pip environment for { p .name } " )
56+ elif p .framework == "poetry" :
57+ if not prepare_env_poetry (p , directory , workflow ):
58+ raise ValueError (f"Failed to prepare poetry environment for { p .name } " )
59+ elif p .framework == "generic" :
60+ if not prepare_generic (p , directory , env , workflow ):
61+ raise ValueError (f"Failed to prepare generic environment for { p .name } " )
62+ else :
63+ raise ValueError (f"Unknown framework { p .framework } " )
64+
65+ setup_path = p .path / "tests" / "setup.sh"
66+ if os .path .exists (setup_path ):
67+ print (f"Running setup script from { setup_path } " )
68+ subprocess .check_call (
69+ ["bash" , setup_path , f"TEST_DIR={ directory } " ],
70+ env = env ,
71+ stderr = subprocess .STDOUT ,
72+ )
73+
74+ return env , vdir
3875
3976
4077def prepare_env_poetry (p : Plugin , directory : Path , workflow : str ) -> bool :
@@ -67,19 +104,35 @@ def prepare_env_poetry(p: Plugin, directory: Path, workflow: str) -> bool:
67104 logging .info (
68105 f"Exporting poetry { poetry } dependencies from { p .details ['pyproject' ]} "
69106 )
70- subprocess .check_call (
71- [
72- poetry ,
73- "export" ,
74- "--with=dev" ,
75- "--without-hashes" ,
76- "-f" ,
77- "requirements.txt" ,
78- "--output" ,
79- "requirements.txt" ,
80- ],
81- cwd = workdir ,
82- )
107+
108+ try :
109+ subprocess .check_call (
110+ [
111+ poetry ,
112+ "export" ,
113+ "--with=dev" ,
114+ "--without-hashes" ,
115+ "-f" ,
116+ "requirements.txt" ,
117+ "--output" ,
118+ "requirements.txt" ,
119+ ],
120+ cwd = workdir ,
121+ )
122+ except Exception as e :
123+ logging .info (f"Poetry export failed: { e } , trying without dev deps" )
124+ subprocess .check_call (
125+ [
126+ poetry ,
127+ "export" ,
128+ "--without-hashes" ,
129+ "-f" ,
130+ "requirements.txt" ,
131+ "--output" ,
132+ "requirements.txt" ,
133+ ],
134+ cwd = workdir ,
135+ )
83136
84137 subprocess .check_call (
85138 [
@@ -95,7 +148,7 @@ def prepare_env_poetry(p: Plugin, directory: Path, workflow: str) -> bool:
95148 if workflow == "nightly" :
96149 install_dev_pyln_testing (pip3 )
97150 else :
98- install_pyln_testing (pip3 )
151+ install_pyln_testing (pip3 , workflow )
99152
100153 subprocess .check_call ([pip3 , "freeze" ])
101154 return True
@@ -122,7 +175,7 @@ def prepare_env_pip(p: Plugin, directory: Path, workflow: str) -> bool:
122175 if workflow == "nightly" :
123176 install_dev_pyln_testing (pip_path )
124177 else :
125- install_pyln_testing (pip_path )
178+ install_pyln_testing (pip_path , workflow )
126179
127180 subprocess .check_call ([pip_path , "freeze" ])
128181 return True
@@ -143,23 +196,14 @@ def prepare_generic(p: Plugin, directory: Path, env: dict, workflow: str) -> boo
143196 if workflow == "nightly" :
144197 install_dev_pyln_testing (pip_path )
145198 else :
146- install_pyln_testing (pip_path )
147-
148- if p .details ["setup" ].exists ():
149- print (f"Running setup script from { p .details ['setup' ]} " )
150- subprocess .check_call (
151- ["bash" , p .details ["setup" ], f"TEST_DIR={ directory } " ],
152- env = env ,
153- stderr = subprocess .STDOUT ,
154- )
199+ install_pyln_testing (pip_path , workflow )
155200
156201 subprocess .check_call ([pip_path , "freeze" ])
157202 return True
158203
159204
160- def install_pyln_testing (pip_path ):
205+ def install_pyln_testing (pip_path , workflow : str ):
161206 # Many plugins only implicitly depend on pyln-testing, so let's help them
162- cln_path = os .environ ["CLN_PATH" ]
163207
164208 # Install pytest (eventually we'd want plugin authors to include
165209 # it in their requirements-dev.txt, but for now let's help them a
@@ -174,13 +218,15 @@ def install_pyln_testing(pip_path):
174218 stderr = subprocess .STDOUT ,
175219 )
176220
221+ pyln_version = re .sub (r'\.0(\d+)' , r'.\1' , workflow )
222+
177223 subprocess .check_call (
178224 [
179225 pip_path ,
180226 "install" ,
181227 * pip_opts ,
182- cln_path + "/contrib/ pyln-client" ,
183- cln_path + "/contrib/ pyln-testing" ,
228+ f" pyln-client== { pyln_version } " ,
229+ f" pyln-testing== { pyln_version } " ,
184230 "MarkupSafe>=2.0" ,
185231 "itsdangerous>=2.0" ,
186232 ],
@@ -223,44 +269,29 @@ def run_one(p: Plugin, workflow: str) -> bool:
223269 )
224270 print ("::group::{p.name}" .format (p = p ))
225271
226- # Create a virtual env
227- vdir = tempfile .TemporaryDirectory ()
228- vpath = Path (vdir .name )
229-
230- bin_path = vpath / "bin"
231- pytest_path = vpath / "bin" / "pytest"
232-
233- env = os .environ .copy ()
234- env .update (
235- {
236- # Need to customize PATH so lightningd can find the correct python3
237- "PATH" : "{}:{}" .format (bin_path , os .environ ["PATH" ]),
238- # Some plugins require a valid locale to be set
239- "LC_ALL" : "C.UTF-8" ,
240- "LANG" : "C.UTF-8" ,
241- }
242- )
243-
244272 try :
245- if not prepare_env (p , vpath , env , workflow ):
246- # Skipping is counted as a success
247- return True
273+ env , tmp_dir = prepare_env (p , workflow )
248274 except Exception as e :
249275 print (f"Error creating test environment: { e } " )
250276 print ("::endgroup::" )
251277 return False
252278
253- logging .info (f"Virtualenv at { vpath } " )
254-
255279 cmd = [
256- str (pytest_path ),
257280 "-vvv" ,
258281 "--timeout=600" ,
259282 "--timeout-method=thread" ,
260283 "--color=yes" ,
261284 "-n=5" ,
262285 ]
263286
287+ if p .framework == "uv" :
288+ cmd = ["uv" , "run" , "pytest" ] + cmd
289+ else :
290+ pytest_path = shutil .which ("pytest" , path = env ["PATH" ])
291+ if not pytest_path :
292+ raise RuntimeError (f"pytest not found in PATH:{ env ['PATH' ]} " )
293+ cmd = [pytest_path ] + cmd
294+
264295 logging .info (f"Running `{ ' ' .join (cmd )} ` in directory { p .path .resolve ()} " )
265296 try :
266297 subprocess .check_call (
0 commit comments