Skip to content

Commit 6ed779f

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

File tree

3 files changed

+140
-2
lines changed

3 files changed

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

0 commit comments

Comments
 (0)