summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--configure.ac6
-rw-r--r--src/Makefile-tests.am46
-rw-r--r--src/Makefile.am17
-rw-r--r--src/tests/README85
-rw-r--r--src/tests/stacking/basic-wayland.metatest22
-rw-r--r--src/tests/stacking/basic-x11.metatest19
-rw-r--r--src/tests/stacking/mixed-windows.metatest26
-rw-r--r--src/tests/stacking/override-redirect.metatest19
-rw-r--r--src/tests/test-client.c339
-rw-r--r--src/tests/test-runner.c1069
11 files changed, 1639 insertions, 11 deletions
diff --git a/.gitignore b/.gitignore
index eaad8ce55..7c0c7b9aa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,6 +44,8 @@ po/*.pot
libmutter.pc
mutter
mutter-restart-helper
+mutter-test-client
+mutter-test-runner
org.gnome.mutter.gschema.valid
org.gnome.mutter.gschema.xml
org.gnome.mutter.wayland.gschema.valid
diff --git a/configure.ac b/configure.ac
index 094a055fd..d5b5d1e21 100644
--- a/configure.ac
+++ b/configure.ac
@@ -127,6 +127,12 @@ AC_ARG_WITH([xwayland-path],
[XWAYLAND_PATH="$withval"],
[XWAYLAND_PATH="$bindir/Xwayland"])
+AC_ARG_ENABLE(installed_tests,
+ AS_HELP_STRING([--enable-installed-tests],
+ [Install test programs (default: no)]),,
+ [enable_installed_tests=no])
+AM_CONDITIONAL(BUILDOPT_INSTALL_TESTS, test x$enable_installed_tests = xyes)
+
## here we get the flags we'll actually use
# Unconditionally use this dir to avoid a circular dep with gnomecc
diff --git a/src/Makefile-tests.am b/src/Makefile-tests.am
new file mode 100644
index 000000000..c9acfb62d
--- /dev/null
+++ b/src/Makefile-tests.am
@@ -0,0 +1,46 @@
+# A framework for running scripted tests
+
+if BUILDOPT_INSTALL_TESTS
+stackingdir = $(pkgdatadir)/tests/stacking
+dist_stacking_DATA = \
+ tests/stacking/basic-x11.metatest \
+ tests/stacking/basic-wayland.metatest \
+ tests/stacking/mixed-windows.metatest \
+ tests/stacking/override-redirect.metatest
+
+mutter-all.test: tests/mutter-all.test.in
+ $(AM_V_GEN) sed -e "s|@libexecdir[@]|$(libexecdir)|g" $< > $@.tmp && mv $@.tmp $@
+
+installedtestsdir = $(datadir)/installed-tests/mutter
+installedtests_DATA = mutter-all.test
+
+installedtestsbindir = $(libexecdir)/installed-tests/mutter
+installedtestsbin_PROGRAMS = mutter-test-client mutter-test-runner
+else
+noinst_PROGRAMS += mutter-test-client mutter-test-runner
+endif
+
+EXTRA_DIST += tests/mutter-all.test.in
+
+mutter_test_client_SOURCES = tests/test-client.c
+mutter_test_client_LDADD = $(MUTTER_LIBS) libmutter.la
+
+mutter_test_runner_SOURCES = tests/test-runner.c
+mutter_test_runner_LDADD = $(MUTTER_LIBS) libmutter.la
+
+.PHONY: run-tests
+
+run-tests: mutter-test-client mutter-test-runner
+ ./mutter-test-runner $(dist_stacking_DATA)
+
+# Some random test programs for bits of the code
+
+testboxes_SOURCES = core/testboxes.c
+testgradient_SOURCES = ui/testgradient.c
+testasyncgetprop_SOURCES = x11/testasyncgetprop.c
+
+noinst_PROGRAMS+=testboxes testgradient testasyncgetprop
+
+testboxes_LDADD = $(MUTTER_LIBS) libmutter.la
+testgradient_LDADD = $(MUTTER_LIBS) libmutter.la
+testasyncgetprop_LDADD = $(MUTTER_LIBS) libmutter.la
diff --git a/src/Makefile.am b/src/Makefile.am
index 75b694b64..9882d7ebb 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -5,6 +5,8 @@ lib_LTLIBRARIES = libmutter.la
SUBDIRS=compositor/plugins
+EXTRA_DIST =
+
AM_CPPFLAGS = \
-DCLUTTER_ENABLE_COMPOSITOR_API \
-DCLUTTER_ENABLE_EXPERIMENTAL_API \
@@ -325,6 +327,7 @@ nodist_libmutterinclude_HEADERS = \
$(libmutterinclude_built_headers)
bin_PROGRAMS=mutter
+noinst_PROGRAMS=
mutter_SOURCES = core/mutter.c
mutter_LDADD = $(MUTTER_LIBS) libmutter.la
@@ -333,6 +336,8 @@ libexec_PROGRAMS = mutter-restart-helper
mutter_restart_helper_SOURCES = core/restart-helper.c
mutter_restart_helper_LDADD = $(MUTTER_LIBS)
+include Makefile-tests.am
+
if HAVE_INTROSPECTION
include $(INTROSPECTION_MAKEFILE)
@@ -366,16 +371,6 @@ Meta-$(api_version).gir: libmutter.la
endif
-testboxes_SOURCES = core/testboxes.c
-testgradient_SOURCES = ui/testgradient.c
-testasyncgetprop_SOURCES = x11/testasyncgetprop.c
-
-noinst_PROGRAMS=testboxes testgradient testasyncgetprop
-
-testboxes_LDADD = $(MUTTER_LIBS) libmutter.la
-testgradient_LDADD = $(MUTTER_LIBS) libmutter.la
-testasyncgetprop_LDADD = $(MUTTER_LIBS) libmutter.la
-
dbus_idle_built_sources = meta-dbus-idle-monitor.c meta-dbus-idle-monitor.h
CLEANFILES = \
@@ -389,7 +384,7 @@ DISTCLEANFILES = \
pkgconfigdir = $(libdir)/pkgconfig
pkgconfig_DATA = libmutter.pc
-EXTRA_DIST = \
+EXTRA_DIST += \
$(wayland_protocols) \
libmutter.pc.in \
mutter-enum-types.h.in \
diff --git a/src/tests/README b/src/tests/README
new file mode 100644
index 000000000..9270a16f1
--- /dev/null
+++ b/src/tests/README
@@ -0,0 +1,85 @@
+This directory implements a framework for automated tests of Mutter. The basic
+idea is that mutter-test-runner acts as the window manager and compositor, and
+forks off instances of mutter-test-client to act as clients.
+
+There's a simple scripting language for tests. A very small test would look like:
+
+---
+# Start up a new X11 client with the client id 1 (doesn't have to be an integer)
+# Windows for this client will be referred to as 1/<window-id>
+new_client 1 x11
+
+# Create and show two windows - again the IDs don't have to be integers
+create 1/1
+show 1/1
+create 1/2
+show 1/2
+
+# Wait for the commands we've executed in the clients to reach Mutter
+wait
+
+# Check that the windows are in the order we expect
+assert_stacking 1/1 1/2
+---
+
+Running
+=======
+
+The tests are installed according to:
+
+https://wiki.gnome.org/Initiatives/GnomeGoals/InstalledTests
+
+if --enable-installed-tests is passed to configure. You can run them
+uninstalled with:
+
+ cd src && make run-tests
+
+Command reference
+=================
+
+The following commands are supported. Quoting and comments follow shell rules.
+
+new_client <client-id> [wayland|x11]
+ Starts a client, connecting by either Wayland or X11. The client
+ will subsequently be known with the given client-id (an arbitrary
+ string)
+
+quit_client <client-id>
+ Destroys all windows for the client, waits for that to be processed,
+ then instructs the client to exit.
+
+create <client-id>/<window-id> [override]
+ Creates a new window. For the X11 backend, the keyword 'override'
+ can be given to create an override-redirect
+
+show <client-id>/<window-id>
+hide <client-id>/<window-id>
+ Ask the client to show (map) or hide (unmap) the given window
+
+activate <client-id>/<window-id>
+ Ask the client to raise and focus the given window. This is currently a no-op
+ for Wayland, where this capability is not supported in the protocol.
+
+local_activate <client-id>-<window-id>
+ The same as 'activate', but the operation is done directly inside Mutter
+ and works for both backends
+
+raise <client-id>/<window-id>
+lower <client-id>/<window-id>
+ Ask the client to raise or lower the given window ID. This is a no-op
+ for Wayland clients. (It's also considered discouraged, but supported, for
+ non-override-redirect X11 clients.)
+
+destroy <client-id>/<window-id>
+ Destroy the given window
+
+wait
+ Wait until all requests sent by Mutter to clients have been received by Mutter,
+ and then wait until all requests by Mutter have been processed by the X server.
+
+assert_stacking <client-id>/<window-id> <client-id>/<window-id> ...
+ Assert that the list of client windows known to Mutter is as given and in
+ the given order, bottom to top.
+
+ This function also queries the X server stack and verifies that Mutter's
+ expectation of the X server stack matches reality.
diff --git a/src/tests/stacking/basic-wayland.metatest b/src/tests/stacking/basic-wayland.metatest
new file mode 100644
index 000000000..63ce6082b
--- /dev/null
+++ b/src/tests/stacking/basic-wayland.metatest
@@ -0,0 +1,22 @@
+new_client 1 wayland
+create 1/1
+show 1/1
+create 1/2
+show 1/2
+wait
+assert_stacking 1/1 1/2
+
+# Currently Wayland clients have no wait to bring themselves to the user's
+# attention; gtk_window_present() is a no-op with the X11 backend of GTK+
+
+# activate 1/1
+# wait
+# assert_stacking 1/2 1/1
+# activate 1/2
+# wait
+# assert_stacking 1/1 1/2
+
+local_activate 1/1
+assert_stacking 1/2 1/1
+local_activate 1/2
+assert_stacking 1/1 1/2
diff --git a/src/tests/stacking/basic-x11.metatest b/src/tests/stacking/basic-x11.metatest
new file mode 100644
index 000000000..ee261ece0
--- /dev/null
+++ b/src/tests/stacking/basic-x11.metatest
@@ -0,0 +1,19 @@
+new_client 1 x11
+create 1/1
+show 1/1
+create 1/2
+show 1/2
+wait
+assert_stacking 1/1 1/2
+
+activate 1/1
+wait
+assert_stacking 1/2 1/1
+activate 1/2
+wait
+assert_stacking 1/1 1/2
+
+local_activate 1/1
+assert_stacking 1/2 1/1
+local_activate 1/2
+assert_stacking 1/1 1/2
diff --git a/src/tests/stacking/mixed-windows.metatest b/src/tests/stacking/mixed-windows.metatest
new file mode 100644
index 000000000..38058b582
--- /dev/null
+++ b/src/tests/stacking/mixed-windows.metatest
@@ -0,0 +1,26 @@
+new_client w wayland
+new_client x x11
+
+create w/1
+show w/1
+create w/2
+show w/2
+wait
+
+create x/1
+show x/1
+create x/2
+show x/2
+wait
+
+assert_stacking w/1 w/2 x/1 x/2
+
+local_activate w/1
+assert_stacking w/2 x/1 x/2 w/1
+
+local_activate x/1
+assert_stacking w/2 x/2 w/1 x/1
+
+lower x/1
+wait
+assert_stacking x/1 w/2 x/2 w/1
diff --git a/src/tests/stacking/override-redirect.metatest b/src/tests/stacking/override-redirect.metatest
new file mode 100644
index 000000000..96dde5b9d
--- /dev/null
+++ b/src/tests/stacking/override-redirect.metatest
@@ -0,0 +1,19 @@
+new_client 1 x11
+create 1/1
+show 1/1
+create 1/2 override
+show 1/2
+wait
+assert_stacking 1/1 1/2
+
+activate 1/1
+wait
+assert_stacking 1/1 1/2
+
+lower 1/2
+wait
+assert_stacking 1/2 1/1
+
+raise 1/2
+wait
+assert_stacking 1/1 1/2
diff --git a/src/tests/test-client.c b/src/tests/test-client.c
new file mode 100644
index 000000000..95b725c94
--- /dev/null
+++ b/src/tests/test-client.c
@@ -0,0 +1,339 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+
+/*
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <gio/gunixinputstream.h>
+#include <gtk/gtk.h>
+#include <gdk/gdkx.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <X11/extensions/sync.h>
+
+char *client_id = "0";
+static gboolean wayland;
+GHashTable *windows;
+
+static void read_next_line (GDataInputStream *in);
+
+static GtkWidget *
+lookup_window (const char *window_id)
+{
+ GtkWidget *window = g_hash_table_lookup (windows, window_id);
+ if (!window)
+ g_print ("Window %s doesn't exist", window_id);
+
+ return window;
+}
+
+static void
+process_line (const char *line)
+{
+ GError *error = NULL;
+ int argc;
+ char **argv;
+
+ if (!g_shell_parse_argv (line, &argc, &argv, &error))
+ {
+ g_print ("error parsing command: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ if (argc < 1)
+ {
+ g_print ("Empty command");
+ goto out;
+ }
+
+ if (strcmp (argv[0], "create") == 0)
+ {
+ int i;
+
+ if (argc < 2)
+ {
+ g_print ("usage: create <id> [override]");
+ goto out;
+ }
+
+ if (g_hash_table_lookup (windows, argv[1]))
+ {
+ g_print ("window %s already exists", argv[1]);
+ goto out;
+ }
+
+ gboolean override = FALSE;
+ for (i = 2; i < argc; i++)
+ if (strcmp (argv[i], "override") == 0)
+ override = TRUE;
+
+ GtkWidget *window = gtk_window_new (override ? GTK_WINDOW_POPUP : GTK_WINDOW_TOPLEVEL);
+ g_hash_table_insert (windows, g_strdup (argv[1]), window);
+
+ gtk_window_set_default_size (GTK_WINDOW (window), 100, 100);
+
+ gchar *title = g_strdup_printf ("test/%s/%s", client_id, argv[1]);
+ gtk_window_set_title (GTK_WINDOW (window), title);
+ g_free (title);
+
+ gtk_widget_realize (window);
+
+ if (!wayland)
+ {
+ /* The cairo xlib backend creates a window when initialized, which
+ * confuses our testing if it happens asynchronously the first
+ * time a window is painted. By creating an Xlib surface and
+ * destroying it, we force initialization at a more predictable time.
+ */
+ GdkWindow *window_gdk = gtk_widget_get_window (window);
+ cairo_surface_t *surface = gdk_window_create_similar_surface (window_gdk,
+ CAIRO_CONTENT_COLOR,
+ 1, 1);
+ cairo_surface_destroy (surface);
+ }
+
+ }
+ else if (strcmp (argv[0], "show") == 0)
+ {
+ if (argc != 2)
+ {
+ g_print ("usage: show <id>");
+ goto out;
+ }
+
+ GtkWidget *window = lookup_window (argv[1]);
+ if (!window)
+ goto out;
+
+ gtk_widget_show (window);
+ }
+ else if (strcmp (argv[0], "hide") == 0)
+ {
+ if (argc != 2)
+ {
+ g_print ("usage: hide <id>");
+ goto out;
+ }
+
+ GtkWidget *window = lookup_window (argv[1]);
+ if (!window)
+ goto out;
+
+ gtk_widget_hide (window);
+ }
+ else if (strcmp (argv[0], "activate") == 0)
+ {
+ if (argc != 2)
+ {
+ g_print ("usage: activate <id>");
+ goto out;
+ }
+
+ GtkWidget *window = lookup_window (argv[1]);
+ if (!window)
+ goto out;
+
+ gtk_window_present (GTK_WINDOW (window));
+ }
+ else if (strcmp (argv[0], "raise") == 0)
+ {
+ if (argc != 2)
+ {
+ g_print ("usage: raise <id>");
+ goto out;
+ }
+
+ GtkWidget *window = lookup_window (argv[1]);
+ if (!window)
+ goto out;
+
+ gdk_window_raise (gtk_widget_get_window (window));
+ }
+ else if (strcmp (argv[0], "lower") == 0)
+ {
+ if (argc != 2)
+ {
+ g_print ("usage: lower <id>");
+ goto out;
+ }
+
+ GtkWidget *window = lookup_window (argv[1]);
+ if (!window)
+ goto out;
+
+ gdk_window_lower (gtk_widget_get_window (window));
+ }
+ else if (strcmp (argv[0], "destroy") == 0)
+ {
+ if (argc != 2)
+ {
+ g_print ("usage: destroy <id>");
+ goto out;
+ }
+
+ GtkWidget *window = lookup_window (argv[1]);
+ if (!window)
+ goto out;
+
+ g_hash_table_remove (windows, argv[1]);
+ gtk_widget_destroy (window);
+ }
+ else if (strcmp (argv[0], "destroy_all") == 0)
+ {
+ if (argc != 1)
+ {
+ g_print ("usage: destroy_all");
+ goto out;
+ }
+
+ GHashTableIter iter;
+ gpointer key, value;
+
+ g_hash_table_iter_init (&iter, windows);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ gtk_widget_destroy (value);
+
+ g_hash_table_remove_all (windows);
+ }
+ else if (strcmp (argv[0], "sync") == 0)
+ {
+ if (argc != 1)
+ {
+ g_print ("usage: sync");
+ goto out;
+ }
+
+ gdk_display_sync (gdk_display_get_default ());
+ }
+ else if (strcmp (argv[0], "set_counter") == 0)
+ {
+ XSyncCounter counter;
+ int value;
+
+ if (argc != 3)
+ {
+ g_print ("usage: set_counter <counter> <value>");
+ goto out;
+ }
+
+ if (wayland)
+ {
+ g_print ("usage: set_counter can only be used for X11");
+ goto out;
+ }
+
+ counter = strtoul(argv[1], NULL, 10);
+ value = atoi(argv[2]);
+ XSyncValue sync_value;
+ XSyncIntToValue (&sync_value, value);
+
+ XSyncSetCounter (gdk_x11_display_get_xdisplay (gdk_display_get_default ()),
+ counter, sync_value);
+ }
+ else
+ {
+ g_print ("Unknown command %s", argv[0]);
+ goto out;
+ }
+
+ g_print ("OK\n");
+
+ out:
+ g_strfreev (argv);
+}
+
+static void
+on_line_received (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GDataInputStream *in = G_DATA_INPUT_STREAM (source);
+ GError *error = NULL;
+ gsize length;
+ char *line = g_data_input_stream_read_line_finish_utf8 (in, result, &length, &error);
+
+ if (line == NULL)
+ {
+ if (error != NULL)
+ g_printerr ("Error reading from stdin: %s\n", error->message);
+ gtk_main_quit ();
+ return;
+ }
+
+ process_line (line);
+ g_free (line);
+ read_next_line (in);
+}
+
+static void
+read_next_line (GDataInputStream *in)
+{
+ g_data_input_stream_read_line_async (in, G_PRIORITY_DEFAULT, NULL,
+ on_line_received, NULL);
+}
+
+const GOptionEntry options[] = {
+ {
+ "wayland", 0, 0, G_OPTION_ARG_NONE,
+ &wayland,
+ "Create a wayland client, not an X11 one",
+ NULL
+ },
+ {
+ "client-id", 0, 0, G_OPTION_ARG_STRING,
+ &client_id,
+ "Identifier used in Window titles for this client",
+ "CLIENT_ID",
+ },
+ { NULL }
+};
+
+int
+main(int argc, char **argv)
+{
+ GOptionContext *context = g_option_context_new (NULL);
+ GError *error = NULL;
+
+ g_option_context_add_main_entries (context, options, NULL);
+
+ if (!g_option_context_parse (context,
+ &argc, &argv, &error))
+ {
+ g_printerr ("%s", error->message);
+ return 1;
+ }
+
+ if (wayland)
+ gdk_set_allowed_backends ("wayland");
+ else
+ gdk_set_allowed_backends ("x11");
+
+ gtk_init (NULL, NULL);
+
+ windows = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, NULL);
+
+ GInputStream *raw_in = g_unix_input_stream_new (0, FALSE);
+ GDataInputStream *in = g_data_input_stream_new (raw_in);
+
+ read_next_line (in);
+
+ gtk_main ();
+
+ return 0;
+}
diff --git a/src/tests/test-runner.c b/src/tests/test-runner.c
new file mode 100644
index 000000000..83c6ee491
--- /dev/null
+++ b/src/tests/test-runner.c
@@ -0,0 +1,1069 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+
+/*
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <gio/gio.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <meta/main.h>
+#include <meta/util.h>
+#include <meta/window.h>
+#include <ui/ui.h>
+#include "meta-plugin-manager.h"
+#include "wayland/meta-wayland.h"
+#include "window-private.h"
+
+#define TEST_RUNNER_ERROR test_runner_error_quark ()
+
+typedef enum
+{
+ TEST_RUNNER_ERROR_BAD_COMMAND,
+ TEST_RUNNER_ERROR_RUNTIME_ERROR,
+ TEST_RUNNER_ERROR_ASSERTION_FAILED
+} TestRunnerError;
+
+
+GQuark test_runner_error_quark (void);
+
+G_DEFINE_QUARK (test-runner-error-quark, test_runner_error)
+
+/**********************************************************************/
+
+typedef struct {
+ XSyncCounter counter;
+ int counter_value;
+ XSyncAlarm alarm;
+
+ GMainLoop *loop;
+ int counter_wait_value;
+} AsyncWaiter;
+
+static AsyncWaiter *
+async_waiter_new (void)
+{
+ AsyncWaiter *waiter = g_new0 (AsyncWaiter, 1);
+
+ Display *xdisplay = meta_get_display ()->xdisplay;
+ XSyncValue value;
+ XSyncAlarmAttributes attr;
+
+ waiter->counter_value = 0;
+ XSyncIntToValue (&value, waiter->counter_value);
+
+ waiter->counter = XSyncCreateCounter (xdisplay, value);
+
+ attr.trigger.counter = waiter->counter;
+ attr.trigger.test_type = XSyncPositiveComparison;
+
+ /* Initialize to one greater than the current value */
+ attr.trigger.value_type = XSyncRelative;
+ XSyncIntToValue (&attr.trigger.wait_value, 1);
+
+ /* After triggering, increment test_value by this until
+ * until the test condition is false */
+ XSyncIntToValue (&attr.delta, 1);
+
+ /* we want events (on by default anyway) */
+ attr.events = True;
+
+ waiter->alarm = XSyncCreateAlarm (xdisplay,
+ XSyncCACounter |
+ XSyncCAValueType |
+ XSyncCAValue |
+ XSyncCATestType |
+ XSyncCADelta |
+ XSyncCAEvents,
+ &attr);
+
+ waiter->loop = g_main_loop_new (NULL, FALSE);
+
+ return waiter;
+}
+
+static void
+async_waiter_destroy (AsyncWaiter *waiter)
+{
+ Display *xdisplay = meta_get_display ()->xdisplay;
+
+ XSyncDestroyAlarm (xdisplay, waiter->alarm);
+ XSyncDestroyCounter (xdisplay, waiter->counter);
+ g_main_loop_unref (waiter->loop);
+}
+
+static int
+async_waiter_next_value (AsyncWaiter *waiter)
+{
+ return waiter->counter_value + 1;
+}
+
+static void
+async_waiter_wait (AsyncWaiter *waiter,
+ int wait_value)
+{
+ if (waiter->counter_value < wait_value)
+ {
+ waiter->counter_wait_value = wait_value;
+ g_main_loop_run (waiter->loop);
+ waiter->counter_wait_value = 0;
+ }
+}
+
+static void
+async_waiter_set_and_wait (AsyncWaiter *waiter)
+{
+ Display *xdisplay = meta_get_display ()->xdisplay;
+ int wait_value = async_waiter_next_value (waiter);
+
+ XSyncValue sync_value;
+ XSyncIntToValue (&sync_value, wait_value);
+
+ XSyncSetCounter (xdisplay, waiter->counter, sync_value);
+ async_waiter_wait (waiter, wait_value);
+}
+
+static gboolean
+async_waiter_alarm_filter (AsyncWaiter *waiter,
+ MetaDisplay *display,
+ XSyncAlarmNotifyEvent *event)
+{
+ if (event->alarm != waiter->alarm)
+ return FALSE;
+
+ waiter->counter_value = XSyncValueLow32 (event->counter_value);
+
+ if (waiter->counter_wait_value != 0 &&
+ waiter->counter_value >= waiter->counter_wait_value)
+ g_main_loop_quit (waiter->loop);
+
+ return TRUE;
+}
+
+/**********************************************************************/
+
+typedef struct {
+ char *id;
+ MetaWindowClientType type;
+ GSubprocess *subprocess;
+ GCancellable *cancellable;
+ GMainLoop *loop;
+ GDataOutputStream *in;
+ GDataInputStream *out;
+
+ char *line;
+ GError **error;
+
+ AsyncWaiter *waiter;
+} TestClient;
+
+static char *test_client_path;
+
+static TestClient *
+test_client_new (const char *id,
+ MetaWindowClientType type,
+ GError **error)
+{
+ TestClient *client = g_new0 (TestClient, 1);
+ GSubprocessLauncher *launcher;
+ GSubprocess *subprocess;
+
+ launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDIN_PIPE | G_SUBPROCESS_FLAGS_STDOUT_PIPE);
+
+ g_assert (meta_is_wayland_compositor ());
+ MetaWaylandCompositor *compositor = meta_wayland_compositor_get_default ();
+
+ g_subprocess_launcher_setenv (launcher,
+ "WAYLAND_DISPLAY", meta_wayland_get_wayland_display_name (compositor),
+ TRUE);
+ g_subprocess_launcher_setenv (launcher,
+ "DISPLAY", meta_wayland_get_xwayland_display_name (compositor),
+ TRUE);
+
+ subprocess = g_subprocess_launcher_spawn (launcher,
+ error,
+ test_client_path,
+ "--client-id",
+ id,
+ type == META_WINDOW_CLIENT_TYPE_WAYLAND ? "--wayland" : NULL,
+ NULL);
+ g_object_unref (launcher);
+
+ if (!subprocess)
+ return NULL;
+
+ client->type = type;
+ client->id = g_strdup (id);
+ client->cancellable = g_cancellable_new ();
+ client->subprocess = subprocess;
+ client->in = g_data_output_stream_new (g_subprocess_get_stdin_pipe (subprocess));
+ client->out = g_data_input_stream_new (g_subprocess_get_stdout_pipe (subprocess));
+ client->loop = g_main_loop_new (NULL, FALSE);
+
+ if (client->type == META_WINDOW_CLIENT_TYPE_X11)
+ client->waiter = async_waiter_new ();
+
+ return client;
+}
+
+static void
+test_client_destroy (TestClient *client)
+{
+ GError *error = NULL;
+
+ if (client->waiter)
+ async_waiter_destroy (client->waiter);
+
+ g_output_stream_close (G_OUTPUT_STREAM (client->in), NULL, &error);
+ if (error)
+ {
+ g_warning ("Error closing client stdin: %s", error->message);
+ g_clear_error (&error);
+ }
+ g_object_unref (client->in);
+
+ g_input_stream_close (G_INPUT_STREAM (client->out), NULL, &error);
+ if (error)
+ {
+ g_warning ("Error closing client stdout: %s", error->message);
+ g_clear_error (&error);
+ }
+ g_object_unref (client->out);
+
+ g_object_unref (client->cancellable);
+ g_object_unref (client->subprocess);
+ g_main_loop_unref (client->loop);
+ g_free (client->id);
+ g_free (client);
+}
+
+static void
+test_client_line_read (GObject *source,
+ GAsyncResult *result,
+ gpointer data)
+{
+ TestClient *client = data;
+
+ client->line = g_data_input_stream_read_line_finish_utf8 (client->out, result,
+ NULL, client->error);
+ g_main_loop_quit (client->loop);
+}
+
+static gboolean test_client_do (TestClient *client,
+ GError **error,
+ ...) G_GNUC_NULL_TERMINATED;
+
+static gboolean
+test_client_do (TestClient *client,
+ GError **error,
+ ...)
+{
+ GString *command = g_string_new (NULL);
+ char *line = NULL;
+
+ va_list vap;
+ va_start (vap, error);
+
+ while (TRUE)
+ {
+ char *word = va_arg (vap, char *);
+ if (word == NULL)
+ break;
+
+ if (command->len > 0)
+ g_string_append_c (command, ' ');
+
+ char *quoted = g_shell_quote (word);
+ g_string_append (command, quoted);
+ g_free (quoted);
+ }
+
+ va_end (vap);
+
+ g_string_append_c (command, '\n');
+
+ if (!g_data_output_stream_put_string (client->in, command->str,
+ client->cancellable, error))
+ goto out;
+
+ g_data_input_stream_read_line_async (client->out,
+ G_PRIORITY_DEFAULT,
+ client->cancellable,
+ test_client_line_read,
+ client);
+
+ client->error = error;
+ g_main_loop_run (client->loop);
+ line = client->line;
+ client->line = NULL;
+ client->error = NULL;
+
+ if (!line)
+ {
+ if (*error == NULL)
+ g_set_error (error, TEST_RUNNER_ERROR, TEST_RUNNER_ERROR_RUNTIME_ERROR,
+ "test client exited");
+ goto out;
+ }
+
+ if (strcmp (line, "OK") != 0)
+ {
+ g_set_error (error, TEST_RUNNER_ERROR, TEST_RUNNER_ERROR_RUNTIME_ERROR,
+ "%s", line);
+ goto out;
+ }
+
+ out:
+ g_string_free (command, TRUE);
+ if (line)
+ g_free (line);
+
+ return *error == NULL;
+}
+
+static gboolean
+test_client_wait (TestClient *client,
+ GError **error)
+{
+ if (client->type == META_WINDOW_CLIENT_TYPE_WAYLAND)
+ {
+ return test_client_do (client, error, "sync", NULL);
+ }
+ else
+ {
+ int wait_value = async_waiter_next_value (client->waiter);
+ char *counter_str = g_strdup_printf ("%lu", client->waiter->counter);
+ char *wait_value_str = g_strdup_printf ("%d", wait_value);
+
+ gboolean success = test_client_do (client, error, "set_counter", counter_str, wait_value_str, NULL);
+ g_free (counter_str);
+ g_free (wait_value_str);
+ if (!success)
+ return FALSE;
+
+ async_waiter_wait (client->waiter, wait_value);
+ return TRUE;
+ }
+}
+
+static MetaWindow *
+test_client_find_window (TestClient *client,
+ const char *window_id,
+ GError **error)
+{
+ MetaDisplay *display = meta_get_display ();
+
+ GSList *windows = meta_display_list_windows (display,
+ META_LIST_INCLUDE_OVERRIDE_REDIRECT);
+ MetaWindow *result = NULL;
+ char *expected_title = g_strdup_printf ("test/%s/%s",
+ client->id, window_id);
+ GSList *l;
+
+ for (l = windows; l; l = l->next)
+ {
+ MetaWindow *window = l->data;
+ if (g_strcmp0 (window->title, expected_title) == 0)
+ {
+ result = window;
+ break;
+ }
+ }
+
+ g_slist_free (windows);
+ g_free (expected_title);
+
+ if (result == NULL)
+ g_set_error (error, TEST_RUNNER_ERROR, TEST_RUNNER_ERROR_RUNTIME_ERROR,
+ "window %s/%s isn't known to Mutter", client->id, window_id);
+
+ return result;
+}
+
+static gboolean
+test_client_alarm_filter (TestClient *client,
+ MetaDisplay *display,
+ XSyncAlarmNotifyEvent *event)
+{
+ if (client->waiter)
+ return async_waiter_alarm_filter (client->waiter, display, event);
+ else
+ return FALSE;
+}
+
+/**********************************************************************/
+
+typedef struct {
+ GHashTable *clients;
+ AsyncWaiter *waiter;
+} TestCase;
+
+static gboolean
+test_case_alarm_filter (MetaDisplay *display,
+ XSyncAlarmNotifyEvent *event,
+ gpointer data)
+{
+ TestCase *test = data;
+ GHashTableIter iter;
+ gpointer key, value;
+
+ if (async_waiter_alarm_filter (test->waiter, display, event))
+ return TRUE;
+
+ g_hash_table_iter_init (&iter, test->clients);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ if (test_client_alarm_filter (value, display, event))
+ return TRUE;
+
+ return FALSE;
+}
+
+static TestCase *
+test_case_new (void)
+{
+ TestCase *test = g_new0 (TestCase, 1);
+
+ meta_display_set_alarm_filter (meta_get_display (),
+ test_case_alarm_filter, test);
+
+ test->clients = g_hash_table_new (g_str_hash, g_str_equal);
+ test->waiter = async_waiter_new ();
+
+ return test;
+}
+
+static gboolean
+test_case_wait (TestCase *test,
+ GError **error)
+{
+ GHashTableIter iter;
+ gpointer key, value;
+
+ g_hash_table_iter_init (&iter, test->clients);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ if (!test_client_wait (value, error))
+ return FALSE;
+
+ async_waiter_set_and_wait (test->waiter);
+ return TRUE;
+}
+
+#define BAD_COMMAND(...) \
+ G_STMT_START { \
+ g_set_error (error, \
+ TEST_RUNNER_ERROR, TEST_RUNNER_ERROR_BAD_COMMAND, \
+ __VA_ARGS__); \
+ return FALSE; \
+ } G_STMT_END
+
+static TestClient *
+test_case_lookup_client (TestCase *test,
+ char *client_id,
+ GError **error)
+{
+ TestClient *client = g_hash_table_lookup (test->clients, client_id);
+ if (!client)
+ g_set_error (error, TEST_RUNNER_ERROR, TEST_RUNNER_ERROR_BAD_COMMAND,
+ "No such client %s", client_id);
+
+ return client;
+}
+
+static gboolean
+test_case_parse_window_id (TestCase *test,
+ const char *client_and_window_id,
+ TestClient **client,
+ const char **window_id,
+ GError **error)
+{
+ const char *slash = strchr (client_and_window_id, '/');
+ char *tmp;
+ if (slash == NULL)
+ BAD_COMMAND ("client/window ID %s doesnt' contain a /", client_and_window_id);
+
+ *window_id = slash + 1;
+
+ tmp = g_strndup (client_and_window_id, slash - client_and_window_id);
+ *client = test_case_lookup_client (test, tmp, error);
+ g_free (tmp);
+
+ return client != NULL;
+}
+
+static gboolean
+test_case_assert_stacking (TestCase *test,
+ char **expected_windows,
+ int n_expected_windows,
+ GError **error)
+{
+ MetaDisplay *display = meta_get_display ();
+ MetaStackWindow *windows;
+ int n_windows;
+ GString *stack_string = g_string_new (NULL);
+ GString *expected_string = g_string_new (NULL);
+ int i;
+
+ meta_stack_tracker_get_stack (display->screen->stack_tracker, &windows, &n_windows);
+ for (i = 0; i < n_windows; i++)
+ {
+ MetaWindow *window;
+
+ if (windows[i].any.type == META_WINDOW_CLIENT_TYPE_X11)
+ window = meta_display_lookup_x_window (display,
+ windows[i].x11.xwindow);
+ else
+ window = windows[i].wayland.meta_window;
+
+ if (window != NULL && window->title)
+ {
+
+ /* See comment in meta_ui_new() about why the dummy window for GTK+ theming
+ * is managed as a MetaWindow.
+ */
+ if (windows[i].any.type == META_WINDOW_CLIENT_TYPE_X11 &&
+ meta_ui_window_is_dummy (display->screen->ui, windows[i].x11.xwindow))
+ continue;
+
+ if (stack_string->len > 0)
+ g_string_append_c (stack_string, ' ');
+
+ if (g_str_has_prefix (window->title, "test/"))
+ g_string_append (stack_string, window->title + 5);
+ else
+ g_string_append_printf (stack_string, "(%s)", window->title);
+ }
+ }
+
+ for (i = 0; i < n_expected_windows; i++)
+ {
+ if (expected_string->len > 0)
+ g_string_append_c (expected_string, ' ');
+
+ g_string_append (expected_string, expected_windows[i]);
+ }
+
+ if (strcmp (expected_string->str, stack_string->str) != 0)
+ {
+ g_set_error (error, TEST_RUNNER_ERROR, TEST_RUNNER_ERROR_ASSERTION_FAILED,
+ "stacking: expected='%s', actual='%s'",
+ expected_string->str, stack_string->str);
+ }
+
+ g_string_free (stack_string, TRUE);
+ g_string_free (expected_string, TRUE);
+
+ return *error == NULL;
+}
+
+static gboolean
+test_case_check_xserver_stacking (TestCase *test,
+ GError **error)
+{
+ MetaDisplay *display = meta_get_display ();
+ GString *local_string = g_string_new (NULL);
+ GString *x11_string = g_string_new (NULL);
+ int i;
+
+ MetaStackWindow *windows;
+ int n_windows;
+ meta_stack_tracker_get_stack (display->screen->stack_tracker, &windows, &n_windows);
+
+ for (i = 0; i < n_windows; i++)
+ {
+ if (windows[i].any.type == META_WINDOW_CLIENT_TYPE_X11)
+ {
+ if (local_string->len > 0)
+ g_string_append_c (local_string, ' ');
+
+ g_string_append_printf (local_string, "%#lx", windows[i].x11.xwindow);
+ }
+ }
+
+ Window root;
+ Window parent;
+ Window *children;
+ unsigned int n_children;
+ XQueryTree (display->xdisplay,
+ meta_screen_get_xroot (display->screen),
+ &root, &parent, &children, &n_children);
+
+ for (i = 0; i < (int)n_children; i++)
+ {
+ if (x11_string->len > 0)
+ g_string_append_c (x11_string, ' ');
+
+ g_string_append_printf (x11_string, "%#lx", (Window)children[i]);
+ }
+
+ if (strcmp (x11_string->str, local_string->str) != 0)
+ g_set_error (error, TEST_RUNNER_ERROR, TEST_RUNNER_ERROR_ASSERTION_FAILED,
+ "xserver stacking: x11='%s', local='%s'",
+ x11_string->str, local_string->str);
+
+ XFree (children);
+
+ g_string_free (local_string, TRUE);
+ g_string_free (x11_string, TRUE);
+
+ return *error == NULL;
+}
+
+static gboolean
+test_case_do (TestCase *test,
+ int argc,
+ char **argv,
+ GError **error)
+{
+ if (strcmp (argv[0], "new_client") == 0)
+ {
+ MetaWindowClientType type;
+
+ if (argc != 3)
+ BAD_COMMAND("usage: new_client <client-id> [wayland|x11]");
+
+ if (strcmp (argv[2], "x11") == 0)
+ type = META_WINDOW_CLIENT_TYPE_X11;
+ else if (strcmp (argv[2], "wayland") == 0)
+ type = META_WINDOW_CLIENT_TYPE_WAYLAND;
+ else
+ BAD_COMMAND("usage: new_client <client-id> [wayland|x11]");
+
+ if (g_hash_table_lookup (test->clients, argv[1]))
+ BAD_COMMAND("client %s already exists", argv[1]);
+
+ TestClient *client = test_client_new (argv[1], type, error);
+ if (!client)
+ return FALSE;
+
+ g_hash_table_insert (test->clients, client->id, client);
+ }
+ else if (strcmp (argv[0], "quit_client") == 0)
+ {
+ if (argc != 2)
+ BAD_COMMAND("usage: quit_client <client-id>");
+
+ TestClient *client = test_case_lookup_client (test, argv[1], error);
+ if (!client)
+ return FALSE;
+
+ if (!test_client_do (client, error, "destroy_all", NULL))
+ return FALSE;
+
+ if (!test_client_wait (client, error))
+ return FALSE;
+
+ g_hash_table_remove (test->clients, client->id);
+ test_client_destroy (client);
+ }
+ else if (strcmp (argv[0], "create") == 0)
+ {
+ if (!(argc == 2 ||
+ (argc == 3 && strcmp (argv[2], "override") == 0)))
+ BAD_COMMAND("usage: %s <client-id>/<window-id > [override]", argv[0]);
+
+ TestClient *client;
+ const char *window_id;
+ if (!test_case_parse_window_id (test, argv[1], &client, &window_id, error))
+ return FALSE;
+
+ if (!test_client_do (client, error,
+ "create", window_id,
+ argc == 3 ? argv[2] : NULL,
+ NULL))
+ return FALSE;
+ }
+ else if (strcmp (argv[0], "show") == 0 ||
+ strcmp (argv[0], "hide") == 0 ||
+ strcmp (argv[0], "activate") == 0 ||
+ strcmp (argv[0], "raise") == 0 ||
+ strcmp (argv[0], "lower") == 0 ||
+ strcmp (argv[0], "destroy") == 0)
+ {
+ if (argc != 2)
+ BAD_COMMAND("usage: %s <client-id>/<window-id>", argv[0]);
+
+ TestClient *client;
+ const char *window_id;
+ if (!test_case_parse_window_id (test, argv[1], &client, &window_id, error))
+ return FALSE;
+
+ if (!test_client_do (client, error, argv[0], window_id, NULL))
+ return FALSE;
+ }
+ else if (strcmp (argv[0], "local_activate") == 0)
+ {
+ if (argc != 2)
+ BAD_COMMAND("usage: %s <client-id>/<window-id>", argv[0]);
+
+ TestClient *client;
+ const char *window_id;
+ if (!test_case_parse_window_id (test, argv[1], &client, &window_id, error))
+ return FALSE;
+
+ MetaWindow *window = test_client_find_window (client, window_id, error);
+ if (!window)
+ return FALSE;
+
+ meta_window_activate (window, 0);
+ }
+ else if (strcmp (argv[0], "wait") == 0)
+ {
+ if (argc != 1)
+ BAD_COMMAND("usage: %s", argv[0]);
+
+ if (!test_case_wait (test, error))
+ return FALSE;
+ }
+ else if (strcmp (argv[0], "assert_stacking") == 0)
+ {
+ if (!test_case_assert_stacking (test, argv + 1, argc - 1, error))
+ return FALSE;
+ if (!test_case_check_xserver_stacking (test, error))
+ return FALSE;
+ }
+ else
+ {
+ BAD_COMMAND("Unknown command %s", argv[0]);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+test_case_destroy (TestCase *test,
+ GError **error)
+{
+ /* Failures when cleaning up the test case aren't recoverable, since we'll
+ * pollute the subsequent test cases, so we just return the error, and
+ * skip the rest of the cleanup.
+ */
+ GHashTableIter iter;
+ gpointer key, value;
+
+ g_hash_table_iter_init (&iter, test->clients);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ {
+ if (!test_client_do (value, error, "destroy_all", NULL))
+ return FALSE;
+
+ }
+
+ if (!test_case_wait (test, error))
+ return FALSE;
+
+ if (!test_case_assert_stacking (test, NULL, 0, error))
+ return FALSE;
+
+ g_hash_table_iter_init (&iter, test->clients);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ test_client_destroy (value);
+
+ async_waiter_destroy (test->waiter);
+
+ meta_display_set_alarm_filter (meta_get_display (), NULL, NULL);
+
+ g_hash_table_destroy (test->clients);
+ g_free (test);
+
+ return TRUE;
+}
+
+/**********************************************************************/
+
+static gboolean
+run_test (const char *filename,
+ int index)
+{
+ TestCase *test = test_case_new ();
+ GError *error = NULL;
+
+ GFile *file = g_file_new_for_path (filename);
+
+ GDataInputStream *in = NULL;
+
+ GFileInputStream *in_raw = g_file_read (file, NULL, &error);
+ g_object_unref (file);
+ if (in_raw == NULL)
+ goto out;
+
+ in = g_data_input_stream_new (G_INPUT_STREAM (in_raw));
+ g_object_unref (in_raw);
+
+ int line_no = 0;
+ while (error == NULL)
+ {
+ char *line = g_data_input_stream_read_line_utf8 (in, NULL, NULL, &error);
+ if (line == NULL)
+ break;
+
+ line_no++;
+
+ int argc;
+ char **argv = NULL;
+ if (!g_shell_parse_argv (line, &argc, &argv, &error))
+ {
+ if (g_error_matches (error, G_SHELL_ERROR, G_SHELL_ERROR_EMPTY_STRING))
+ {
+ g_clear_error (&error);
+ goto next;
+ }
+
+ goto next;
+ }
+
+ test_case_do (test, argc, argv, &error);
+
+ next:
+ if (error)
+ g_prefix_error (&error, "%d: ", line_no);
+
+ g_free (line);
+ g_strfreev (argv);
+ }
+
+ {
+ GError *tmp_error = NULL;
+ if (!g_input_stream_close (G_INPUT_STREAM (in), NULL, &tmp_error))
+ {
+ if (error != NULL)
+ g_clear_error (&tmp_error);
+ else
+ g_propagate_error (&error, tmp_error);
+ }
+ }
+
+ out:
+ if (in != NULL)
+ g_object_unref (in);
+
+ GError *cleanup_error = NULL;
+ test_case_destroy (test, &cleanup_error);
+
+ const char *testspos = strstr (filename, "tests/");
+ char *pretty_name;
+ if (testspos)
+ pretty_name = g_strdup (testspos + strlen("tests/"));
+ else
+ pretty_name = g_strdup (filename);
+
+ if (error || cleanup_error)
+ {
+ g_print ("not ok %d %s\n", index, pretty_name);
+
+ if (error)
+ g_print (" %s\n", error->message);
+
+ if (cleanup_error)
+ {
+ g_print (" Fatal Error During Cleanup\n");
+ g_print (" %s\n", cleanup_error->message);
+ exit (1);
+ }
+ }
+ else
+ {
+ g_print ("ok %d %s\n", index, pretty_name);
+ }
+
+ g_free (pretty_name);
+
+ gboolean success = error == NULL;
+
+ g_clear_error (&error);
+ g_clear_error (&cleanup_error);
+
+ return success;
+}
+
+typedef struct {
+ int n_tests;
+ char **tests;
+} RunTestsInfo;
+
+static gboolean
+run_tests (gpointer data)
+{
+ RunTestsInfo *info = data;
+ int i;
+ gboolean success = TRUE;
+
+ g_print ("1..%d\n", info->n_tests);
+
+ for (i = 0; i < info->n_tests; i++)
+ if (!run_test (info->tests[i], i + 1))
+ success = FALSE;
+
+ meta_quit (success ? 0 : 1);
+
+ return FALSE;
+}
+
+/**********************************************************************/
+
+static gboolean
+find_metatests_in_directory (GFile *directory,
+ GPtrArray *results,
+ GError **error)
+{
+ GFileEnumerator *enumerator = g_file_enumerate_children (directory,
+ "standard::name,standard::type",
+ G_FILE_QUERY_INFO_NONE,
+ NULL, error);
+ if (!enumerator)
+ return FALSE;
+
+ while (*error == NULL)
+ {
+ GFileInfo *info = g_file_enumerator_next_file (enumerator, NULL, error);
+ if (info == NULL)
+ break;
+
+ GFile *child = g_file_enumerator_get_child (enumerator, info);
+ switch (g_file_info_get_file_type (info))
+ {
+ case G_FILE_TYPE_REGULAR:
+ {
+ const char *name = g_file_info_get_name (info);
+ if (g_str_has_suffix (name, ".metatest"))
+ g_ptr_array_add (results, g_file_get_path (child));
+ break;
+ }
+ case G_FILE_TYPE_DIRECTORY:
+ find_metatests_in_directory (child, results, error);
+ break;
+ default:
+ break;
+ }
+
+ g_object_unref (child);
+ g_object_unref (info);
+ }
+
+ {
+ GError *tmp_error = NULL;
+ if (!g_file_enumerator_close (enumerator, NULL, &tmp_error))
+ {
+ if (*error != NULL)
+ g_clear_error (&tmp_error);
+ else
+ g_propagate_error (error, tmp_error);
+ }
+ }
+
+ g_object_unref (enumerator);
+ return *error == NULL;
+}
+
+static gboolean all_tests = FALSE;
+
+const GOptionEntry options[] = {
+ {
+ "all", 0, 0, G_OPTION_ARG_NONE,
+ &all_tests,
+ "Run all installed tests",
+ NULL
+ },
+ { NULL }
+};
+
+int
+main (int argc, char **argv)
+{
+ GOptionContext *ctx;
+ GError *error = NULL;
+
+ /* First parse the arguments that are passed to us */
+
+ ctx = g_option_context_new (NULL);
+ g_option_context_add_main_entries (ctx, options, NULL);
+
+ if (!g_option_context_parse (ctx,
+ &argc, &argv, &error))
+ {
+ g_printerr ("%s", error->message);
+ return 1;
+ }
+
+ g_option_context_free (ctx);
+
+ GPtrArray *tests = g_ptr_array_new ();
+
+ if (all_tests)
+ {
+ GFile *test_dir = g_file_new_for_path (MUTTER_PKGDATADIR "/tests");
+ GError *error = NULL;
+
+ if (!find_metatests_in_directory (test_dir, tests, &error))
+ {
+ g_printerr ("Error enumerating tests: %s\n", error->message);
+ return 1;
+ }
+ }
+ else
+ {
+ int i;
+ char *curdir = g_get_current_dir ();
+
+ for (i = 1; i < argc; i++)
+ {
+ if (g_path_is_absolute (argv[i]))
+ g_ptr_array_add (tests, g_strdup (argv[i]));
+ else
+ g_ptr_array_add (tests, g_build_filename (curdir, argv[i], NULL));
+ }
+
+ g_free (curdir);
+ }
+
+ /* Then initalize mutter with a different set of arguments */
+
+ char *fake_args[] = { NULL, "--wayland" };
+ fake_args[0] = argv[0];
+ char **fake_argv = fake_args;
+ int fake_argc = 2;
+
+ char *basename = g_path_get_basename (argv[0]);
+ char *dirname = g_path_get_dirname (argv[0]);
+ if (g_str_has_prefix (basename, "lt-"))
+ test_client_path = g_build_filename (dirname, "../mutter-test-client", NULL);
+ else
+ test_client_path = g_build_filename (dirname, "mutter-test-client", NULL);
+ g_free (basename);
+ g_free (dirname);
+
+ ctx = meta_get_option_context ();
+ if (!g_option_context_parse (ctx, &fake_argc, &fake_argv, &error))
+ {
+ g_printerr ("mutter: %s\n", error->message);
+ exit (1);
+ }
+ g_option_context_free (ctx);
+
+ meta_plugin_manager_load ("default");
+
+ meta_init ();
+ meta_register_with_session ();
+
+ RunTestsInfo info;
+ info.tests = (char **)tests->pdata;
+ info.n_tests = tests->len;
+
+ g_idle_add (run_tests, &info);
+
+ return meta_run ();
+}