[mutter] Add a test framework and stacking tests



commit 2f63c39fa65549d38a734fbc8b35779796666630
Author: Owen W. Taylor <otaylor fishsoup net>
Date:   Thu Sep 11 13:43:32 2014 -0400

    Add a test framework and stacking tests
    
    Add a basic framework for tests of Mutter handling of client behavior;
    mutter-test-runner is a Mutter-based compositor that forks off instances
    of mutter-test-client and sends commands to them based on scripts.
    The scripts also include assertions.
    
    mutter-test-runner always runs in nested-Wayland mode since the separate
    copy of Xwayland is helpful in giving a reliably clean X server to
    test against.
    
    Initially the commands and assertions are designed to test the stacking
    behavior of Mutter, but the framework should be extensible to test other
    parts of client behavior like focus.
    
    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
    
    (Not in 'make check' to avoid breaking 'make distcheck' if Mutter can't be
    run nested.)
    
    https://bugzilla.gnome.org/show_bug.cgi?id=736505

 .gitignore                                    |    2 +
 configure.ac                                  |    6 +
 src/Makefile-tests.am                         |   46 ++
 src/Makefile.am                               |   17 +-
 src/tests/README                              |   85 ++
 src/tests/stacking/basic-wayland.metatest     |   22 +
 src/tests/stacking/basic-x11.metatest         |   19 +
 src/tests/stacking/mixed-windows.metatest     |   26 +
 src/tests/stacking/override-redirect.metatest |   19 +
 src/tests/test-client.c                       |  339 ++++++++
 src/tests/test-runner.c                       | 1069 +++++++++++++++++++++++++
 11 files changed, 1639 insertions(+), 11 deletions(-)
---
diff --git a/.gitignore b/.gitignore
index eaad8ce..7c0c7b9 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 094a055..d5b5d1e 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 0000000..c9acfb6
--- /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 75b694b..9882d7e 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 0000000..9270a16
--- /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 0000000..63ce608
--- /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 0000000..ee261ec
--- /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 0000000..38058b5
--- /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 0000000..96dde5b
--- /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 0000000..95b725c
--- /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 0000000..83c6ee4
--- /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 ();
+}


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