Skip to content

Commit aa12040

Browse files
linesightclaude
andcommitted
Add a headless CI smoke test for the built wheel
Add tools/smoke_test.py, a harness that runs the standalone examples (hello_world.py, tutorial.py) each in its own subprocess and checks the exit code. The example files run unmodified: a small bootstrap monkey-patches, before each example runs, cef.Initialize() to merge in the per-platform switches the unit tests use (so it runs headless under CI: Xvfb on Linux, single-process on macOS for unsigned CI processes) and cef.MessageLoop() to auto-close after a few seconds. Wire it into the Linux, macOS and Windows CI workflows as a separate step after the unit tests, so a broken wheel is caught before it is published as an artifact. tools/run_examples.py is left untouched for interactive use. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 519d747 commit aa12040

4 files changed

Lines changed: 152 additions & 0 deletions

File tree

.github/workflows/ci-linux.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ jobs:
137137
pip install build/dist/*.whl
138138
xvfb-run python unittests/_test_runner.py
139139
140+
- name: Smoke-test the wheel
141+
timeout-minutes: 5
142+
run: xvfb-run python tools/smoke_test.py
143+
140144
- name: Upload wheel artifact
141145
uses: actions/upload-artifact@v7
142146
with:

.github/workflows/ci-macos.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,11 @@ jobs:
153153
pip install build/dist/*.whl
154154
python unittests/_test_runner.py
155155
156+
- name: Smoke-test the wheel
157+
timeout-minutes: 5
158+
# CFProcessPath is inherited from the app-bundle step above.
159+
run: python tools/smoke_test.py
160+
156161
- name: Upload wheel artifact
157162
uses: actions/upload-artifact@v7
158163
with:

.github/workflows/ci-windows.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ jobs:
119119
pip install (Get-ChildItem build/dist/*.whl | Select-Object -First 1).FullName
120120
python unittests/_test_runner.py
121121
122+
- name: Smoke-test the wheel
123+
timeout-minutes: 5
124+
run: python tools/smoke_test.py
125+
122126
- name: Upload wheel artifact
123127
uses: actions/upload-artifact@v7
124128
with:

tools/smoke_test.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2026 CEF Python, see the Authors file.
3+
# All rights reserved. Licensed under BSD 3-clause license.
4+
# Project website: https://github.com/cztomczak/cefpython
5+
6+
"""Headless CI smoke-test harness for the standalone examples.
7+
8+
Runs each example in examples/ in its own subprocess (CEF cannot be
9+
re-initialized within a single process) and checks the exit code. The example
10+
files are run *unmodified*: a small bootstrap monkey-patches, before the example
11+
runs, cef.Initialize() to merge in the platform-specific switches the unit tests
12+
use (so it works headless under CI: Xvfb on Linux, single-process on macOS for
13+
unsigned CI processes, etc.) and cef.MessageLoop() to auto-close after a few
14+
seconds so the example exits on its own.
15+
16+
This is separate from tools/run_examples.py, which stays for interactive use.
17+
18+
Exit code:
19+
0 every example launched and exited 0
20+
1 one or more examples failed, timed out, or crashed
21+
22+
Usage (CI):
23+
Linux: xvfb-run python tools/smoke_test.py
24+
macOS/Windows: python tools/smoke_test.py
25+
"""
26+
import json
27+
import os
28+
import subprocess
29+
import sys
30+
31+
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
32+
EXAMPLES_DIR = os.path.join(REPO_ROOT, "examples")
33+
34+
# Pure-cefpython examples (no third-party GUI toolkit) that drive cef.MessageLoop().
35+
EXAMPLES = ["hello_world.py", "tutorial.py"]
36+
37+
# The example closes itself this long after its message loop starts.
38+
AUTO_CLOSE_MS = 5000
39+
# Backstop against a hung example; CI also caps the step via timeout-minutes.
40+
PER_EXAMPLE_TIMEOUT_S = 90
41+
42+
LINUX = sys.platform.startswith("linux")
43+
MAC = sys.platform == "darwin"
44+
45+
46+
def _ci_switches():
47+
"""Per-platform switches needed to run headless / unsigned under CI.
48+
Mirrors the switches applied by unittests/main_test.py."""
49+
if LINUX:
50+
return {
51+
"disable-setuid-sandbox": "",
52+
"disable-dev-shm-usage": "",
53+
"disable-gpu": "",
54+
"disable-gpu-compositing": "",
55+
"in-process-gpu": "",
56+
"no-zygote": "",
57+
"ozone-platform": "x11",
58+
"enable-features": "NetworkServiceInProcess2",
59+
"password-store": "basic",
60+
}
61+
if MAC:
62+
return {
63+
"no-sandbox": "",
64+
"disable-gpu": "",
65+
"disable-gpu-compositing": "",
66+
"in-process-gpu": "",
67+
"use-mock-keychain": "",
68+
# An unsigned CI process can't spawn the renderer subprocess (Mach
69+
# port rendezvous fails), so run it in the browser process instead.
70+
"single-process": "",
71+
"js-flags": "--jitless",
72+
"enable-features": "NetworkServiceInProcess2",
73+
}
74+
return {} # Windows needs no special switches.
75+
76+
77+
# Bootstrap run inside each example's subprocess. Patches the two entry points
78+
# the examples use, then runs the example file as __main__. argv:
79+
# [1] = JSON of the CI switches, [2] = example .py path, [3] = auto-close ms
80+
_BOOTSTRAP = r"""
81+
import json, runpy, sys
82+
from cefpython3 import cefpython as cef
83+
_switches = json.loads(sys.argv[1])
84+
_example = sys.argv[2]
85+
_auto_close_ms = int(sys.argv[3])
86+
87+
_orig_initialize = cef.Initialize
88+
def _initialize(*args, **kwargs):
89+
user = kwargs.pop("switches", None)
90+
if user is None:
91+
user = kwargs.pop("commandLineSwitches", None)
92+
merged = dict(user or {})
93+
merged.update(_switches)
94+
return _orig_initialize(*args, switches=merged, **kwargs)
95+
cef.Initialize = _initialize
96+
97+
_orig_message_loop = cef.MessageLoop
98+
def _message_loop():
99+
cef.PostDelayedTask(cef.TID_UI, _auto_close_ms, cef.QuitMessageLoop)
100+
_orig_message_loop()
101+
cef.MessageLoop = _message_loop
102+
103+
sys.argv = [_example]
104+
runpy.run_path(_example, run_name="__main__")
105+
"""
106+
107+
108+
def main():
109+
switches_json = json.dumps(_ci_switches())
110+
failures = []
111+
for name in EXAMPLES:
112+
path = os.path.join(EXAMPLES_DIR, name)
113+
print("[smoke_test] running examples/{0} ...".format(name), flush=True)
114+
cmd = [sys.executable, "-c", _BOOTSTRAP,
115+
switches_json, path, str(AUTO_CLOSE_MS)]
116+
try:
117+
# cwd = examples/ so `import cefpython3` resolves to the installed
118+
# wheel (site-packages) rather than the repo's build directory.
119+
ret = subprocess.run(cmd, cwd=EXAMPLES_DIR,
120+
timeout=PER_EXAMPLE_TIMEOUT_S)
121+
except subprocess.TimeoutExpired:
122+
print("[smoke_test] FAIL examples/{0} (timeout)".format(name))
123+
failures.append(name)
124+
continue
125+
if ret.returncode == 0:
126+
print("[smoke_test] OK examples/{0}".format(name))
127+
else:
128+
print("[smoke_test] FAIL examples/{0} (exit {1})".format(
129+
name, ret.returncode))
130+
failures.append(name)
131+
132+
if failures:
133+
print("[smoke_test] FAILED: " + ", ".join(failures))
134+
sys.exit(1)
135+
print("[smoke_test] OK: all examples passed")
136+
137+
138+
if __name__ == "__main__":
139+
main()

0 commit comments

Comments
 (0)