[file-roller/wip/jtojnar/dogtail-tests: 1/4] Add basic integration test




commit 965503030c475ae203e8537951c15c1a866f329f
Author: Jan Tojnar <jtojnar gmail com>
Date:   Tue Apr 19 17:03:43 2022 +0200

    Add basic integration test

 .gitignore              |   2 +
 data/meson.build        |  11 +++-
 default.nix             |   6 ++
 meson.build             |   2 +
 meson_options.txt       |   6 ++
 tests/basic.py          |  25 ++++++++
 tests/data/texts.tar.gz | Bin 0 -> 220 bytes
 tests/meson.build       |  37 ++++++++++++
 tests/testutil.py       | 158 ++++++++++++++++++++++++++++++++++++++++++++++++
 9 files changed, 246 insertions(+), 1 deletion(-)
---
diff --git a/.gitignore b/.gitignore
index cd958352..835002c6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,5 @@
 .vscode/
 build/
 po/file-roller.pot
+__pycache__/
+*.pyc
diff --git a/data/meson.build b/data/meson.build
index 54adbd86..0682b1a6 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -1,9 +1,18 @@
 po_dir = join_paths(meson.project_source_root(), 'po')
 
-install_data('org.gnome.FileRoller.gschema.xml',
+settings_schema = 'org.gnome.FileRoller.gschema.xml'
+install_data(
+  settings_schema,
   install_dir : join_paths(datadir, 'glib-2.0', 'schemas')
 )
 
+# Required by tests.
+compiled_schemas = gnome.compile_schemas(
+  depend_files: [
+    settings_schema,
+  ],
+)
+
 install_data('packages.match',
   install_dir : join_paths(datadir, meson.project_name())
 )
diff --git a/default.nix b/default.nix
index d3b581ce..83109e2c 100644
--- a/default.nix
+++ b/default.nix
@@ -96,6 +96,12 @@ makeDerivation rec {
     meson
     ninja
     pkg-config
+
+    # For tests
+    python3.pkgs.dogtail
+    python3.pkgs.pygobject3
+    gobject-introspection # for finding typelibs
+
     wrapGAppsHook
   ] ++ lib.optionals shell [
     niv
diff --git a/meson.build b/meson.build
index 4bb09ab9..26df4830 100644
--- a/meson.build
+++ b/meson.build
@@ -127,6 +127,7 @@ if build_nautilus_actions
 endif
 subdir('po')
 subdir('src')
+subdir('tests')
 
 gnome.post_install(
   gtk_update_icon_cache: true,
@@ -141,6 +142,7 @@ summary = [
   '',
   '           project: @0@ @1@'.format(meson.project_name(), meson.project_version()),
   '            prefix: @0@'.format(prefix),
+  '     dogtail tests: @0@'.format(enable_dogtail_tests),
   '  nautilus actions: @0@'.format(build_nautilus_actions),
   '        packagekit: @0@'.format(get_option('packagekit')),
   '        libarchive: @0@'.format(use_libarchive),
diff --git a/meson_options.txt b/meson_options.txt
index 6040c516..671e2b2a 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -1,3 +1,9 @@
+option('dogtail',
+  type : 'feature',
+  value : 'disabled',
+  description : 'Enable dogtail-based integration tests',
+)
+
 option('run-in-place', 
   type : 'boolean', 
   value : false, 
diff --git a/tests/basic.py b/tests/basic.py
new file mode 100644
index 00000000..3f88ec23
--- /dev/null
+++ b/tests/basic.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+
+from gi.repository import Gio, GLib
+from pathlib import Path
+from testutil import test_file_roller
+
+with test_file_roller() as (app, app_tree):
+    app.open_file(Path(__file__).parent / "data" / "texts.tar.gz")
+
+    win = app_tree.get_window("texts.tar.gz")
+
+    file_listing = win.child(roleName="table")
+    assert (
+        file_listing.get_n_rows() == 1
+    ), "The archive should contain single directory in top-level"
+
+    texts_directory = file_listing.child("texts")
+    texts_directory.doubleClick()
+
+    assert (
+        file_listing.get_n_rows() == 2
+    ), "The archive should contain two files inside texts/ directory"
+
+    hello_txt = file_listing.child("hello.txt")
+    hello_txt.doubleClick()
diff --git a/tests/data/texts.tar.gz b/tests/data/texts.tar.gz
new file mode 100644
index 00000000..3da452c3
Binary files /dev/null and b/tests/data/texts.tar.gz differ
diff --git a/tests/meson.build b/tests/meson.build
new file mode 100644
index 00000000..e1b6c238
--- /dev/null
+++ b/tests/meson.build
@@ -0,0 +1,37 @@
+pymod = import('python')
+
+python3_for_dogtail_tests = pymod.find_installation(
+  'python3',
+  modules: [
+    'dogtail',
+    'pyatspi',
+    'gi',
+    'gi.repository.Gtk',
+  ],
+  required: get_option('dogtail'),
+)
+
+enable_dogtail_tests = python3_for_dogtail_tests.found()
+
+if enable_dogtail_tests
+  test_env = [
+    'LC_ALL=C',
+    'G_TEST_SRCDIR=' + meson.current_source_dir(),
+    'G_TEST_BUILDDIR=' + meson.current_build_dir(),
+    'GSETTINGS_SCHEMA_DIR=' + meson.project_build_root() / 'data',
+    'GSETTINGS_BACKEND=memory',
+  ]
+
+  test(
+    'basic',
+    python3_for_dogtail_tests,
+    args: [
+      meson.current_source_dir() / 'basic.py',
+    ],
+    env: test_env,
+    depends: [
+      compiled_schemas,
+    ],
+  )
+
+endif
diff --git a/tests/testutil.py b/tests/testutil.py
new file mode 100644
index 00000000..0579ab02
--- /dev/null
+++ b/tests/testutil.py
@@ -0,0 +1,158 @@
+from gi.repository import GLib, Gio
+
+from dogtail.utils import isA11yEnabled, enableA11y
+
+if not isA11yEnabled():
+    enableA11y(True)
+
+from contextlib import contextmanager
+from enum import Enum
+from dogtail import tree
+from dogtail.predicate import Predicate
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from textwrap import dedent
+
+import os
+import subprocess
+
+
+class WaitState(Enum):
+    RUNNING = 1  # waiting to see the service
+    SUCCESS = 2  # seen it successfully
+    TIMEOUT = 3  # timed out before seeing it
+
+
+def wait_for_name(bus: Gio.DBusConnection, name: str, timeout: int = 10) -> bool:
+    wait_state = WaitState.RUNNING
+
+    def wait_name_appeared_cb(connection: Gio.DBusConnection, name: str, name_owner: str):
+        nonlocal wait_state
+        wait_state = WaitState.SUCCESS
+
+    def wait_timeout_cb():
+        nonlocal wait_state
+        wait_state = WaitState.TIMEOUT
+
+    watch_id = Gio.bus_watch_name_on_connection(bus, name, Gio.BusNameWatcherFlags.NONE, 
wait_name_appeared_cb, None)
+
+    timer_id = GLib.timeout_add_seconds(interval=timeout, function=wait_timeout_cb)
+
+    ctx = GLib.main_context_default()
+    while wait_state == WaitState.RUNNING:
+        ctx.iteration(True)
+
+    Gio.bus_unwatch_name(watch_id)
+    GLib.source_remove(timer_id)
+
+    return wait_state == WaitState.SUCCESS
+
+
+class IsAWindowApproximatelyNamed(Predicate):
+    """Predicate subclass that looks for a top-level window by name substring"""
+
+    def __init__(self, windowNameInfix):
+        self.windowNameInfix = windowNameInfix
+
+    def satisfiedByNode(self, node):
+        return node.roleName == "frame" and self.windowNameInfix in node.name
+
+    def describeSearchResult(self):
+        return f"window with “{self.windowNameInfix}” in name"
+
+
+class FileRollerDbus:
+    APPLICATION_ID = "org.gnome.FileRoller"
+
+    def __init__(self, bus: Gio.DBusConnection):
+        self._bus = bus
+        builddir = os.environ.get("G_TEST_BUILDDIR", None)
+        if builddir and not "TESTUTIL_DONT_START" in os.environ:
+            subprocess.Popen(
+                [os.path.join(builddir, "..", "src", "file-roller")],
+                cwd=os.path.join(builddir, ".."),
+            )
+        else:
+            self._do_bus_call("Activate", GLib.Variant("(a{sv})", ([],)))
+
+    def _do_bus_call(self, method: str, params: GLib.Variant) -> None:
+        self._bus.call_sync(
+            self.APPLICATION_ID,
+            "/" + self.APPLICATION_ID.replace(".", "/"),
+            "org.freedesktop.Application",
+            method,
+            params,
+            None,
+            Gio.DBusCallFlags.NONE,
+            -1,
+            None,
+        )
+
+    def open_file(self, path: Path) -> None:
+        self._do_bus_call(
+            "ActivateAction",
+            GLib.Variant(
+                "(sava{sv})",
+                (
+                    "open-archive",
+                    [
+                        GLib.Variant("s", str(path)),
+                    ],
+                    [],
+                ),
+            ),
+        )
+
+    def quit(self) -> None:
+        self._do_bus_call(
+            "ActivateAction", GLib.Variant("(sava{sv})", ("quit", [], []))
+        )
+
+
+class FileRollerDogtail:
+    def __init__(self, app_name):
+        self.app_name = app_name
+
+    def get_application(self) -> tree.Application:
+        return tree.root.application(self.app_name)
+
+    def get_window(self, name: str) -> tree.Window:
+        # Cannot use default window finder because the window name has extra space after the file name.
+        return self.get_application().findChild(
+            IsAWindowApproximatelyNamed(name), False
+        )
+
+
+@contextmanager
+def test_file_roller():
+    try:
+        dbus_app = None
+        _bus = Gio.bus_get_sync(Gio.BusType.SESSION)
+        with TemporaryDirectory() as confdir, TemporaryDirectory() as datadir:
+            os.environ["XDG_CONFIG_HOME"] = confdir
+            os.environ["XDG_DATA_HOME"] = datadir
+
+            desktop_file_path = Path(datadir) / "applications" / "dummy-editor.desktop"
+            desktop_file_path.parent.mkdir(parents=True, exist_ok=True)
+            with open(desktop_file_path, "w") as desktop_file:
+                desktop_file.write(
+                    dedent(
+                        """[Desktop Entry]
+                        Exec=sh -c 'exec 3< "$1"; gdbus call --session --dest org.freedesktop.portal.Desktop 
--object-path /org/freedesktop/portal/desktop --method org.freedesktop.portal.OpenURI.OpenFile --timeout 5 0 
"3" "{}"' portal-open %u
+                        MimeType=text/plain;
+                        Terminal=false
+                        Type=Application
+                        """
+                    )
+                )
+
+            desktop = Gio.DesktopAppInfo.new_from_filename(str(desktop_file_path))
+            desktop.set_as_default_for_type("text/plain")
+
+            dbus_app = FileRollerDbus(_bus)
+            assert wait_for_name(_bus, dbus_app.APPLICATION_ID), f"Waiting for {dbus_app.APPLICATION_ID} 
should not time out."
+            dogtail_app = FileRollerDogtail("file-roller")
+            yield dbus_app, dogtail_app
+    finally:
+        if dbus_app:
+            dbus_app.quit()


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]