Skip to content

Commit 1b84273

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

File tree

7 files changed

+211
-1
lines changed

7 files changed

+211
-1
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,20 @@ 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 libtbb-dev libvdt-dev libgif-dev libfftw3-dev
16+
1117
- name: Build wheel
1218
uses: pypa/cibuildwheel@v3.0.1
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_TEST_SOURCES: "test_tutorials"
24+
CIBW_TEST_COMMAND: "pytest -vv -rF --show-capture=all test_tutorials"
1525

1626
- name: Upload wheel
1727
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: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
scikit-learn
47+
48+
# Look for CPU-only versions of PyTorch to avoid pulling CUDA in the CI docker images.
49+
# -f https://download.pytorch.org/whl/cpu/torch_stable.html

test_tutorials/test_tutorials.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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+
for f in sub_path.rglob("*.py"):
29+
# skip distrdf tutorials for now
30+
if "distrdf" in f.name:
31+
continue
32+
py_tutorials.append(f)
33+
34+
def test_tutorials_are_detected():
35+
assert len(py_tutorials) > 0
36+
37+
@pytest.mark.parametrize("tutorial", py_tutorials, ids=lambda p: p.name)
38+
def test_tutorial(tutorial):
39+
env = dict(**os.environ)
40+
# force matplotlib to use a non-GUI backend
41+
env["MPLBACKEND"] = "Agg"
42+
print("Test env:", env)
43+
try:
44+
result = subprocess.run(
45+
[sys.executable, str(tutorial)],
46+
check=True,
47+
env=env,
48+
timeout=60,
49+
)
50+
print("Test stderr:", result.stderr)
51+
except subprocess.TimeoutExpired:
52+
pytest.skip(f"Tutorial {tutorial} timed out")
53+
except subprocess.CalledProcessError as e:
54+
# read stderr to see if EOFError occurred
55+
if "EOFError" in e.stderr:
56+
pytest.skip(f"Skipping {tutorial.name} (requires user input)")
57+
raise
58+
59+
# ----------------------
60+
# C++ tutorials tests
61+
# ----------------------
62+
cpp_tutorials = []
63+
for sub in subdirs:
64+
sub_path = tutorial_dir / sub
65+
cpp_tutorials.extend(sub_path.rglob("*.C"))
66+
67+
def test_cpp_tutorials_are_detected():
68+
assert len(cpp_tutorials) > 0
69+
70+
@pytest.mark.parametrize("tutorial", cpp_tutorials, ids=lambda p: p.name)
71+
def test_cpp_tutorial(tutorial):
72+
try:
73+
result = subprocess.run(
74+
[sys.executable, "-c", f'import ROOT; ROOT.gROOT.ProcessLine(".x {tutorial}")'],
75+
check=True,
76+
timeout=60,
77+
capture_output=True,
78+
text=True
79+
)
80+
except subprocess.TimeoutExpired:
81+
pytest.skip(f"Tutorial {tutorial} timed out")
82+
except subprocess.CalledProcessError as e:
83+
if e.returncode == -signal.SIGILL or e.returncode == 132:
84+
pytest.fail(f"Failing {tutorial.name} (illegal instruction on this platform)")
85+
elif "EOFError" in e.stderr:
86+
pytest.skip(f"Skipping {tutorial.name} (requires user input)")
87+
raise
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## \file
2+
## \ingroup tutorial_dataframe
3+
## \notebook -draw
4+
## Simple RDataFrame example in Python.
5+
##
6+
## This tutorial shows a minimal example of RDataFrame. It starts without input
7+
## data, generates a new column `x` with random numbers, and finally draws
8+
## a histogram for `x`.
9+
##
10+
## \macro_code
11+
## \macro_output
12+
##
13+
## \date September 2021
14+
## \author Enric Tejedor (CERN)
15+
16+
import ROOT
17+
18+
# Create a data frame with 100 rows
19+
rdf = ROOT.RDataFrame(100)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
## \file
2+
## \ingroup tutorial_dataframe
3+
## \notebook -draw
4+
## Simple RDataFrame example in Python.
5+
##
6+
## This tutorial shows a minimal example of RDataFrame. It starts without input
7+
## data, generates a new column `x` with random numbers, and finally draws
8+
## a histogram for `x`.
9+
##
10+
## \macro_code
11+
## \macro_output
12+
##
13+
## \date September 2021
14+
## \author Enric Tejedor (CERN)
15+
16+
import ROOT
17+
18+
# Create a data frame with 100 rows
19+
rdf = ROOT.RDataFrame(100)
20+
21+
# Define a new column `x` that contains random numbers
22+
rdf_x = rdf.Define("x", "gRandom->Rndm()")
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
## \file
2+
## \ingroup tutorial_dataframe
3+
## \notebook -draw
4+
## Simple RDataFrame example in Python.
5+
##
6+
## This tutorial shows a minimal example of RDataFrame. It starts without input
7+
## data, generates a new column `x` with random numbers, and finally draws
8+
## a histogram for `x`.
9+
##
10+
## \macro_code
11+
## \macro_output
12+
##
13+
## \date September 2021
14+
## \author Enric Tejedor (CERN)
15+
16+
import ROOT
17+
18+
# Create a data frame with 100 rows
19+
rdf = ROOT.RDataFrame(100)
20+
21+
# Define a new column `x` that contains random numbers
22+
rdf_x = rdf.Define("x", "42")

0 commit comments

Comments
 (0)