Skip to content

Commit face598

Browse files
committed
ci: add tests with LaTeX
Add dedicated GitHub Actions workflow (ci_latex.yml) for testing LaTeX-dependent matplotlib styles: - CMSTex, ROOTTex (CMS experiment) - DUNETex, DUNETex1 (DUNE experiment) - ATLASTex (ATLAS experiment) - LHCbTex1, LHCbTex2 (LHCb experiment) The workflow: - Installs full TeXLive distribution with required packages - Runs on PRs that modify style files or LaTeX tests - Runs weekly (Monday at 0:00 UTC) to catch regressions - Can be triggered manually via workflow_dispatch Also adds comprehensive test file (test_styles_latex.py) with baseline images for all LaTeX styles. Closes #603
1 parent b84511a commit face598

File tree

9 files changed

+279
-14
lines changed

9 files changed

+279
-14
lines changed

.github/workflows/ci_latex.yml

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# GitHub Actions workflow for testing LaTeX-dependent styles
2+
#
3+
# This workflow tests matplotlib styles that require LaTeX to be installed:
4+
# - CMSTex, ROOTTex (CMS experiment)
5+
# - DUNETex, DUNETex1 (DUNE experiment)
6+
# - ATLASTex (ATLAS experiment)
7+
# - LHCbTex1, LHCbTex2 (LHCb experiment)
8+
#
9+
# These tests are separated from the main CI workflow because:
10+
# 1. LaTeX installation adds ~500MB and several minutes to CI time
11+
# 2. LaTeX rendering can be environment-dependent
12+
# 3. Most PRs don't affect LaTeX-dependent features
13+
#
14+
# The workflow runs:
15+
# - On pull requests that modify style files or LaTeX tests
16+
# - Weekly (every Monday) to catch regressions
17+
# - Manually via workflow_dispatch
18+
19+
name: CI LaTeX
20+
21+
on:
22+
workflow_dispatch:
23+
pull_request:
24+
paths:
25+
- 'src/mplhep/styles/**'
26+
- 'tests/test_styles_latex.py'
27+
- 'tests/test_dune.py'
28+
- '.github/workflows/ci_latex.yml'
29+
# Run weekly on Monday at 0:00 UTC
30+
schedule:
31+
- cron: '0 0 * * 1'
32+
33+
jobs:
34+
35+
test-latex:
36+
name: "LaTeX Tests • Python ${{ matrix.python-version }}"
37+
runs-on: ubuntu-latest
38+
strategy:
39+
fail-fast: false
40+
matrix:
41+
python-version: ['3.12']
42+
43+
steps:
44+
- uses: actions/checkout@v4
45+
46+
- name: Setup TeXLive
47+
uses: xu-cheng/texlive-action@v3
48+
49+
- name: Setup uv
50+
uses: astral-sh/setup-uv@v5
51+
with:
52+
version: 'latest'
53+
python-version: ${{ matrix.python-version }}
54+
enable-cache: true
55+
cache-dependency-glob: '**/pyproject.toml'
56+
57+
- name: Install core fonts
58+
run: |
59+
echo ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true | sudo debconf-set-selections
60+
sudo apt-get install ttf-mscorefonts-installer
61+
62+
- name: Install package
63+
run: |
64+
uv pip install -e '.[all]'
65+
uv pip install pytest-github-actions-annotate-failures
66+
uv pip install pytest-xdist
67+
uv pip list
68+
69+
- name: Test LaTeX styles with pytest
70+
run: |
71+
python -m pytest tests/test_styles_latex.py -r sa --mpl --mpl-results-path=pytest_results_latex -n 4
72+
73+
- name: Test DUNE LaTeX styles with pytest
74+
run: |
75+
python -m pytest tests/test_dune.py::test_style_dunetex -r sa --mpl --mpl-results-path=pytest_results_dune_tex
76+
python -m pytest tests/test_dune.py::test_style_dunetex1 -r sa --mpl --mpl-results-path=pytest_results_dune_tex
77+
python -m pytest tests/test_dune.py::test_dune_style_string_aliases -r sa --mpl --mpl-results-path=pytest_results_dune_tex
78+
79+
- name: Upload pytest test results
80+
uses: actions/upload-artifact@v4
81+
if: failure()
82+
with:
83+
name: pytest_results_latex-${{ matrix.python-version }}
84+
retention-days: 7
85+
path: |
86+
pytest_results_latex
87+
pytest_results_dune_tex
13.6 KB
Loading
20 KB
Loading
21.6 KB
Loading
24.2 KB
Loading
21.4 KB
Loading

tests/conftest.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import shutil
2+
import subprocess
3+
14
import matplotlib.pyplot as plt
25
import pytest
36

@@ -10,6 +13,42 @@ def pytest_configure(config):
1013
# Running with xdist, disable pytest-asyncio
1114
config.pluginmanager.set_blocked("pytest_asyncio")
1215

16+
# Register custom markers
17+
config.addinivalue_line(
18+
"markers", "latex: tests that require LaTeX to be installed"
19+
)
20+
21+
22+
def _has_latex():
23+
"""Check if LaTeX is available on the system."""
24+
if not shutil.which("latex"):
25+
return False
26+
try:
27+
subprocess.run(
28+
["latex", "--version"],
29+
check=True,
30+
capture_output=True,
31+
timeout=5,
32+
)
33+
return True
34+
except (
35+
subprocess.CalledProcessError,
36+
subprocess.TimeoutExpired,
37+
FileNotFoundError,
38+
):
39+
return False
40+
41+
42+
def pytest_collection_modifyitems(config, items): # noqa: ARG001
43+
"""Skip LaTeX tests if LaTeX is not installed."""
44+
if _has_latex():
45+
return
46+
47+
skip_latex = pytest.mark.skip(reason="LaTeX not installed")
48+
for item in items:
49+
if "latex" in item.keywords:
50+
item.add_marker(skip_latex)
51+
1352

1453
@pytest.fixture(autouse=True)
1554
def clear_mplhep_rcparams():

tests/test_dune.py

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,8 @@ def test_dune_style_str_alias(fig_test, fig_ref):
4242
fig_test.subplots()
4343

4444

45+
@pytest.mark.latex
4546
@pytest.mark.skipif(sys.platform != "linux", reason="Linux only")
46-
@pytest.mark.skipif(
47-
os.environ.get("GITHUB_ACTIONS") == "true", reason="Fails in GitHub Actions"
48-
)
4947
@pytest.mark.mpl_image_compare(style="default", remove_text=False)
5048
def test_style_dunetex():
5149
plt.rcParams.update(plt.rcParamsDefault)
@@ -58,10 +56,8 @@ def test_style_dunetex():
5856
return fig
5957

6058

59+
@pytest.mark.latex
6160
@pytest.mark.skipif(sys.platform != "linux", reason="Linux only")
62-
@pytest.mark.skipif(
63-
os.environ.get("GITHUB_ACTIONS") == "true", reason="Fails in GitHub Actions"
64-
)
6561
@pytest.mark.mpl_image_compare(style="default", remove_text=False)
6662
def test_style_dunetex1():
6763
plt.rcParams.update(plt.rcParamsDefault)
@@ -84,18 +80,12 @@ def test_style_dunetex1():
8480
pytest.param(
8581
mh.style.DUNETex,
8682
"DUNETex",
87-
marks=pytest.mark.skipif(
88-
os.environ.get("GITHUB_ACTIONS") == "true",
89-
reason="Fails in GitHub Actions",
90-
),
83+
marks=pytest.mark.latex,
9184
),
9285
pytest.param(
9386
mh.style.DUNETex1,
9487
"DUNETex1",
95-
marks=pytest.mark.skipif(
96-
os.environ.get("GITHUB_ACTIONS") == "true",
97-
reason="Fails in GitHub Actions",
98-
),
88+
marks=pytest.mark.latex,
9989
),
10090
],
10191
ids=["DUNE", "DUNE1", "DUNETex", "DUNETex1"],

tests/test_styles_latex.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import sys
5+
6+
import matplotlib.pyplot as plt
7+
import pytest
8+
from matplotlib.testing.decorators import check_figures_equal
9+
10+
os.environ["RUNNING_PYTEST"] = "true"
11+
12+
import mplhep as mh
13+
14+
"""
15+
Tests for LaTeX-dependent styles (CMSTex, DUNETex, ATLASTex, LHCbTex, etc.)
16+
17+
To test run:
18+
pytest tests/test_styles_latex.py --mpl
19+
20+
When adding new tests, run:
21+
pytest tests/test_styles_latex.py --mpl-generate-path=tests/baseline
22+
"""
23+
24+
plt.switch_backend("Agg")
25+
26+
pytestmark = [
27+
pytest.mark.latex,
28+
pytest.mark.skipif(sys.platform != "linux", reason="Linux only"),
29+
]
30+
31+
32+
@pytest.mark.mpl_image_compare(style="default", remove_text=False)
33+
def test_style_cmstex():
34+
plt.rcParams.update(plt.rcParamsDefault)
35+
36+
plt.style.use(mh.style.CMSTex)
37+
fig, ax = plt.subplots()
38+
mh.cms.label("Preliminary")
39+
40+
plt.rcParams.update(plt.rcParamsDefault)
41+
return fig
42+
43+
44+
@pytest.mark.mpl_image_compare(style="default", remove_text=False)
45+
def test_style_roottex():
46+
plt.rcParams.update(plt.rcParamsDefault)
47+
48+
plt.style.use(mh.style.ROOTTex)
49+
fig, ax = plt.subplots()
50+
ax.set_xlabel(r"$p_T$ [GeV]")
51+
ax.set_ylabel(r"Events / 10 GeV")
52+
53+
plt.rcParams.update(plt.rcParamsDefault)
54+
return fig
55+
56+
57+
@pytest.mark.mpl_image_compare(style="default", remove_text=False)
58+
def test_style_dunetex():
59+
plt.rcParams.update(plt.rcParamsDefault)
60+
61+
plt.style.use(mh.style.DUNETex)
62+
fig, ax = plt.subplots()
63+
mh.dune.label(text="Preliminary")
64+
65+
plt.rcParams.update(plt.rcParamsDefault)
66+
return fig
67+
68+
69+
@pytest.mark.mpl_image_compare(style="default", remove_text=False)
70+
def test_style_dunetex1():
71+
plt.rcParams.update(plt.rcParamsDefault)
72+
73+
plt.style.use(mh.style.DUNETex1)
74+
fig, ax = plt.subplots()
75+
mh.dune.label(text="Preliminary")
76+
77+
plt.rcParams.update(plt.rcParamsDefault)
78+
return fig
79+
80+
81+
@pytest.mark.mpl_image_compare(style="default", remove_text=False)
82+
def test_style_atlastex():
83+
plt.rcParams.update(plt.rcParamsDefault)
84+
85+
plt.style.use(mh.style.ATLASTex)
86+
fig, ax = plt.subplots()
87+
mh.atlas.label(text="Preliminary")
88+
89+
plt.rcParams.update(plt.rcParamsDefault)
90+
return fig
91+
92+
93+
@pytest.mark.mpl_image_compare(style="default", remove_text=False)
94+
def test_style_lhcbtex1():
95+
plt.rcParams.update(plt.rcParamsDefault)
96+
97+
plt.style.use([mh.style.LHCbTex1, {"figure.autolayout": False}])
98+
fig, ax = plt.subplots()
99+
mh.lhcb.label("Preliminary")
100+
101+
plt.rcParams.update(plt.rcParamsDefault)
102+
return fig
103+
104+
105+
@pytest.mark.mpl_image_compare(style="default", remove_text=False)
106+
def test_style_lhcbtex2():
107+
plt.rcParams.update(plt.rcParamsDefault)
108+
109+
plt.style.use([mh.style.LHCbTex2, {"figure.autolayout": False}])
110+
fig, ax = plt.subplots()
111+
mh.lhcb.label("Preliminary")
112+
113+
plt.rcParams.update(plt.rcParamsDefault)
114+
return fig
115+
116+
117+
@check_figures_equal(extensions=["pdf"])
118+
@pytest.mark.parametrize(
119+
("mplhep_style", "str_alias"),
120+
[
121+
(mh.style.CMSTex, "CMSTex"),
122+
(mh.style.ROOTTex, "ROOTTex"),
123+
(mh.style.DUNETex, "DUNETex"),
124+
(mh.style.DUNETex1, "DUNETex1"),
125+
(mh.style.ATLASTex, "ATLASTex"),
126+
(mh.style.LHCbTex1, "LHCbTex1"),
127+
(mh.style.LHCbTex2, "LHCbTex2"),
128+
],
129+
ids=[
130+
"CMSTex",
131+
"ROOTTex",
132+
"DUNETex",
133+
"DUNETex1",
134+
"ATLASTex",
135+
"LHCbTex1",
136+
"LHCbTex2",
137+
],
138+
)
139+
def test_latex_style_str_alias(fig_test, fig_ref, mplhep_style, str_alias):
140+
"""Test that string aliases work for all LaTeX style variants."""
141+
plt.rcParams.update(plt.rcParamsDefault)
142+
143+
mh.rcParams.clear()
144+
plt.style.use(mplhep_style)
145+
fig_ref.subplots()
146+
147+
mh.rcParams.clear()
148+
mh.style.use(str_alias)
149+
fig_test.subplots()

0 commit comments

Comments
 (0)