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

Run testbed with Wayland #2670

Merged
merged 10 commits into from
Jun 23, 2024
38 changes: 32 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ jobs:
strategy:
fail-fast: false
matrix:
backend: [ "macOS-x86_64", "macOS-arm64", "windows", "linux", "android", "iOS" ]
backend: [ "macOS-x86_64", "macOS-arm64", "windows", "linux-x11", "linux-wayland","android", "iOS" ]
include:
- pre-command: ""
briefcase-run-prefix: ""
Expand All @@ -208,7 +208,7 @@ jobs:
# We use a fixed Ubuntu version rather than `-latest` because at some point,
# `-latest` will be updated, but it will be a soft changeover, which would cause
# the system Python version to become inconsistent from run to run.
- backend: "linux"
- backend: "linux-x11"
platform: "linux"
runs-on: "ubuntu-22.04"
# The package list should be the same as in tutorial-0.rst, and the BeeWare
Expand All @@ -223,19 +223,45 @@ jobs:
sudo apt install -y --no-install-recommends \
blackbox pkg-config python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-webkit2-4.1

# Start Virtual X server
# Start Virtual X Server
echo "Start X server..."
Xvfb :99 -screen 0 2048x1536x24 &
sleep 1

# Start Window manager
# Start Window Mmanager
echo "Start window manager..."
DISPLAY=:99 blackbox &
sleep 1
briefcase-run-prefix: 'DISPLAY=:99'
setup-python: false # Use the system Python packages
app-user-data-path: "$HOME/.local/share/testbed"

- backend: "linux-wayland"
platform: "linux"
runs-on: "ubuntu-22.04"
# The package list should be the same as in tutorial-0.rst, and the BeeWare
# tutorial, plus mutter to provide a window manager, and libjpeg-dev for Pillow.
pre-command: |
sudo apt update -y
sudo apt install -y --no-install-recommends \
mutter pkg-config python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-webkit2-4.1

# Start Virtual X Server
echo "Start X server..."
Xvfb :99 -screen 0 2048x1536x24 &
sleep 1

# Start Window Manager
echo "Start window manager..."
# mutter is being run inside a virtual X server because mutter's headless
# mode is not compatible with Gtk
DISPLAY=:99 MUTTER_DEBUG_DUMMY_MODE_SPECS=2048x1536 \
mutter --nested --wayland --no-x11 --wayland-display toga &
sleep 1
briefcase-run-prefix: "WAYLAND_DISPLAY=toga"
setup-python: false # Use the system Python packages
app-user-data-path: "$HOME/.local/share/testbed"

- backend: "windows"
platform: "windows"
runs-on: "windows-latest"
Expand All @@ -258,7 +284,7 @@ jobs:
platform: "android"
runs-on: "ubuntu-latest"
briefcase-run-prefix: JAVA_HOME=${JAVA_HOME_17_X64}
briefcase-run-args: >
briefcase-run-args: >-
--device '{"avd":"beePhone","skin":"pixel_3a"}'
--Xemulator=-no-window
--Xemulator=-no-snapshot
Expand Down Expand Up @@ -304,7 +330,7 @@ jobs:
timeout-minutes: 15
run: |
${{ matrix.briefcase-run-prefix }} \
briefcase run ${{ matrix.platform }} --log --test ${{ matrix.briefcase-run-args }}
briefcase run ${{ matrix.platform }} --log --test ${{ matrix.briefcase-run-args }} -- --ci

- name: Upload Logs
uses: actions/[email protected]
Expand Down
1 change: 1 addition & 0 deletions android/tests_backend/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class WindowProbe(BaseProbe):
def __init__(self, app, window):
super().__init__(app)
self.native = self.app._impl.native
self.window = window

async def wait_for_window(self, message, minimize=False, full_screen=False):
await self.redraw(message)
Expand Down
1 change: 1 addition & 0 deletions changes/2668.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add Wayland to the matrix for testbed CI testing.
1 change: 1 addition & 0 deletions cocoa/tests_backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
class AppProbe(BaseProbe):
supports_key = True
supports_key_mod3 = True
supports_current_window_assignment = True

def __init__(self, app):
super().__init__()
Expand Down
2 changes: 2 additions & 0 deletions cocoa/tests_backend/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class WindowProbe(BaseProbe):
supports_move_while_hidden = True
supports_multiple_select_folder = True
supports_unminimize = True
supports_minimize = True
supports_placement = True

def __init__(self, app, window):
super().__init__()
Expand Down
6 changes: 3 additions & 3 deletions gtk/src/toga_gtk/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,13 +192,13 @@ def set_main_window(self, window):

def get_screens(self):
display = Gdk.Display.get_default()
if "WAYLAND_DISPLAY" in os.environ: # pragma: no cover
if "WAYLAND_DISPLAY" in os.environ: # pragma: no-cover-if-linux-x
# `get_primary_monitor()` doesn't work on wayland, so return as it is.
return [
ScreenImpl(native=display.get_monitor(i))
for i in range(display.get_n_monitors())
]
else:
else: # pragma: no-cover-if-linux-wayland
primary_screen = ScreenImpl(display.get_primary_monitor())
screen_list = [primary_screen] + [
ScreenImpl(native=display.get_monitor(i))
Expand Down Expand Up @@ -252,7 +252,7 @@ def show_cursor(self):
# Window control
######################################################################

def get_current_window(self):
def get_current_window(self): # pragma: no-cover-if-linux-wayland
current_window = self.native.get_active_window()._impl
return current_window if current_window.interface.visible else None

Expand Down
4 changes: 2 additions & 2 deletions gtk/src/toga_gtk/screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ def get_size(self) -> Size:
return Size(geometry.width, geometry.height)

def get_image_data(self):
if "WAYLAND_DISPLAY" in os.environ:
if "WAYLAND_DISPLAY" in os.environ: # pragma: no cover
# Not implemented on wayland due to wayland security policies.
self.interface.factory.not_implemented("Screen.get_image_data() on Wayland")
Comment on lines 33 to 36
Copy link
Member Author

@rmartin16 rmartin16 Jun 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is left as blanket "no cover" because the probe prevents fetching an image for the screen if running on Wayland. However, I imagine that's because Toga crashes out if this code is ever actually ran in practice....because the Screen.as_image() API in core sends the returned None in to toga.Image. This may not be the best UX...especially since developers may never test on Wayland before shipping...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed it's not ideal UX; returning a dummy image (blank, same size as the screen?) may be preferable. Up to you whether you roll that into this PR, or log it as a standalone issue.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created #2673 to capture this.

else:
else: # pragma: no-cover-if-linux-wayland
# Only works for Xorg
display = self.native.get_display()
screen = display.get_default_screen()
Expand Down
4 changes: 4 additions & 0 deletions gtk/tests_backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
class AppProbe(BaseProbe):
supports_key = True
supports_key_mod3 = True
# Gtk 3.24.41 ships with Ubuntu 24.04 where present() works on Wayland
supports_current_window_assignment = not (
BaseProbe.IS_WAYLAND and BaseProbe.GTK_VERSION < (3, 24, 41)
)

def __init__(self, app):
super().__init__()
Expand Down
4 changes: 4 additions & 0 deletions gtk/tests_backend/probe.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import asyncio
import os

import toga
from toga_gtk.libs import Gtk


class BaseProbe:
GTK_VERSION = Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION, Gtk.MICRO_VERSION
IS_WAYLAND = os.environ.get("WAYLAND_DISPLAY", "") != ""

def repaint_needed(self):
return Gtk.events_pending()

Expand Down
8 changes: 3 additions & 5 deletions gtk/tests_backend/screens.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import os

import pytest
from gi.repository import GdkX11

Expand All @@ -14,15 +12,15 @@ def __init__(self, screen):
self.screen = screen
self._impl = screen._impl
self.native = screen._impl.native
if "WAYLAND_DISPLAY" in os.environ:
if self.IS_WAYLAND:
# The native display type on Wayland is `__gi__.GdkWaylandMonitor`
# However, that class can't be imported directly.
pass
else:
assert isinstance(self.native, GdkX11.X11Monitor)

def get_screenshot(self, format=TogaImage):
if "WAYLAND_DISPLAY" in os.environ:
pytest.skip("Screen.as_image() is not implemented on wayland.")
if self.IS_WAYLAND:
pytest.skip("Screen.as_image() is not implemented on Wayland.")
else:
return self.screen.as_image(format=format)
7 changes: 6 additions & 1 deletion gtk/tests_backend/widgets/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ class CanvasProbe(SimpleProbe):
native_class = Gtk.DrawingArea

def reference_variant(self, reference):
if reference in {"write_text", "multiline_text"}:
if reference == "multiline_text":
if self.IS_WAYLAND:
return f"{reference}-gtk-wayland"
else:
return f"{reference}-gtk-x11"
elif reference == "write_text":
return f"{reference}-gtk"
else:
return reference
Expand Down
3 changes: 3 additions & 0 deletions gtk/tests_backend/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class WindowProbe(BaseProbe):
supports_multiple_select_folder = True
supports_move_while_hidden = False
supports_unminimize = False
# Wayland mostly prohibits interaction with the larger windowing environment
supports_minimize = not BaseProbe.IS_WAYLAND
supports_placement = not BaseProbe.IS_WAYLAND
rmartin16 marked this conversation as resolved.
Show resolved Hide resolved

def __init__(self, app, window):
super().__init__()
Expand Down
1 change: 1 addition & 0 deletions testbed/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ version = "0.0.1"
[project.optional-dependencies]
test = [
"coverage==7.5.3",
"coverage-conditional-plugin == 0.9.0",
# fonttools is only needed by Android, but we need to use
# sys.platform == 'linux' as there's no dependency identifier
# that can target Android exclusively until 3.13 lands.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 22 additions & 15 deletions testbed/tests/app/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ async def test_full_screen(app):
app.set_full_screen(app.current_window)
app.exit_full_screen()

async def test_current_window(app, main_window, main_window_probe):
async def test_current_window(app, main_window, app_probe, main_window_probe):
"""The current window can be retrieved"""
assert app.current_window == main_window

Expand Down Expand Up @@ -153,12 +153,13 @@ async def test_menu_exit(monkeypatch, app, app_probe, mock_app_exit):
mock_app_exit.assert_called_once_with()

async def test_menu_close_windows(monkeypatch, app, app_probe, mock_app_exit):
window1 = toga.Window("Test Window 1", position=(150, 150), size=(200, 200))
window2 = toga.Window("Test Window 2", position=(400, 150), size=(200, 200))
window3 = toga.Window("Test Window 3", position=(300, 400), size=(200, 200))

try:
window1 = toga.Window("Test Window 1", position=(150, 150), size=(200, 200))
window1.content = toga.Box(style=Pack(background_color=REBECCAPURPLE))
window2 = toga.Window("Test Window 2", position=(400, 150), size=(200, 200))
window2.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE))
window3 = toga.Window("Test Window 3", position=(300, 400), size=(200, 200))
window3.content = toga.Box(style=Pack(background_color=FIREBRICK))

window1.show()
Expand Down Expand Up @@ -209,8 +210,9 @@ async def test_menu_close_windows(monkeypatch, app, app_probe, mock_app_exit):
window3.close()

async def test_menu_minimize(app, app_probe):
window1 = toga.Window("Test Window 1", position=(150, 150), size=(200, 200))

try:
window1 = toga.Window("Test Window 1", position=(150, 150), size=(200, 200))
window1.content = toga.Box(style=Pack(background_color=REBECCAPURPLE))
window1.show()

Expand All @@ -228,10 +230,11 @@ async def test_menu_minimize(app, app_probe):

async def test_full_screen(app, app_probe):
"""Window can be made full screen"""
window1 = toga.Window("Test Window 1", position=(150, 150), size=(200, 200))
window2 = toga.Window("Test Window 2", position=(400, 150), size=(200, 200))

try:
window1 = toga.Window("Test Window 1", position=(150, 150), size=(200, 200))
window1.content = toga.Box(style=Pack(background_color=REBECCAPURPLE))
window2 = toga.Window("Test Window 2", position=(400, 150), size=(200, 200))
window2.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE))
window1_probe = window_probe(app, window1)
window2_probe = window_probe(app, window2)
Expand Down Expand Up @@ -351,7 +354,8 @@ async def test_show_hide_cursor(app, app_probe):
async def test_current_window(app, app_probe, main_window):
"""The current window can be retrieved."""
try:
assert app.current_window == main_window
if app_probe.supports_current_window_assignment:
assert app.current_window == main_window

# When all windows are hidden, WinForms and Cocoa return None, while GTK
# returns the last active window.
Expand All @@ -363,12 +367,13 @@ async def test_current_window(app, app_probe, main_window):
finally:
main_window.show()

window1 = toga.Window("Test Window 1", position=(150, 150), size=(200, 200))
window2 = toga.Window("Test Window 2", position=(400, 150), size=(200, 200))
window3 = toga.Window("Test Window 3", position=(300, 400), size=(200, 200))

try:
window1 = toga.Window("Test Window 1", position=(150, 150), size=(200, 200))
window1.content = toga.Box(style=Pack(background_color=REBECCAPURPLE))
window2 = toga.Window("Test Window 2", position=(400, 150), size=(200, 200))
window2.content = toga.Box(style=Pack(background_color=CORNFLOWERBLUE))
window3 = toga.Window("Test Window 3", position=(300, 400), size=(200, 200))
window3.content = toga.Box(style=Pack(background_color=FIREBRICK))

# We don't need to probe anything window specific; we just need
Expand All @@ -383,11 +388,13 @@ async def test_current_window(app, app_probe, main_window):

app.current_window = window2
await window1_probe.wait_for_window("Window 2 is current")
assert app.current_window == window2
if app_probe.supports_current_window_assignment:
assert app.current_window == window2

app.current_window = window3
await window1_probe.wait_for_window("Window 3 is current")
assert app.current_window == window3
if app_probe.supports_current_window_assignment:
assert app.current_window == window3

# app_probe.platform tests?
finally:
Expand Down Expand Up @@ -511,8 +518,8 @@ async def test_menu_visit_homepage(monkeypatch, app, app_probe):
"""The visit homepage menu item can be used"""
# If the backend defines a VISIT_HOMEPAGE command, mock the visit_homepage method,
# and rebind the visit homepage command to the visit_homepage method.
visit_homepage = Mock()
if toga.Command.VISIT_HOMEPAGE in app.commands:
visit_homepage = Mock()
monkeypatch.setattr(app, "visit_homepage", visit_homepage)
monkeypatch.setattr(
app.commands[toga.Command.VISIT_HOMEPAGE], "_action", app.visit_homepage
Expand Down Expand Up @@ -593,7 +600,7 @@ async def test_menu_items(app, app_probe):
enabled=False,
)

# Dislble the items
# Disable the items
app.disabled_cmd.enabled = False
app.no_action_cmd.enabled = False

Expand Down
19 changes: 18 additions & 1 deletion testbed/tests/testbed.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from testbed.app import main


def run_tests(app, cov, args, report_coverage, run_slow):
def run_tests(app, cov, args, report_coverage, run_slow, running_in_ci):
try:
# Wait for the app's main window to be visible.
print("Waiting for app to be ready for testing... ", end="", flush=True)
Expand All @@ -26,6 +26,8 @@ def run_tests(app, cov, args, report_coverage, run_slow):
project_path = Path(__file__).parent.parent
os.chdir(project_path)

os.environ["RUNNING_IN_CI"] = "true" if running_in_ci else ""

app.returncode = pytest.main(
[
# Output formatting
Expand Down Expand Up @@ -115,6 +117,14 @@ def run_tests(app, cov, args, report_coverage, run_slow):
branch=True,
source_pkgs=[toga_backend],
)
cov.set_option("run:plugins", ["coverage_conditional_plugin"])
cov.set_option(
"coverage_conditional_plugin:rules",
{
"no-cover-if-linux-wayland": "os_environ.get('WAYLAND_DISPLAY', '') != ''",
"no-cover-if-linux-x": "os_environ.get('WAYLAND_DISPLAY', 'not-set') == 'not-set'",
},
)
cov.start()

# Create the test app, starting the test suite as a background task
Expand All @@ -138,6 +148,12 @@ def run_tests(app, cov, args, report_coverage, run_slow):
except ValueError:
report_coverage = False

try:
args.remove("--ci")
running_in_ci = True
except ValueError:
running_in_ci = False
rmartin16 marked this conversation as resolved.
Show resolved Hide resolved

# If there are no other specified arguments, default to running the whole suite,
# and reporting coverage.
if len(args) == 0:
Expand All @@ -152,6 +168,7 @@ def run_tests(app, cov, args, report_coverage, run_slow):
args=args,
run_slow=run_slow,
report_coverage=report_coverage,
running_in_ci=running_in_ci,
)
)
# Queue a background task to run that will start the main thread. We do this,
Expand Down
Loading
Loading