diff --git a/mir-ci/mir_ci/apps.py b/mir-ci/mir_ci/apps.py index 40eaf196..d222b54f 100644 --- a/mir-ci/mir_ci/apps.py +++ b/mir-ci/mir_ci/apps.py @@ -1,8 +1,9 @@ +from textwrap import dedent from typing import Any, Collection, Literal, Optional, Union import pytest -AppType = Literal["snap", "deb", "pip"] +AppType = Optional[Literal["snap", "deb", "pip"]] class App: @@ -16,7 +17,7 @@ def __init__(self, command: Collection[str], app_type: Optional[AppType] = None) def _dependency( cmd: Collection[str], - app_type: AppType, + app_type: AppType = None, snap: Optional[str] = None, channel: str = "latest/stable", debs: Collection[str] = (), @@ -63,6 +64,30 @@ def qterminal(*args: str, debs: Collection[str] = ("qterminal", "qtwayland5"), m return deb("qterminal", *args, debs=debs, marks=marks, **kwargs) +def gtkapp(*cmd, marks=(), **kwargs): + marks = ( + pytest.mark.xdg( + XDG_CONFIG_HOME={ + "glib-2.0/settings/keyfile": dedent( + """\ + [org/gnome/desktop/interface] + color-scheme='prefer-light' + gtk-theme='Adwaita' + icon-theme='Adwaita' + font-name='Ubuntu 11' + cursor-theme='Adwaita' + cursor-size=24 + font-antialiasing='grayscale' + """ + ), + }, + ), + pytest.mark.env(GSETTINGS_BACKEND="keyfile"), + *marks, + ) + return _dependency(cmd=cmd, marks=marks, **kwargs) + + def pluma(*args: str, **kwargs): return deb("pluma", *args, **kwargs) diff --git a/mir-ci/mir_ci/robot_templates/dragged_app_titlebar.png b/mir-ci/mir_ci/robot_templates/dragged_app_titlebar.png new file mode 100644 index 00000000..6c248718 Binary files /dev/null and b/mir-ci/mir_ci/robot_templates/dragged_app_titlebar.png differ diff --git a/mir-ci/mir_ci/robot_wrapper.py b/mir-ci/mir_ci/robot_wrapper.py new file mode 100644 index 00000000..7cf166ea --- /dev/null +++ b/mir-ci/mir_ci/robot_wrapper.py @@ -0,0 +1,38 @@ +from pathlib import Path + +from mir_ci import apps +from mir_ci.screencopy_tracker import ScreencopyTracker +from mir_ci.virtual_pointer import VirtualPointer + +MIR_CI_PATH = Path(__file__).parent +ROBOT_RESOURCES = MIR_CI_PATH / "robot_resources" + +ROBOT_TEMPLATE = """\ +*** Settings *** +{settings} + +*** Variables *** +{variables} + +*** Test Cases *** +{test_cases} +""" + +WAYLAND_SETTINGS = f"Resource {ROBOT_RESOURCES}/screencopy.resource" +WAYLAND_VARIABLES = f"${{template_path}}= {MIR_CI_PATH}/robot_templates" + + +class Robot(apps.App): + def __init__(self, request, test_cases, settings="", variables=""): + tmp_path = request.getfixturevalue("tmp_path") + procedure = tmp_path / f"{request.node.name}.robot" + with open(procedure, "w") as f: + f.write(ROBOT_TEMPLATE.format(test_cases=test_cases, settings=settings, variables=variables)) + super().__init__(("robot", "--outputdir", tmp_path, procedure)) + + +class WaylandRobot(Robot): + required_extensions = VirtualPointer.required_extensions + ScreencopyTracker.required_extensions + + def __init__(self, request, test_cases, settings="", variables=""): + super().__init__(request, test_cases, f"{WAYLAND_SETTINGS}\n{settings}", f"{WAYLAND_VARIABLES}\n{variables}") diff --git a/mir-ci/mir_ci/test_drag_and_drop.py b/mir-ci/mir_ci/test_drag_and_drop.py index 732cd79a..f3562ee2 100644 --- a/mir-ci/mir_ci/test_drag_and_drop.py +++ b/mir-ci/mir_ci/test_drag_and_drop.py @@ -1,51 +1,16 @@ -import tempfile from pathlib import Path from textwrap import dedent import pytest from mir_ci import apps from mir_ci.display_server import DisplayServer +from mir_ci.robot_wrapper import WaylandRobot from mir_ci.screencopy_tracker import ScreencopyTracker from mir_ci.virtual_pointer import VirtualPointer -MIR_CI_PATH = Path(__file__).parent -APP_PATH = MIR_CI_PATH / "clients/drag_and_drop_demo.py" +APP_PATH = Path(__file__).parent / "clients/drag_and_drop_demo.py" -ROBOT_TEMPLATE = """\ -*** Settings *** -{settings} -*** Variables *** -{variables} - -*** Test Cases *** -{test_case} -""" -ROBOT_SETTINGS = f"Resource {MIR_CI_PATH}/robot_resources/screencopy.resource" -ROBOT_VARIABLES = f"""\ -${{SRC_TEMPLATE}} {MIR_CI_PATH}/robot_templates/drag_and_drop_src.png -${{DST_TEMPLATE}} {MIR_CI_PATH}/robot_templates/drag_and_drop_dst.png -${{END_TEMPLATE}} {MIR_CI_PATH}/robot_templates/drag_and_drop_end.png -""" - - -@pytest.mark.xdg( - XDG_CONFIG_HOME={ - "glib-2.0/settings/keyfile": dedent( - """\ - [org/gnome/desktop/interface] - color-scheme='prefer-light' - gtk-theme='Adwaita' - icon-theme='Adwaita' - font-name='Ubuntu 11' - cursor-theme='Adwaita' - cursor-size=24 - font-antialiasing='grayscale' - """ - ), - }, -) -@pytest.mark.env(GSETTINGS_BACKEND="keyfile") @pytest.mark.parametrize( "modern_server", [ @@ -74,68 +39,60 @@ class TestDragAndDrop: @pytest.mark.parametrize( "app", [ - ("python3", APP_PATH, "--source", "pixbuf", "--target", "pixbuf", "--expect", "pixbuf"), - ("python3", APP_PATH, "--source", "text", "--target", "text", "--expect", "text"), + apps.gtkapp("python3", APP_PATH, "--source", "pixbuf", "--target", "pixbuf", "--expect", "pixbuf"), + apps.gtkapp("python3", APP_PATH, "--source", "text", "--target", "text", "--expect", "text"), ], ) - async def test_source_and_dest_match(self, modern_server, app, tmp_path) -> None: + async def test_source_and_dest_match(self, modern_server, app, request) -> None: extensions = VirtualPointer.required_extensions + ScreencopyTracker.required_extensions server_instance = DisplayServer(modern_server, add_extensions=extensions) - program = server_instance.program(apps.App(app)) + program = server_instance.program(app) robot_test_case = dedent( """\ Source and Destination Match - ${pos}= Move Pointer To ${SRC_TEMPLATE} + ${pos}= Move Pointer To ${template_path}/drag_and_drop_src.png Press LEFT Button - Walk Pointer From ${pos} To ${DST_TEMPLATE} + Walk Pointer From ${pos} To ${template_path}/drag_and_drop_dst.png Release LEFT Button """ ) - with tempfile.NamedTemporaryFile(mode="w+", suffix=".robot", buffering=1) as robot_file: - robot_file.write( - ROBOT_TEMPLATE.format(settings=ROBOT_SETTINGS, variables=ROBOT_VARIABLES, test_case=robot_test_case) - ) - robot = server_instance.program(apps.App(("robot", "-d", tmp_path, robot_file.name))) + robot = server_instance.program(WaylandRobot(request, robot_test_case)) - async with server_instance, program, robot: - await robot.wait(60) - await program.wait() + async with server_instance, program, robot: + await robot.wait(60) + await program.wait() @pytest.mark.parametrize( "app", [ - ("python3", "-u", APP_PATH, "--source", "pixbuf", "--target", "text", "--expect", "pixbuf"), - ("python3", "-u", APP_PATH, "--source", "text", "--target", "pixbuf", "--expect", "text"), - ("python3", "-u", APP_PATH, "--source", "pixbuf", "--target", "text", "--expect", "text"), - ("python3", "-u", APP_PATH, "--source", "text", "--target", "pixbuf", "--expect", "pixbuf"), + apps.gtkapp("python3", "-u", APP_PATH, "--source", "pixbuf", "--target", "text", "--expect", "pixbuf"), + apps.gtkapp("python3", "-u", APP_PATH, "--source", "text", "--target", "pixbuf", "--expect", "text"), + apps.gtkapp("python3", "-u", APP_PATH, "--source", "pixbuf", "--target", "text", "--expect", "text"), + apps.gtkapp("python3", "-u", APP_PATH, "--source", "text", "--target", "pixbuf", "--expect", "pixbuf"), ], ) - async def test_source_and_dest_mismatch(self, modern_server, app, tmp_path) -> None: + async def test_source_and_dest_mismatch(self, modern_server, app, request) -> None: extensions = VirtualPointer.required_extensions + ScreencopyTracker.required_extensions server_instance = DisplayServer(modern_server, add_extensions=extensions) - program = server_instance.program(apps.App(app)) + program = server_instance.program(app) robot_test_case = dedent( """\ Source and Destination Mismatch - ${pos}= Move Pointer To ${SRC_TEMPLATE} + ${pos}= Move Pointer To ${template_path}/drag_and_drop_src.png Press LEFT Button - ${pos}= Walk Pointer From ${pos} To ${DST_TEMPLATE} + ${pos}= Walk Pointer From ${pos} To ${template_path}/drag_and_drop_dst.png Release LEFT Button - Walk Pointer From ${pos} To ${END_TEMPLATE} + Walk Pointer From ${pos} To ${template_path}/drag_and_drop_end.png """ ) - with tempfile.NamedTemporaryFile(mode="w+", suffix=".robot", buffering=1) as robot_file: - robot_file.write( - ROBOT_TEMPLATE.format(settings=ROBOT_SETTINGS, variables=ROBOT_VARIABLES, test_case=robot_test_case) - ) - robot = server_instance.program(apps.App(("robot", "-d", tmp_path, robot_file.name))) + robot = server_instance.program(WaylandRobot(request, robot_test_case)) - async with server_instance, program, robot: - await robot.wait(60) - assert program.is_running() - await program.kill() - assert "drag-begin\ndrag-failed\nenter-notify-event: dropbox" in program.output + async with server_instance, program, robot: + await robot.wait(60) + assert program.is_running() + await program.kill() + assert "drag-begin\ndrag-failed\nenter-notify-event: dropbox" in program.output diff --git a/mir-ci/mir_ci/test_screencopy_bandwidth.py b/mir-ci/mir_ci/test_screencopy_bandwidth.py index fdab8ec9..9d2be126 100644 --- a/mir-ci/mir_ci/test_screencopy_bandwidth.py +++ b/mir-ci/mir_ci/test_screencopy_bandwidth.py @@ -1,12 +1,13 @@ import asyncio import os from pathlib import Path +from textwrap import dedent import pytest from mir_ci import SLOWDOWN, apps from mir_ci.display_server import DisplayServer +from mir_ci.robot_wrapper import WaylandRobot from mir_ci.screencopy_tracker import ScreencopyTracker -from mir_ci.virtual_pointer import Button, VirtualPointer long_wait_time = 10 @@ -68,7 +69,15 @@ async def test_inactive_app(self, record_property, server, app) -> None: await asyncio.sleep(long_wait_time) _record_properties(record_property, server, tracker, 2) - @pytest.mark.deps(debs=("libgtk-4-dev",), pip_pkgs=(("pygobject", "gi"),)) + @pytest.mark.deps( + debs=("libgtk-4-dev",), + pip_pkgs=( + ("pygobject", "gi"), + ("robotframework~=6.1.1", "robot"), + ("rpaframework", "RPA"), + ("rpaframework-recognition", "RPA.recognition"), + ), + ) @pytest.mark.parametrize( "local_server", [ @@ -77,24 +86,33 @@ async def test_inactive_app(self, record_property, server, app) -> None: apps.mir_demo_server(), ], ) - async def test_app_dragged_around(self, record_property, local_server) -> None: - async def pause(): - await asyncio.sleep(0.2) - - extensions = ScreencopyTracker.required_extensions + VirtualPointer.required_extensions - app_path = Path(__file__).parent / "clients" / "maximizing_gtk_app.py" + @pytest.mark.parametrize( + "client", + [apps.gtkapp("python3", Path(__file__).parent / "clients" / "maximizing_gtk_app.py")], + ) + async def test_app_dragged_around(self, record_property, local_server, client, request) -> None: + extensions = ScreencopyTracker.required_extensions + WaylandRobot.required_extensions server = DisplayServer(local_server, add_extensions=extensions) - app = server.program(apps.App(("python3", str(app_path)))) tracker = ScreencopyTracker(server.display_name) - pointer = VirtualPointer(server.display_name) - async with server, tracker, app, pointer: - await asyncio.sleep(2 * SLOWDOWN) # TODO: detect when the window is drawn instead - pointer.move_to_absolute(pointer.output_width / 2, 10) - await pause() - pointer.button(Button.LEFT, True) - await pause() - for y in range(4): - for x in range(4): - pointer.move_to_proportional((x + 0.5) / 4, (y + 0.5) / 4) - await pause() + + robot_test_case = dedent( + """\ + Drag app + ${pos}= Move Pointer To Template ${template_path}/dragged_app_titlebar.png + Press LEFT Button + FOR ${y} IN RANGE 4 + FOR ${x} IN RANGE 4 + ${px} evaluate ($x + 0.5) / 4 + ${py} evaluate ($y + 0.5) / 4 + Move Pointer To Proportional ${px} ${py} + Sleep 0.2 + END + END + """ + ) + + robot = server.program(WaylandRobot(request, robot_test_case)) + + async with server, tracker, server.program(client), robot: + await robot.wait() _record_properties(record_property, server, tracker, 16)