Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure that widgets can be garbage collected on each platform #2088

Merged
merged 8 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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