Skip to content

Commit efa68eb

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

File tree

3 files changed

+165
-1
lines changed

3 files changed

+165
-1
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# ROOT requirements for third-party Python packages used to run tutorials
2+
3+
# PyROOT: Interoperability with numpy arrays
4+
numpy
5+
pandas
6+
7+
# PyROOT: ROOT.Numba.Declare decorator
8+
numba>=0.48
9+
cffi>=1.9.1
10+
11+
# Unified Histogram Interface (UHI)
12+
uhi
13+
matplotlib
14+
mplhep
15+
16+
# Other
17+
pytest
18+
scikit-learn
19+
xgboost
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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+
"rf503_wspaceread.py",
18+
"rf615_simulation_based_inference.py",
19+
"ntpl004_dimuon.C",
20+
"ntpl008_import.C",
21+
# skip distributed RDataFrame tutorials for now
22+
"distrdf",
23+
}
24+
25+
# ----------------------
26+
# Python tutorials tests
27+
# ----------------------
28+
py_tutorials = []
29+
for sub in subdirs:
30+
sub_path = tutorial_dir / sub
31+
for f in sub_path.rglob("*.py"):
32+
if any(skip in f.name for skip in SKIP_TUTORIALS):
33+
print("Skipping Python tutorial:", f)
34+
continue
35+
py_tutorials.append(f)
36+
37+
38+
def test_tutorials_are_detected():
39+
assert len(py_tutorials) > 0
40+
41+
42+
@pytest.mark.parametrize("tutorial", py_tutorials, ids=lambda p: p.name)
43+
def test_tutorial(tutorial):
44+
env = dict(**os.environ)
45+
# force matplotlib to use a non-GUI backend
46+
env["MPLBACKEND"] = "Agg"
47+
print("Test env:", env)
48+
try:
49+
result = subprocess.run(
50+
[sys.executable, str(tutorial)],
51+
check=True,
52+
env=env,
53+
timeout=60,
54+
capture_output=True,
55+
text=True,
56+
)
57+
print("Test stderr:", result.stderr)
58+
except subprocess.TimeoutExpired:
59+
pytest.skip(f"Tutorial {tutorial} timed out")
60+
except subprocess.CalledProcessError as e:
61+
# read stderr to see if EOFError occurred
62+
if "EOFError" in e.stderr:
63+
pytest.skip(f"Skipping {tutorial.name} (requires user input)")
64+
raise
65+
66+
67+
# ----------------------
68+
# C++ tutorials tests
69+
# ----------------------
70+
cpp_tutorials = []
71+
for sub in subdirs:
72+
sub_path = tutorial_dir / sub
73+
for f in sub_path.rglob("*.C"):
74+
if any(skip in f.name for skip in SKIP_TUTORIALS):
75+
print("Skipping C++ tutorial:", f)
76+
continue
77+
cpp_tutorials.append(f)
78+
79+
80+
def test_cpp_tutorials_are_detected():
81+
assert len(cpp_tutorials) > 0
82+
83+
84+
@pytest.mark.parametrize("tutorial", cpp_tutorials, ids=lambda p: p.name)
85+
def test_cpp_tutorial(tutorial):
86+
try:
87+
result = subprocess.run(
88+
[sys.executable, "-c", f'import ROOT; ROOT.gROOT.ProcessLine(".x {tutorial}")'],
89+
check=True,
90+
timeout=60,
91+
capture_output=True,
92+
text=True,
93+
)
94+
print("Test stderr:", result.stderr)
95+
except subprocess.TimeoutExpired:
96+
pytest.skip(f"Tutorial {tutorial} timed out")
97+
except subprocess.CalledProcessError as e:
98+
if e.returncode == -signal.SIGILL or e.returncode == 132:
99+
pytest.fail(f"Failing {tutorial.name} (illegal instruction on this platform)")
100+
elif "EOFError" in e.stderr:
101+
pytest.skip(f"Skipping {tutorial.name} (requires user input)")
102+
raise

.github/workflows/python_wheel_build.yml

Lines changed: 44 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,48 @@ jobs:
3536
with:
3637
build-tag: ${{ matrix.target }}
3738

39+
test-tutorials:
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+
steps:
47+
- uses: actions/checkout@v4
48+
49+
- name: Install required system packages
50+
run: |
51+
sudo apt-get update
52+
sudo apt-get install -y libfftw3-dev
53+
54+
- name: Download produced wheels
55+
uses: actions/download-artifact@v4
56+
with:
57+
path: wheels
58+
merge-multiple: true
59+
60+
- name: Setup Python
61+
uses: actions/setup-python@v5
62+
with:
63+
python-version: ${{ matrix.python-version }}
64+
65+
- name: Install produced wheel
66+
run: |
67+
ls -R wheels
68+
PY_VER=$(python -c "import sys; print(f'cp{sys.version_info.major}{sys.version_info.minor}')")
69+
WHEEL=$(ls wheels/*${PY_VER}*.whl | head -n 1)
70+
echo "Python version: ${PY_VER}, installing wheel: ${WHEEL}"
71+
pip install "$WHEEL"
72+
73+
- name: Install tutorials dependencies
74+
run: |
75+
python -m pip install -r .github/wheel-tests/requirements.txt
76+
77+
- name: Run tutorials
78+
run: |
79+
pytest -vv -rF --show-capture=all .github/wheel-tests
80+
3881
create-and-upload-wheel-registry:
3982
if: github.event_name != 'pull_request' # The secrets are not available in PR
4083
needs: build-wheels

0 commit comments

Comments
 (0)