Skip to content

Commit 0d13ee8

Browse files
committed
[ci] test the python wheels by running all tutorials
1 parent 8152c4d commit 0d13ee8

File tree

3 files changed

+142
-2
lines changed

3 files changed

+142
-2
lines changed

.github/workflows/python_wheel_build.yml

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ on:
1111
schedule:
1212
- cron: '01 1 * * *'
1313
pull_request:
14-
types: [labeled]
14+
types: [opened, synchronize, reopened, labeled]
1515

1616
concurrency:
1717
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
@@ -26,6 +26,7 @@ jobs:
2626
contains(github.event.pull_request.labels.*.name, 'build-python-wheels')
2727
runs-on: ubuntu-latest
2828
strategy:
29+
fail-fast: false
2930
matrix:
3031
target: [cp39-manylinux_x86_64, cp310-manylinux_x86_64, cp311-manylinux_x86_64, cp312-manylinux_x86_64, cp313-manylinux_x86_64]
3132
name: ${{ matrix.target }}
@@ -35,6 +36,44 @@ jobs:
3536
with:
3637
build-tag: ${{ matrix.target }}
3738

39+
test-wheels:
40+
needs: build-wheels
41+
runs-on: ubuntu-latest
42+
strategy:
43+
fail-fast: false
44+
matrix:
45+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
46+
name: test-wheel-cp${{ matrix.python-version }}
47+
steps:
48+
- uses: actions/checkout@v4
49+
50+
- name: Download produced wheels
51+
uses: actions/download-artifact@v4
52+
with:
53+
path: wheels
54+
merge-multiple: true
55+
56+
- name: Setup Python
57+
uses: actions/setup-python@v5
58+
with:
59+
python-version: ${{ matrix.python-version }}
60+
61+
- name: Install produced wheel
62+
run: |
63+
ls -R wheels
64+
PY_VER=$(python -c "import sys; print(f'cp{sys.version_info.major}{sys.version_info.minor}')")
65+
WHEEL=$(ls wheels/*${PY_VER}*.whl | head -n 1)
66+
echo "Python version: ${PY_VER}, installing wheel: ${WHEEL}"
67+
pip install "$WHEEL"
68+
69+
- name: Install tutorials dependencies
70+
run: |
71+
python -m pip install --no-cache-dir -r requirements.txt
72+
73+
- name: Run tutorials
74+
run: |
75+
pytest -vv -rF --show-capture=all test/wheels
76+
3877
create-and-upload-wheel-registry:
3978
if: github.event_name != 'pull_request' # The secrets are not available in PR
4079
needs: build-wheels

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def run(self):
5252
"-Dbuiltin_nlohmannjson=ON -Dbuiltin_tbb=ON -Dbuiltin_xrootd=ON " # builtins
5353
"-Dbuiltin_lz4=ON -Dbuiltin_lzma=ON -Dbuiltin_zstd=ON -Dbuiltin_xxhash=ON " # builtins
5454
"-Dpyroot=ON -Ddataframe=ON -Dxrootd=ON -Dssl=ON -Dimt=ON "
55-
"-Droofit=ON "
55+
"-Droofit=ON -Dmathmore=ON -Dbuiltin_fftw3=ON -Dbuiltin_gsl=ON "
5656
# Next 4 paths represent the structure of the target binaries/headers/libs
5757
# as the target installation directory of the Python environment would expect
5858
f"-DCMAKE_INSTALL_BINDIR={ROOT_BUILD_INTERNAL_DIRNAME}/ROOT/bin "

test/wheels/test_tutorials.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import os
2+
import pathlib
3+
import shutil
4+
import signal
5+
import subprocess
6+
import sys
7+
8+
import pytest
9+
import ROOT
10+
11+
ROOT.gROOT.SetBatch(True)
12+
13+
tutorial_dir = pathlib.Path(str(ROOT.gROOT.GetTutorialDir()))
14+
15+
subdirs = ["analysis/dataframe", "analysis/tree", "hist", "io/ntuple", "roofit/roofit"]
16+
17+
SKIP_TUTORIALS = {
18+
"ntpl004_dimuon.C", # require reading remote data via HTTP
19+
"ntpl008_import.C", # require reading remote data via HTTP
20+
"distrdf004_dask_lxbatch.py", # only works on lxplus
21+
}
22+
23+
# ----------------------
24+
# Python tutorials tests
25+
# ----------------------
26+
py_tutorials = []
27+
for sub in subdirs:
28+
sub_path = tutorial_dir / sub
29+
for f in sub_path.rglob("*.py"):
30+
if any(skip in f.name for skip in SKIP_TUTORIALS):
31+
print("Skipping Python tutorial:", f)
32+
continue
33+
py_tutorials.append(f)
34+
35+
36+
def test_tutorials_are_detected():
37+
assert len(py_tutorials) > 0
38+
39+
40+
@pytest.mark.parametrize("tutorial", py_tutorials, ids=lambda p: p.name)
41+
def test_tutorial(tutorial):
42+
env = dict(**os.environ)
43+
# force matplotlib to use a non-GUI backend
44+
env["MPLBACKEND"] = "Agg"
45+
print("Test env:", env)
46+
try:
47+
result = subprocess.run(
48+
[sys.executable, str(tutorial)],
49+
check=True,
50+
env=env,
51+
timeout=60,
52+
capture_output=True,
53+
text=True,
54+
)
55+
print("Test stderr:", result.stderr)
56+
except subprocess.TimeoutExpired:
57+
pytest.skip(f"Tutorial {tutorial} timed out")
58+
except subprocess.CalledProcessError as e:
59+
# read stderr to see if EOFError occurred
60+
if "EOFError" in e.stderr:
61+
pytest.skip(f"Skipping {tutorial.name} (requires user input)")
62+
raise
63+
64+
65+
# ----------------------
66+
# C++ tutorials tests
67+
# ----------------------
68+
cpp_tutorials = []
69+
for sub in subdirs:
70+
sub_path = tutorial_dir / sub
71+
for f in sub_path.rglob("*.C"):
72+
if any(skip in f.name for skip in SKIP_TUTORIALS):
73+
print("Skipping C++ tutorial:", f)
74+
continue
75+
cpp_tutorials.append(f)
76+
77+
78+
def test_cpp_tutorials_are_detected():
79+
assert len(cpp_tutorials) > 0
80+
81+
82+
@pytest.mark.parametrize("tutorial", cpp_tutorials, ids=lambda p: p.name)
83+
def test_cpp_tutorial(tutorial):
84+
try:
85+
root_exe = shutil.which("root")
86+
result = subprocess.run(
87+
[root_exe, "-b", "-q", f".x {tutorial}"],
88+
check=True,
89+
timeout=60,
90+
capture_output=True,
91+
text=True,
92+
)
93+
print("Test stderr:", result.stderr)
94+
except subprocess.TimeoutExpired:
95+
pytest.skip(f"Tutorial {tutorial} timed out")
96+
except subprocess.CalledProcessError as e:
97+
if e.returncode == -signal.SIGILL or e.returncode == 132:
98+
pytest.fail(f"Failing {tutorial.name} (illegal instruction on this platform)")
99+
elif "EOFError" in e.stderr:
100+
pytest.skip(f"Skipping {tutorial.name} (requires user input)")
101+
raise

0 commit comments

Comments
 (0)