Skip to content

Commit dcc06a1

Browse files
authored
Run examples on ci (#23)
1 parent 748c6f3 commit dcc06a1

File tree

7 files changed

+166
-1
lines changed

7 files changed

+166
-1
lines changed

.github/workflows/ci.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,45 @@ jobs:
9595
run: |
9696
pytest -v tests
9797
98+
test-examples-build:
99+
name: Test examples ${{ matrix.pyversion }}
100+
runs-on: ${{ matrix.os }}
101+
strategy:
102+
fail-fast: false
103+
matrix:
104+
include:
105+
- os: ubuntu-latest
106+
pyversion: '3.10'
107+
- os: ubuntu-latest
108+
pyversion: '3.12'
109+
steps:
110+
- uses: actions/checkout@v4
111+
- name: Set up Python
112+
uses: actions/setup-python@v5
113+
with:
114+
python-version: 3.12
115+
- name: Install llvmpipe and lavapipe for offscreen canvas
116+
run: |
117+
sudo apt-get update -y -qq
118+
sudo apt-get install --no-install-recommends -y libegl1-mesa-dev libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers
119+
- name: Install dev dependencies
120+
run: |
121+
python -m pip install --upgrade pip
122+
pip install -e .[examples]
123+
- name: Show wgpu backend
124+
run: |
125+
python -c "from examples.tests.test_examples import adapter_summary; print(adapter_summary)"
126+
- name: Test examples
127+
env:
128+
PYGFX_EXPECT_LAVAPIPE: true
129+
run: |
130+
pytest -v examples
131+
- uses: actions/upload-artifact@v4
132+
if: ${{ failure() }}
133+
with:
134+
name: screenshots{{ matrix.pyversion }}
135+
path: examples/screenshots
136+
98137
test-pyinstaller:
99138
name: Test pyinstaller
100139
runs-on: ubuntu-latest

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
nogit/
33
docs/gallery/
44
docs/sg_execution_times.rst
5+
examples/screenshots/
56

67
# Byte-compiled / optimized / DLL files
78
__pycache__/

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,5 @@ This code is distributed under the 2-clause BSD license.
120120
* Use `ruff check` to check for linting errors.
121121
* Optionally, if you install [pre-commit](https://github.com/pre-commit/pre-commit/) hooks with `pre-commit install`, lint fixes and formatting will be automatically applied on `git commit`.
122122
* Use `pytest tests` to run the tests.
123+
* Use `pytest examples` to run a subset of the examples.
124+

examples/cube_auto.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
Run a wgpu example on an automatically selected backend.
66
"""
77

8+
# run_example = true
9+
810
from rendercanvas.auto import RenderCanvas, run
911

1012
from rendercanvas.utils.cube import setup_drawing_sync

examples/noise.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
Simple example that uses the bitmap-context to show images of noise.
66
"""
77

8+
# run_example = true
9+
810
import numpy as np
911
from rendercanvas.auto import RenderCanvas, loop
1012

examples/tests/test_examples.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""
2+
Test that the examples run without error.
3+
"""
4+
5+
import os
6+
import sys
7+
import importlib
8+
from pathlib import Path
9+
10+
import imageio.v2 as iio
11+
import numpy as np
12+
import pytest
13+
import wgpu
14+
15+
16+
ROOT = Path(__file__).parent.parent.parent # repo root
17+
examples_dir = ROOT / "examples"
18+
screenshots_dir = examples_dir / "screenshots"
19+
20+
21+
def find_examples(query=None, negative_query=None, return_stems=False):
22+
result = []
23+
for example_path in examples_dir.glob("*.py"):
24+
example_code = example_path.read_text()
25+
query_match = query is None or query in example_code
26+
negative_query_match = (
27+
negative_query is None or negative_query not in example_code
28+
)
29+
if query_match and negative_query_match:
30+
result.append(example_path)
31+
result = list(sorted(result))
32+
if return_stems:
33+
result = [r for r in result]
34+
return result
35+
36+
37+
def get_default_adapter_summary():
38+
"""Get description of adapter, or None when no adapter is available."""
39+
try:
40+
adapter = wgpu.gpu.request_adapter_sync()
41+
except RuntimeError:
42+
return None # lib not available, or no adapter on this system
43+
return adapter.summary
44+
45+
46+
adapter_summary = get_default_adapter_summary()
47+
can_use_wgpu_lib = bool(adapter_summary)
48+
is_ci = bool(os.getenv("CI", None))
49+
50+
51+
is_lavapipe = adapter_summary and all(
52+
x in adapter_summary.lower() for x in ("llvmpipe", "vulkan")
53+
)
54+
55+
if not can_use_wgpu_lib:
56+
pytest.skip("Skipping tests that need the wgpu lib", allow_module_level=True)
57+
58+
59+
# run all tests unless they opt out
60+
examples_to_run = find_examples(query="# run_example = true", return_stems=True)
61+
62+
63+
def import_from_path(module_name, filename):
64+
spec = importlib.util.spec_from_file_location(module_name, filename)
65+
module = importlib.util.module_from_spec(spec)
66+
spec.loader.exec_module(module)
67+
68+
# With this approach the module is not added to sys.modules, which
69+
# is great, because that way the gc can simply clean up when we lose
70+
# the reference to the module
71+
assert module.__name__ == module_name
72+
assert module_name not in sys.modules
73+
74+
return module
75+
76+
77+
@pytest.fixture
78+
def force_offscreen():
79+
"""Force the offscreen canvas to be selected by the auto gui module."""
80+
os.environ["RENDERCANVAS_FORCE_OFFSCREEN"] = "true"
81+
try:
82+
yield
83+
finally:
84+
del os.environ["RENDERCANVAS_FORCE_OFFSCREEN"]
85+
86+
87+
@pytest.mark.skipif(not os.getenv("CI"), reason="Not on CI")
88+
def test_that_we_are_on_lavapipe():
89+
print(adapter_summary)
90+
assert is_lavapipe
91+
92+
93+
@pytest.mark.parametrize("filename", examples_to_run, ids=lambda x: x.stem)
94+
def test_examples_compare(filename, force_offscreen):
95+
"""Run every example marked to compare its result against a reference screenshot."""
96+
check_example(filename)
97+
98+
99+
def check_example(filename):
100+
# import the example module
101+
module = import_from_path(filename.stem, filename)
102+
103+
# render a frame
104+
img = np.asarray(module.canvas.draw())
105+
106+
# check if _something_ was rendered
107+
assert img is not None and img.size > 0
108+
109+
# store screenshot
110+
screenshots_dir.mkdir(exist_ok=True)
111+
screenshot_path = screenshots_dir / f"{filename.stem}.png"
112+
iio.imsave(screenshot_path, img)
113+
114+
115+
if __name__ == "__main__":
116+
# Enable tweaking in an IDE by running in an interactive session.
117+
os.environ["RENDERCANVAS_FORCE_OFFSCREEN"] = "true"
118+
for name in examples_to_run:
119+
check_example(name)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jupyter = ["jupyter_rfb>=0.4.2"]
2525
glfw = ["glfw>=1.9"]
2626
# For devs / ci
2727
lint = ["ruff", "pre-commit"]
28-
examples = ["numpy", "wgpu", "glfw", "pyside6"]
28+
examples = ["numpy", "wgpu", "glfw", "pyside6", "imageio", "pytest"]
2929
docs = ["sphinx>7.2", "sphinx_rtd_theme", "sphinx-gallery", "numpy", "wgpu"]
3030
tests = ["pytest", "numpy", "wgpu", "glfw"]
3131
dev = ["rendercanvas[lint,tests,examples,docs]"]

0 commit comments

Comments
 (0)