Skip to content

Commit 9a7ee76

Browse files
linesightclaude
andcommitted
unittests: run with default config; remove CI switch hacks; fix flaky OSR test
Reviewer feedback on cztomczak#691: the unit tests must run with the default cefpython configuration on a normal development machine, not with Chromium switches added only to make CI pass. _common.py: revert to base. Drop the Linux Initialize monkey-patch, init_gtk(), the dead _linux_needs_no_sandbox() helper, the off-screen OnBeforePopup hack and CloseBrowser(False). The library already applies the needed Linux defaults (ozone-platform=x11, windowless_rendering_enabled, no-sandbox, GDK display) in window_utils_linux.pyx, and the windowed popup-close no longer crashes, so these test-side hacks are unnecessary. main_test.py: remove the per-platform CI switch block. Keep only disable-popup-blocking, a functional requirement of the popup sub-test (Chrome blocks gesture-less window.open) rather than a CI workaround. osr_test.py: remove the Linux/macOS CI switch blocks (keep the OSR switches from Issue cztomczak#240/cztomczak#463). Fix the intermittent OnTextSelectionChanged failure: post the selection click after OnLoadEnd instead of from the first paint, let the body fill the viewport so a fixed center click reliably hits it, and drive the test with cef.MessageLoop()/QuitMessageLoop() plus a watchdog task instead of looping for a fixed duration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 126deab commit 9a7ee76

3 files changed

Lines changed: 47 additions & 198 deletions

File tree

unittests/_common.py

Lines changed: 2 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2018 CEF Python, see the Authors file.
1+
# Copyright (c) 2018 CEF Python, see the Authors file.
22
# All rights reserved. Licensed under BSD 3-clause license.
33
# Project website: https://github.com/cztomczak/cefpython
44

@@ -29,71 +29,6 @@
2929
g_on_load_end_callbacks = []
3030

3131

32-
if LINUX:
33-
# Ensure windowless_rendering_enabled=True on Linux so that JS-created
34-
# popup browsers can be configured as off-screen (see LoadHandler.
35-
# OnBeforePopup). Off-screen browsers are destroyed immediately when
36-
# DoClose returns False — no X11/GLib delete_event dispatch is needed,
37-
# avoiding the main browser's GTK window receiving the close notification.
38-
_orig_cef_initialize = cef.Initialize
39-
def _cef_initialize_linux(settings=None, switches=None, **kw):
40-
if settings is None:
41-
settings = {}
42-
settings.setdefault("windowless_rendering_enabled", True)
43-
return _orig_cef_initialize(settings, switches=switches, **kw)
44-
cef.Initialize = _cef_initialize_linux
45-
46-
47-
def init_gtk():
48-
"""Open a GDK/X11 display connection before CEF initialises.
49-
50-
On a desktop GNOME/Wayland session gdk_display_get_default() returns NULL
51-
unless a GTK application has already opened a display. Calling
52-
gtk_init(NULL, NULL) here ensures the display is available so that the
53-
browser window becomes visible. On CI (xvfb) this is a no-op.
54-
GTK is safe to initialise multiple times.
55-
"""
56-
if LINUX:
57-
try:
58-
import ctypes
59-
gtk = ctypes.CDLL("libgtk-3.so.0")
60-
gtk.gtk_init(None, None)
61-
except Exception as e:
62-
print("WARNING: gtk_init failed: %s" % e)
63-
64-
65-
def _linux_needs_no_sandbox():
66-
"""Return True if unprivileged user namespaces are not available.
67-
68-
Chrome's namespace sandbox requires clone(CLONE_NEWUSER). Two sysctls
69-
can block it:
70-
* apparmor_restrict_unprivileged_userns=1 (Ubuntu 23.10+)
71-
* unprivileged_userns_clone=0 (older Debian/Ubuntu)
72-
73-
When namespaces are unavailable and no SUID sandbox binary is present,
74-
Chrome FATALs with "No usable sandbox!" unless --no-sandbox is passed.
75-
76-
Note: do NOT pass --no-sandbox at all. It causes Chrome to skip
77-
GlobalDescriptors key 7 registration while still encoding it in
78-
--pseudonymization-salt-handle, causing a CHECK-crash in subprocesses.
79-
"""
80-
if not LINUX:
81-
return False
82-
try:
83-
with open("/proc/sys/kernel/apparmor_restrict_unprivileged_userns") as f:
84-
if f.read().strip() == "1":
85-
return True
86-
except OSError:
87-
pass
88-
try:
89-
with open("/proc/sys/kernel/unprivileged_userns_clone") as f:
90-
if f.read().strip() == "0":
91-
return True
92-
except OSError:
93-
pass
94-
return False
95-
96-
9732
def subtest_message(message):
9833
global g_subtests_ran
9934
g_subtests_ran += 1
@@ -185,10 +120,7 @@ def OnConsoleMessage(self, message, **_):
185120

186121

187122
def close_popup(global_handler, browser):
188-
# The popup was created as off-screen on Linux (see LoadHandler.OnBeforePopup).
189-
# For off-screen browsers DoClose returning False causes immediate destruction
190-
# without any GLib/X11 event dispatch, so CloseBrowser(False) works cleanly.
191-
browser.CloseBrowser(False)
123+
browser.CloseBrowser()
192124
global_handler.PopupClosed_True = True
193125

194126
# Test developer tools popup
@@ -261,20 +193,6 @@ def __init__(self, test_case, datauri):
261193
# self.OnLoadingStateChange_Start_True = False # FAILS
262194
self.OnLoadingStateChange_End_True = False
263195

264-
def OnBeforePopup(self, browser, frame, target_url, target_frame_name,
265-
target_disposition, user_gesture, popup_features,
266-
window_info_out, client, browser_settings_out,
267-
no_javascript_access_out, **_):
268-
if LINUX:
269-
# Configure JS-created popups as off-screen so they can be closed
270-
# without GLib/X11 event dispatch. For off-screen browsers CEF
271-
# destroys the browser immediately when DoClose returns False,
272-
# without sending delete_event to any parent GTK window.
273-
winfo = cef.WindowInfo()
274-
winfo.SetAsOffscreen(0)
275-
window_info_out.append(winfo)
276-
return False # Allow the popup
277-
278196
def OnLoadStart(self, browser, frame, **_):
279197
self.test_case.assertFalse(self.OnLoadStart_True)
280198
self.OnLoadStart_True = True

unittests/main_test.py

Lines changed: 5 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2016 CEF Python, see the Authors file.
1+
# Copyright (c) 2016 CEF Python, see the Authors file.
22
# All rights reserved. Licensed under BSD 3-clause license.
33
# Project website: https://github.com/cztomczak/cefpython
44

@@ -141,68 +141,11 @@ def test_main(self):
141141
if "--debug-warning" in sys.argv:
142142
settings["debug"] = True
143143
settings["log_severity"] = cef.LOGSEVERITY_WARNING
144-
# Chrome 130+ blocks window.open() called without a user gesture.
144+
# The popup sub-test opens a window via window.open() during page
145+
# load, i.e. without a user gesture, which Chrome blocks by default.
146+
# This is a functional requirement of the test itself (not a CI
147+
# workaround), so it applies on every platform.
145148
switches = {"disable-popup-blocking": ""}
146-
if LINUX:
147-
# Open a GDK/X11 display connection before CEF initialises so
148-
# that gdk_display_get_default() returns a valid display. On a
149-
# desktop session this is required for the browser window to be
150-
# visible; on xvfb (CI) it is a no-op.
151-
init_gtk()
152-
# cefpython does not ship a chrome-sandbox (setuid) binary.
153-
# Disable SUID/namespace sandboxes; Chrome falls back to seccomp-BPF
154-
# which keeps GlobalDescriptors key 7 registered for subprocesses.
155-
# Do NOT pass --no-sandbox: it skips key 7 registration but encodes
156-
# it in --pseudonymization-salt-handle, causing a CHECK-crash.
157-
switches["disable-setuid-sandbox"] = ""
158-
# /dev/shm is too small in CI containers.
159-
switches["disable-dev-shm-usage"] = ""
160-
# GPU acceleration is not available under xvfb.
161-
switches["disable-gpu"] = ""
162-
switches["disable-gpu-compositing"] = ""
163-
# Run GPU process inside the browser process so it is not
164-
# spawned during CefInitialize() where it would fail.
165-
switches["in-process-gpu"] = ""
166-
switches["no-zygote"] = ""
167-
# Force X11 rendering via XWayland. On Ubuntu 24 GNOME/Wayland
168-
# Chrome 130+ defaults to the Wayland Ozone backend when
169-
# WAYLAND_DISPLAY is set; cefpython uses raw X11 APIs so the
170-
# window would never appear. On CI (xvfb) this is a no-op.
171-
switches["ozone-platform"] = "x11"
172-
# Run the network service in-process so no utility subprocess
173-
# needs to be spawned (reduces spawn overhead on CI).
174-
# The feature string in Chrome 130+ is "NetworkServiceInProcess2".
175-
switches["enable-features"] = "NetworkServiceInProcess2"
176-
# Suppress the GNOME Keyring unlock prompt on desktop sessions.
177-
switches["password-store"] = "basic"
178-
if MAC:
179-
# cefpython does not ship a chrome-sandbox binary.
180-
switches["no-sandbox"] = ""
181-
# No real GPU available on macOS CI runners.
182-
switches["disable-gpu"] = ""
183-
switches["disable-gpu-compositing"] = ""
184-
switches["in-process-gpu"] = ""
185-
# Prevent macOS keychain authorization prompts during init
186-
# (matches CEF's own test infrastructure on macOS).
187-
switches["use-mock-keychain"] = ""
188-
# Chrome 130+ MachPortRendezvousServer registers via
189-
# bootstrap_check_in; renderer subprocesses look up the service
190-
# via bootstrap_look_up, which fails on unsigned CI processes
191-
# because Chrome gives them a restricted bootstrap namespace.
192-
# --in-process-renderer was removed from Chrome 130+.
193-
# --single-process runs the renderer in the browser process,
194-
# eliminating renderer subprocess bootstrap_look_up failures.
195-
switches["single-process"] = ""
196-
# --single-process puts the renderer's V8 in the browser process,
197-
# which requires a large contiguous CodeRange for JIT code.
198-
# On constrained CI runner images this reservation fails with an
199-
# OOM error. --jitless disables all V8 JIT compilers, eliminating
200-
# the CodeRange requirement entirely.
201-
switches["js-flags"] = "--jitless"
202-
# Run network service in-process to avoid Mach port rendezvous
203-
# failures for utility subprocesses on macOS CI runners.
204-
# The feature string in Chrome 130+ is "NetworkServiceInProcess2".
205-
switches["enable-features"] = "NetworkServiceInProcess2"
206149
cef.Initialize(settings, switches=switches)
207150
subtest_message("cef.Initialize() ok")
208151

unittests/osr_test.py

Lines changed: 40 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2018 CEF Python, see the Authors file.
1+
# Copyright (c) 2018 CEF Python, see the Authors file.
22
# All rights reserved. Licensed under BSD 3-clause license.
33
# Project website: https://github.com/cztomczak/cefpython
44

@@ -18,7 +18,11 @@
1818
<html>
1919
<head>
2020
<style type="text/css">
21-
body,html {
21+
html, body {
22+
height: 100%;
23+
margin: 0;
24+
}
25+
body {
2226
font-family: Arial;
2327
font-size: 11pt;
2428
}
@@ -115,49 +119,6 @@ def test_osr(self):
115119
# switches (which disable it) must not be passed there.
116120
switches["enable-begin-frame-scheduling"] = ""
117121
switches["disable-surfaces"] = "" # Required for PDF ext to work
118-
if LINUX:
119-
# Open a GDK/X11 display connection before CEF initialises.
120-
init_gtk()
121-
# cefpython does not ship a chrome-sandbox (setuid) binary.
122-
# Disable SUID/namespace sandboxes; Chrome falls back to seccomp-BPF
123-
# which keeps GlobalDescriptors key 7 registered for subprocesses.
124-
# Do NOT pass --no-sandbox: it skips key 7 registration but encodes
125-
# it in --pseudonymization-salt-handle, causing a CHECK-crash.
126-
switches["disable-setuid-sandbox"] = ""
127-
# /dev/shm is too small in CI containers.
128-
switches["disable-dev-shm-usage"] = ""
129-
# Run GPU process inside the browser process so it is not
130-
# spawned during CefInitialize() where it would fail.
131-
switches["in-process-gpu"] = ""
132-
switches["no-zygote"] = ""
133-
# Force X11 rendering via XWayland (see main_test.py for details).
134-
switches["ozone-platform"] = "x11"
135-
# Run the network service in-process so no utility subprocess
136-
# needs to be spawned (reduces spawn overhead on CI).
137-
# The feature string in Chrome 130+ is "NetworkServiceInProcess2".
138-
switches["enable-features"] = "NetworkServiceInProcess2"
139-
if MAC:
140-
# Prevent macOS keychain authorization prompts during init.
141-
# CEF's own test infrastructure (client_app_browser.cc) does
142-
# the same on macOS.
143-
switches["use-mock-keychain"] = ""
144-
# Chrome 130+ MachPortRendezvousServer registers its bootstrap
145-
# service as BaseBundleID()+".MachPortRendezvousServer."+pid.
146-
# Python processes with only ad-hoc code signing receive a
147-
# restricted bootstrap namespace from macOS, so renderer
148-
# subprocesses cannot bootstrap_look_up the service.
149-
# --single-process runs the renderer inside the browser process,
150-
# eliminating the subprocess bootstrap_look_up entirely.
151-
# (--in-process-renderer was removed in Chrome 130+.)
152-
switches["single-process"] = ""
153-
# --single-process puts the renderer's V8 in the browser process,
154-
# which requires a large contiguous CodeRange for JIT code.
155-
# --jitless disables V8 JIT, removing that requirement.
156-
switches["js-flags"] = "--jitless"
157-
# Run the network service in-process to avoid Mach port rendezvous
158-
# failures for the network utility subprocess on macOS.
159-
# (Feature name in Chrome 130+: "NetworkServiceInProcess2".)
160-
switches["enable-features"] = "NetworkServiceInProcess2"
161122
browser_settings = {
162123
# Tweaking OSR performance (Issue #240)
163124
"windowless_frame_rate": 30, # Default frame rate in CEF is 30
@@ -208,8 +169,20 @@ def test_osr(self):
208169
browser.SetFocus(True)
209170
browser.WasResized()
210171

211-
# Message loop
212-
run_message_loop()
172+
# Trigger the text-selection sub-test once the page has fully
173+
# loaded (registered here, executed from LoadHandler.OnLoadEnd).
174+
on_load_end(_select_h1_after_load, browser)
175+
176+
# Message loop.
177+
# The test is event-driven: RenderHandler.OnTextSelectionChanged
178+
# calls QuitMessageLoop() as soon as the <h1> selection is reported,
179+
# so the loop ends exactly when the work is done rather than after a
180+
# fixed delay (which raced the selection round-trip and made this
181+
# test flaky on CI). The watchdog task quits the loop if that event
182+
# never arrives, turning a would-be hang into a clean assert failure.
183+
cef.PostDelayedTask(cef.TID_UI, 15000, cef.QuitMessageLoop)
184+
cef.MessageLoop()
185+
subtest_message("cef.MessageLoop() ok")
213186

214187
# Close browser and clean reference
215188
browser.CloseBrowser(True)
@@ -265,8 +238,18 @@ def _OnAccessibilityLocationChange(self, **_):
265238
pass
266239

267240

241+
def _select_h1_after_load(browser):
242+
"""Register the selection click after the page has finished loading.
243+
244+
Runs from LoadHandler.OnLoadEnd, i.e. the document (and its onclick
245+
handler) is ready. The click itself is posted with a small delay so
246+
layout has been flushed to the compositor and hit-testing is reliable.
247+
"""
248+
cef.PostDelayedTask(cef.TID_UI, 250, _click_h1_to_select, browser)
249+
250+
268251
def _click_h1_to_select(browser):
269-
"""Send a real click anywhere in the viewport after layout is complete.
252+
"""Send a real click at the center of the viewport.
270253
271254
Chrome 130+ requires the Selection API to run inside a real user-gesture
272255
event handler for OnTextSelectionChanged to fire. The body has an onclick
@@ -293,6 +276,10 @@ def __init__(self, test_case):
293276
self.GetViewRect_True = False
294277
self.OnPaint_True = False
295278
self.OnTextSelectionChanged_True = False
279+
# Set once the non-empty <h1> selection has been reported. Used as
280+
# the message-loop termination condition so the test does not rely
281+
# on a fixed-duration loop (which was the source of CI flakiness).
282+
self.OnTextSelectionChanged_h1_True = False
296283

297284
def GetViewRect(self, rect_out, **_):
298285
"""Called to retrieve the view rectangle which is relative
@@ -311,10 +298,6 @@ def OnPaint(self, browser, element_type, paint_buffer, **_):
311298
if not self.OnPaint_True:
312299
self.OnPaint_True = True
313300
subtest_message("RenderHandler.OnPaint: viewport ok")
314-
# Layout is now complete. Post the click so it isn't
315-
# re-entrant with OnPaint and hit-testing is reliable.
316-
cef.PostDelayedTask(cef.TID_UI, 100, _click_h1_to_select,
317-
browser)
318301
else:
319302
raise Exception("Unsupported element_type in OnPaint")
320303

@@ -324,6 +307,11 @@ def OnTextSelectionChanged(self, selected_text, selected_range, **_):
324307
# Verify the h1 text is selected when a non-empty selection fires.
325308
self.test_case.assertEqual(selected_text,
326309
"Off-screen rendering test")
310+
self.OnTextSelectionChanged_h1_True = True
311+
# Selection round-trip complete — stop the message loop. Safe to
312+
# call from this callback (unlike closing the browser, which must
313+
# be deferred out of OnPaint/OnLoadingStateChange).
314+
cef.QuitMessageLoop()
327315

328316

329317
if __name__ == "__main__":

0 commit comments

Comments
 (0)