Skip to content

Commit aec6501

Browse files
daywalker90chrisguida
authored andcommitted
CI: update main and add uv support
1 parent 1c1a539 commit aec6501

File tree

3 files changed

+124
-128
lines changed

3 files changed

+124
-128
lines changed

.ci/test.py

Lines changed: 95 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import logging
22
import os
3+
import re
34
import subprocess
45
import sys
56
import tempfile
67
import time
8+
import shutil
79
import json
810
from itertools import chain
911
from pathlib import Path
12+
from typing import Tuple
1013

1114
from utils import Plugin, configure_git, enumerate_plugins
1215

@@ -22,19 +25,53 @@
2225
pip_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

4077
def 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(

.ci/utils.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,21 @@ def enumerate_plugins(basedir: Path) -> Generator[Plugin, None, None]:
5858
plugins = list(
5959
[x for x in basedir.iterdir() if x.is_dir() and x.name not in exclude]
6060
)
61-
# Explicitly detect rust in case there's python testing code
62-
rust_plugins = [x for x in plugins if (x / Path("Cargo.toml")).exists()]
63-
print(f"Rust plugins: {list_plugins(rust_plugins)}")
6461

65-
pip_pytest = [x for x in plugins if (x / Path("requirements.txt")).exists() and x not in rust_plugins]
62+
pip_pytest = [x for x in plugins if (x / Path("requirements.txt")).exists()]
6663
print(f"Pip plugins: {list_plugins(pip_pytest)}")
6764

68-
poetry_pytest = [x for x in plugins if (x / Path("pyproject.toml")).exists() and x not in rust_plugins]
65+
uv_pytest = [x for x in plugins if (x / Path("uv.lock")).exists()]
66+
print(f"Uv plugins: {list_plugins(uv_pytest)}")
67+
68+
# Don't double detect plugins migrating to uv
69+
poetry_pytest = [x for x in plugins if (x / Path("poetry.lock")).exists() and x not in uv_pytest]
6970
print(f"Poetry plugins: {list_plugins(poetry_pytest)}")
7071

7172
generic_plugins = [
72-
x for x in plugins if x not in pip_pytest and x not in poetry_pytest
73+
x for x in plugins if x not in pip_pytest and x not in poetry_pytest and x not in uv_pytest
7374
]
74-
print(f"Generic plugins (includes Rust): {list_plugins(generic_plugins)}")
75+
print(f"Generic plugins: {list_plugins(generic_plugins)}")
7576

7677
for p in sorted(pip_pytest):
7778
yield Plugin(
@@ -98,6 +99,18 @@ def enumerate_plugins(basedir: Path) -> Generator[Plugin, None, None]:
9899
},
99100
)
100101

102+
for p in sorted(uv_pytest):
103+
yield Plugin(
104+
name=p.name,
105+
path=p,
106+
language="python",
107+
framework="uv",
108+
testfiles=get_testfiles(p),
109+
details={
110+
"pyproject": p / Path("pyproject.toml"),
111+
},
112+
)
113+
101114
for p in sorted(generic_plugins):
102115
yield Plugin(
103116
name=p.name,
@@ -107,6 +120,5 @@ def enumerate_plugins(basedir: Path) -> Generator[Plugin, None, None]:
107120
testfiles=get_testfiles(p),
108121
details={
109122
"requirements": p / Path("tests/requirements.txt"),
110-
"setup": p / Path("tests/setup.sh"),
111123
},
112124
)

0 commit comments

Comments
 (0)