Skip to content

Commit 974531e

Browse files
committed
[ci] test the python wheels by running all tutorials
1 parent ee08f36 commit 974531e

File tree

3 files changed

+155
-2
lines changed

3 files changed

+155
-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 -s -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: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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", # requires reading remote data via HTTP
19+
"ntpl008_import.C", # requires reading remote data via HTTP
20+
"ntpl011_global_temperatures.C", # requires reading remote data via HTTP
21+
"distrdf004_dask_lxbatch.py", # only works on lxplus
22+
"_SQlite", # requires SQLite, not supported yet in ROOT wheels
23+
"h1analysisProxy.C", # helper macro, not meant to run standalone
24+
"hist001_RHist_basics.C", # required RHist, not supported in ROOT wheels
25+
"hist002_RHist_weighted.C", # required RHist, not supported in ROOT wheels
26+
}
27+
28+
# ----------------------
29+
# Python tutorials tests
30+
# ----------------------
31+
py_tutorials = []
32+
for sub in subdirs:
33+
sub_path = tutorial_dir / sub
34+
for f in sub_path.rglob("*.py"):
35+
if any(skip in f.name for skip in SKIP_TUTORIALS):
36+
print("Skipping Python tutorial:", f)
37+
continue
38+
py_tutorials.append(f)
39+
40+
py_tutorials = sorted(py_tutorials, key=lambda p: p.name)
41+
42+
43+
def test_tutorials_are_detected():
44+
assert len(py_tutorials) > 0
45+
46+
47+
@pytest.mark.parametrize("tutorial", py_tutorials, ids=lambda p: p.name)
48+
def test_tutorial(tutorial):
49+
env = dict(**os.environ)
50+
# force matplotlib to use a non-GUI backend
51+
env["MPLBACKEND"] = "Agg"
52+
print("Test env:", env)
53+
try:
54+
result = subprocess.run(
55+
[sys.executable, str(tutorial)],
56+
check=True,
57+
env=env,
58+
timeout=60,
59+
capture_output=True,
60+
text=True,
61+
)
62+
print("Test stderr:", result.stderr)
63+
64+
except subprocess.TimeoutExpired:
65+
pytest.skip(f"Tutorial {tutorial} timed out")
66+
67+
except subprocess.CalledProcessError as e:
68+
# read stderr to see if EOFError occurred
69+
if "EOFError" in e.stderr:
70+
pytest.skip(f"Skipping {tutorial.name} (requires user input)")
71+
raise
72+
73+
74+
# ----------------------
75+
# C++ tutorials tests
76+
# ----------------------
77+
cpp_tutorials = []
78+
for sub in subdirs:
79+
sub_path = tutorial_dir / sub
80+
for f in sub_path.rglob("*.C"):
81+
if any(skip in f.name for skip in SKIP_TUTORIALS):
82+
print("Skipping C++ tutorial:", f)
83+
continue
84+
cpp_tutorials.append(f)
85+
86+
cpp_tutorials = sorted(cpp_tutorials, key=lambda p: p.name)
87+
88+
89+
def test_cpp_tutorials_are_detected():
90+
assert len(cpp_tutorials) > 0
91+
92+
93+
@pytest.mark.parametrize("tutorial", cpp_tutorials, ids=lambda p: p.name)
94+
def test_cpp_tutorial(tutorial):
95+
try:
96+
root_exe = shutil.which("root")
97+
result = subprocess.run(
98+
[root_exe, "-b", "-q", str(tutorial)],
99+
check=True,
100+
timeout=60,
101+
capture_output=True,
102+
text=True,
103+
)
104+
print("Test stderr:", result.stderr)
105+
106+
except subprocess.TimeoutExpired:
107+
pytest.skip(f"Tutorial {tutorial} timed out")
108+
109+
except subprocess.CalledProcessError as e:
110+
if e.returncode == -signal.SIGILL or e.returncode == 132:
111+
pytest.fail(f"Failing {tutorial.name} (illegal instruction on this platform)")
112+
elif "EOFError" in e.stderr:
113+
pytest.skip(f"Skipping {tutorial.name} (requires user input)")
114+
raise

0 commit comments

Comments
 (0)