Skip to content

Commit 40ef363

Browse files
committed
[ci] test the python wheels by running all tutorials
1 parent e520baf commit 40ef363

File tree

4 files changed

+147
-1
lines changed

4 files changed

+147
-1
lines changed

.github/workflows/cibuildwheel-impl/action.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,24 @@ inputs:
88
runs:
99
using: "composite"
1010
steps:
11+
- name: Install system dependencies
12+
shell: bash
13+
run: |
14+
sudo apt install -y binutils cmake dpkg-dev g++ gcc libssl-dev git libx11-dev \
15+
libxext-dev libxft-dev libxpm-dev python3 libtbb-dev libvdt-dev libgif-dev
16+
1117
- name: Build wheel
1218
uses: pypa/[email protected]
1319
env:
20+
PIP_NO_CACHE_DIR: "1"
1421
CIBW_BUILD: ${{ inputs.build-tag }}
22+
CIBW_TEST_REQUIRES: "-r test_tutorials/requirements.txt"
23+
CIBW_BEFORE_TEST: >
24+
yum install -y git make cmake gcc-c++ gcc binutils \
25+
libX11-devel libXpm-devel libXft-devel libXext-devel python openssl-devel \
26+
xrootd-client-devel xrootd-libs-devel
27+
CIBW_TEST_SOURCES: "test_tutorials"
28+
CIBW_TEST_COMMAND: "pytest -vv -rF"
1529

1630
- name: Upload wheel
1731
uses: actions/upload-artifact@v4

.github/workflows/python_wheel_build.yml

Lines changed: 2 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 }}
@@ -25,6 +25,7 @@ jobs:
2525
contains(github.event.pull_request.labels.*.name, 'build-python-wheels')
2626
runs-on: ubuntu-latest
2727
strategy:
28+
fail-fast: false
2829
matrix:
2930
target: [cp38-manylinux_x86_64, cp39-manylinux_x86_64, cp310-manylinux_x86_64, cp311-manylinux_x86_64, cp312-manylinux_x86_64, cp313-manylinux_x86_64]
3031
name: ${{ matrix.target }}

test_tutorials/requirements.txt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# ROOT requirements for third-party Python packages
2+
3+
# PyROOT: Interoperability with numpy arrays
4+
numpy
5+
pandas
6+
7+
# TMVA: SOFIE
8+
# dm-sonnet # used for GNNs
9+
# graph_nets
10+
# onnx
11+
12+
# TMVA: PyMVA interfaces
13+
# scikit-learn
14+
# tensorflow ; python_version < "3.13" # TensorFlow doesn't support Python 3.13 yet
15+
# torch
16+
# xgboost
17+
18+
# PyROOT: ROOT.Numba.Declare decorator
19+
numba>=0.48
20+
cffi>=1.9.1
21+
22+
# Notebooks: ROOT C++ kernel
23+
# IPython
24+
# jupyter
25+
# metakernel>=0.20.0
26+
# notebook>=4.4.1
27+
28+
# Distributed RDataFrame
29+
pyspark>=2.4 # Spark backend
30+
dask>=2022.08.1 # Dask backend
31+
distributed>=2022.08.1 # Dask backend
32+
33+
# JsMVA: Jupyter notebook magic for TMVA
34+
# ipywidgets
35+
36+
# Unified Histogram Interface (UHI)
37+
uhi
38+
matplotlib
39+
mplhep
40+
41+
# For testing
42+
# nbconvert>=7.4.0
43+
pytest
44+
# setuptools
45+
46+
# Look for CPU-only versions of PyTorch to avoid pulling CUDA in the CI docker images.
47+
# -f https://download.pytorch.org/whl/cpu/torch_stable.html

test_tutorials/test_tutorials.py

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

0 commit comments

Comments
 (0)