Skip to content

Commit

Permalink
Prevent garbage collection of WebView during testbed testing
Browse files Browse the repository at this point in the history
  • Loading branch information
rmartin16 committed Jun 15, 2024
1 parent 1060842 commit cf400d2
Show file tree
Hide file tree
Showing 4 changed files with 17 additions and 27 deletions.
1 change: 1 addition & 0 deletions changes/2648.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The testbed testing strategy for MapView and WebView were updated to avoid Python crashes from WebKit.
4 changes: 4 additions & 0 deletions testbed/src/testbed/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@


class Testbed(toga.App):
# Objects can be added to this list to avoid them being garbage collected in the
# middle of the tests running. This is problematic, at least, for WebView (#2648).
_gc_protector = []

def startup(self):
# Set a default return code for the app, so that a value is
# available if the app exits for a reason other than the test
Expand Down
20 changes: 6 additions & 14 deletions testbed/tests/widgets/test_mapview.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import asyncio
import gc
import platform
from time import time
from unittest.mock import Mock
Expand Down Expand Up @@ -32,14 +31,6 @@ async def on_select():

@pytest.fixture
async def widget(on_select):
if toga.platform.current_platform == "linux":
# On Gtk, ensure that any WebViews from a previous test runs have been garbage
# collected. This prevents a segfault at GC time likely coming from the test
# suite running in a thread and Gtk WebViews sharing resources between
# instances. We perform the GC run here since pytest fixtures make earlier
# cleanup difficult.
gc.collect()

widget = toga.MapView(style=Pack(flex=1), on_select=on_select)

# Some implementations of MapView are a WebView wearing a trenchcoat.
Expand All @@ -58,11 +49,12 @@ async def widget(on_select):
yield widget

if toga.platform.current_platform == "linux":
# On Gtk, ensure that the MapView is garbage collection before the next test
# case. This prevents a segfault at GC time likely coming from the test suite
# running in a thread and Gtk WebViews sharing resources between instances.
del widget
gc.collect()
# On Gtk, ensure that the MapView evades garbage collection by keeping a
# reference to it in the app. The WebKit2 WebView will raise a SIGABRT if the
# thread disposing of it is not the same thread running the event loop. Since
# garbage collection for the WebView can run in either thread, just defer GC
# for it until the testing thread joins.
toga.App.app._gc_protector.append(widget)


# The next two tests fail about 75% of the time in the macOS x86_64 CI configuration.
Expand Down
19 changes: 6 additions & 13 deletions testbed/tests/widgets/test_webview.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import asyncio
import gc
from asyncio import wait_for
from contextlib import nullcontext
from time import time
Expand Down Expand Up @@ -79,13 +78,6 @@ async def on_load():

@pytest.fixture
async def widget(on_load):
if toga.platform.current_platform == "linux":
# On Gtk, ensure that the WebView from a previous test run is garbage collected.
# This prevents a segfault at GC time likely coming from the test suite running
# in a thread and Gtk WebViews sharing resources between instances. We perform
# the GC run here since pytest fixtures make earlier cleanup difficult.
gc.collect()

widget = toga.WebView(style=Pack(flex=1), on_webview_load=on_load)
# We shouldn't be able to get a callback until at least one tick of the event loop
# has completed.
Expand Down Expand Up @@ -114,11 +106,12 @@ async def widget(on_load):
yield widget

if toga.platform.current_platform == "linux":
# On Gtk, ensure that the WebView is garbage collection before the next test
# case. This prevents a segfault at GC time likely coming from the test suite
# running in a thread and Gtk WebViews sharing resources between instances.
del widget
gc.collect()
# On Gtk, ensure that the MapView evades garbage collection by keeping a
# reference to it in the app. The WebKit2 WebView will raise a SIGABRT if the
# thread disposing of it is not the same thread running the event loop. Since
# garbage collection for the WebView can run in either thread, just defer GC
# for it until the testing thread joins.
toga.App.app._gc_protector.append(widget)


async def test_set_url(widget, probe, on_load):
Expand Down

0 comments on commit cf400d2

Please sign in to comment.