Skip to content

Commit

Permalink
Merge pull request #2088 from samschott/gc-audit
Browse files Browse the repository at this point in the history
Ensure that widgets can be garbage collected on each platform
  • Loading branch information
freakboy3742 committed Sep 15, 2024
2 parents c277ca5 + 60f233c commit 3ee8b12
Show file tree
Hide file tree
Showing 28 changed files with 191 additions and 6 deletions.
1 change: 1 addition & 0 deletions changes/2088.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Test garbage collection of widgets on all platforms.
8 changes: 4 additions & 4 deletions testbed/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@

# Use this for widgets or tests which are not supported on some platforms, but could be
# supported in the future.
def skip_on_platforms(*platforms):
def skip_on_platforms(*platforms, reason=None):
current_platform = toga.platform.current_platform
if current_platform in platforms:
skip(f"not yet implemented on {current_platform}")
skip(reason or f"not yet implemented on {current_platform}")


# Use this for widgets or tests which are not supported on some platforms, and will not
# be supported in the foreseeable future.
def xfail_on_platforms(*platforms):
def xfail_on_platforms(*platforms, reason=None):
current_platform = toga.platform.current_platform
if current_platform in platforms:
skip(f"not applicable on {current_platform}")
skip(reason or f"not applicable on {current_platform}")


@fixture(autouse=True)
Expand Down
31 changes: 31 additions & 0 deletions testbed/tests/widgets/conftest.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import gc
import weakref
from unittest.mock import Mock

from pytest import fixture

import toga
from toga.style.pack import TOP

from ..conftest import skip_on_platforms, xfail_on_platforms
from .probe import get_probe


Expand Down Expand Up @@ -78,3 +81,31 @@ def verify_focus_handlers():
def verify_vertical_alignment():
"""The widget's default vertical alignment"""
return TOP


def build_cleanup_test(
widget_constructor, args=None, kwargs=None, skip_platforms=(), xfail_platforms=()
):
async def test_cleanup():
nonlocal args, kwargs

skip_on_platforms(*skip_platforms)
xfail_on_platforms(*xfail_platforms, reason="Leaks memory")

if args is None:
args = ()

if kwargs is None:
kwargs = {}

widget = widget_constructor(*args, **kwargs)
ref = weakref.ref(widget)

# Args or kwargs may hold a backref to the widget itself, for example if they
# are widget content. Ensure that they are deleted before garbage collection.
del widget, args, kwargs
gc.collect()

assert ref() is None

return test_cleanup
6 changes: 6 additions & 0 deletions testbed/tests/widgets/test_activityindicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import toga

from ..conftest import skip_on_platforms
from .conftest import build_cleanup_test
from .properties import ( # noqa: F401
test_enable_noop,
test_focus_noop,
Expand All @@ -15,6 +16,11 @@ async def widget():
return toga.ActivityIndicator()


test_cleanup = build_cleanup_test(
toga.ActivityIndicator, skip_platforms=("android", "windows")
)


async def test_start_stop(widget, probe):
"The activity indicator can be started and stopped"
# Widget should be initially stopped
Expand Down
4 changes: 4 additions & 0 deletions testbed/tests/widgets/test_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import toga
from toga.style import Pack

from .conftest import build_cleanup_test
from .properties import ( # noqa: F401
test_background_color,
test_background_color_reset,
Expand All @@ -15,3 +16,6 @@
@pytest.fixture
async def widget():
return toga.Box(style=Pack(width=100, height=200))


test_cleanup = build_cleanup_test(toga.Box, xfail_platforms=("iOS",))
6 changes: 6 additions & 0 deletions testbed/tests/widgets/test_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from ..assertions import assert_color
from ..data import TEXTS
from .conftest import build_cleanup_test
from .properties import ( # noqa: F401
test_background_color,
test_background_color_reset,
Expand All @@ -31,6 +32,11 @@ async def widget():
return toga.Button("Hello")


test_cleanup = build_cleanup_test(
toga.Button, args=("Hello",), skip_platforms=("android",)
)


async def test_text(widget, probe):
"The text displayed on a button can be changed"
initial_height = probe.height
Expand Down
4 changes: 4 additions & 0 deletions testbed/tests/widgets/test_canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from toga.fonts import BOLD
from toga.style.pack import SYSTEM, Pack

from .conftest import build_cleanup_test
from .properties import ( # noqa: F401
test_background_color,
test_background_color_reset,
Expand Down Expand Up @@ -108,6 +109,9 @@ def assert_pixel(image, x, y, color):
assert image.getpixel((x, y)) == color


test_cleanup = build_cleanup_test(toga.Canvas, xfail_platforms=("android",))


async def test_resize(widget, probe, on_resize_handler):
"Resizing the widget causes on-resize events"
# Make the canvas visible against window background.
Expand Down
8 changes: 8 additions & 0 deletions testbed/tests/widgets/test_dateinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import toga

from ..conftest import skip_on_platforms
from .conftest import build_cleanup_test
from .properties import ( # noqa: F401
test_background_color,
test_background_color_reset,
Expand Down Expand Up @@ -82,6 +83,13 @@ async def widget():
return toga.DateInput()


test_cleanup = build_cleanup_test(
toga.DateInput,
skip_platforms=("macOS", "iOS", "linux"),
xfail_platforms=("android",),
)


async def test_init():
"Properties can be set in the constructor"
skip_on_platforms("macOS", "iOS", "linux")
Expand Down
6 changes: 6 additions & 0 deletions testbed/tests/widgets/test_detailedlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from toga.sources import ListSource
from toga.style.pack import Pack

from .conftest import build_cleanup_test
from .properties import ( # noqa: F401
test_enable_noop,
test_flex_widget_size,
Expand Down Expand Up @@ -74,6 +75,11 @@ async def widget(
)


test_cleanup = build_cleanup_test(
toga.DetailedList, xfail_platforms=("android", "linux")
)


async def test_scroll(widget, probe):
"""The detailedList can be scrolled"""

Expand Down
4 changes: 4 additions & 0 deletions testbed/tests/widgets/test_divider.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from toga.constants import Direction
from toga.style.pack import COLUMN, ROW

from .conftest import build_cleanup_test
from .properties import ( # noqa: F401
test_enable_noop,
test_focus_noop,
Expand All @@ -15,6 +16,9 @@ async def widget():
return toga.Divider()


test_cleanup = build_cleanup_test(toga.Divider, xfail_platforms=("iOS",))


async def test_directions(widget, probe):
"The divider has the right size as direction is changed"
# Widget should be initially horizontal.
Expand Down
6 changes: 6 additions & 0 deletions testbed/tests/widgets/test_imageview.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import toga
from toga.style.pack import COLUMN, ROW

from .conftest import build_cleanup_test
from .properties import ( # noqa: F401
test_background_color,
test_background_color_reset,
Expand All @@ -17,6 +18,11 @@ async def widget():
return toga.ImageView(image="resources/sample.png")


test_cleanup = build_cleanup_test(
toga.ImageView, kwargs={"image": "resources/sample.png"}, xfail_platforms=("iOS",)
)


async def test_implicit_size(widget, probe, container_probe):
"""If the image view size is implicit, the image provides flexible size hints."""

Expand Down
4 changes: 4 additions & 0 deletions testbed/tests/widgets/test_label.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import toga

from .conftest import build_cleanup_test
from .properties import ( # noqa: F401
test_alignment,
test_background_color,
Expand All @@ -24,6 +25,9 @@ async def widget():
return toga.Label("hello, this is a label")


test_cleanup = build_cleanup_test(toga.Label, args=("hello, this is a label",))


async def test_multiline(widget, probe):
"""If the label contains multiline text, it resizes vertically."""

Expand Down
4 changes: 4 additions & 0 deletions testbed/tests/widgets/test_mapview.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import toga
from toga.style import Pack

from .conftest import build_cleanup_test
from .properties import ( # noqa: F401
test_flex_widget_size,
)
Expand Down Expand Up @@ -57,6 +58,9 @@ async def widget(on_select):
toga.App.app._gc_protector.append(widget)


test_cleanup = build_cleanup_test(toga.MapView, xfail_platforms=("android",))


# The next two tests fail about 75% of the time in the macOS x86_64 CI configuration.
# The failure mode appears to be that the widget *exists*, but doesn't respond to
# changes in location or zoom. I've been unable to reproduce this in actual testing on
Expand Down
6 changes: 6 additions & 0 deletions testbed/tests/widgets/test_multilinetextinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import toga
from toga.style import Pack

from .conftest import build_cleanup_test
from .properties import ( # noqa: F401
test_alignment,
test_background_color,
Expand Down Expand Up @@ -42,6 +43,11 @@ def verify_font_sizes():
return False, False


test_cleanup = build_cleanup_test(
toga.MultilineTextInput, xfail_platforms=("android", "linux")
)


async def test_scroll_position(widget, probe):
"The widget can be programmatically scrolled."
# The document initially fits within the visible area.
Expand Down
4 changes: 4 additions & 0 deletions testbed/tests/widgets/test_numberinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import toga

from ..conftest import skip_on_platforms
from .conftest import build_cleanup_test
from .properties import ( # noqa: F401
test_alignment,
test_background_color,
Expand Down Expand Up @@ -41,6 +42,9 @@ def verify_focus_handlers():
return False


test_cleanup = build_cleanup_test(toga.NumberInput, xfail_platforms=("android",))


async def test_on_change_handler(widget, probe):
"The on_change handler is triggered when the user types."
widget.step = "0.01"
Expand Down
9 changes: 9 additions & 0 deletions testbed/tests/widgets/test_optioncontainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from toga.colors import CORNFLOWERBLUE, GOLDENROD, REBECCAPURPLE, SEAGREEN
from toga.style.pack import Pack

from .conftest import build_cleanup_test
from .probe import get_probe
from .properties import ( # noqa: F401
test_enable_noop,
Expand Down Expand Up @@ -71,6 +72,14 @@ async def widget(content1, content2, content3, on_select_handler):
)


test_cleanup = build_cleanup_test(
# Pass a function here to prevent init of toga.Box() in a different thread than
# toga.OptionContainer. This would raise a runtime error on Windows.
lambda: toga.OptionContainer(content=[("Tab 1", toga.Box())]),
xfail_platforms=("android", "iOS", "linux"),
)


async def test_select_tab(
widget,
probe,
Expand Down
4 changes: 4 additions & 0 deletions testbed/tests/widgets/test_passwordinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import toga

from .conftest import build_cleanup_test
from .properties import ( # noqa: F401
test_alignment,
test_background_color,
Expand Down Expand Up @@ -44,6 +45,9 @@ def verify_font_sizes():
return False, True


test_cleanup = build_cleanup_test(toga.PasswordInput, xfail_platforms=("android",))


async def test_value_hidden(widget, probe):
"Value should always be hidden in a PasswordInput"
assert probe.value_hidden
Expand Down
4 changes: 4 additions & 0 deletions testbed/tests/widgets/test_progressbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import toga

from .conftest import build_cleanup_test
from .properties import ( # noqa: F401
test_enable_noop,
test_flex_horizontal_widget_size,
Expand All @@ -19,6 +20,9 @@ async def widget():
return toga.ProgressBar(max=100, value=5)


test_cleanup = build_cleanup_test(toga.ProgressBar)


async def test_start_stop_determinate(widget, probe):
"A determinate progress bar can be started and stopped"
# Widget should be initially stopped and determinate
Expand Down
9 changes: 9 additions & 0 deletions testbed/tests/widgets/test_scrollcontainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from toga.colors import CORNFLOWERBLUE, REBECCAPURPLE, TRANSPARENT
from toga.style.pack import COLUMN, ROW, Pack

from .conftest import build_cleanup_test
from .properties import ( # noqa: F401
test_background_color,
test_background_color_reset,
Expand Down Expand Up @@ -72,6 +73,14 @@ async def widget(content, on_scroll):
)


test_cleanup = build_cleanup_test(
# Pass a function here to prevent init of toga.Box() in a different thread than
# toga.ScrollContainer. This would raise a runtime error on Windows.
lambda: toga.ScrollContainer(content=toga.Box()),
xfail_platforms=("android", "iOS", "linux"),
)


async def test_clear_content(widget, probe, small_content):
"Widget content can be cleared and reset"
assert probe.document_width == probe.width - probe.scrollbar_inset
Expand Down
Loading

0 comments on commit 3ee8b12

Please sign in to comment.