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/ +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 [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 + Destroys all windows for the client, waits for that to be processed, + then instructs the client to exit. + +create / [override] + Creates a new window. For the X11 backend, the keyword 'override' + can be given to create an override-redirect + +show / +hide / + Ask the client to show (map) or hide (unmap) the given window + +activate / + 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 - + The same as 'activate', but the operation is done directly inside Mutter + and works for both backends + +raise / +lower / + 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 / + 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 / / ... + 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 . + */ + +#include +#include +#include +#include +#include +#include +#include + +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 [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 "); + 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 "); + 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 "); + 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 "); + 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 "); + 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 "); + 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 "); + 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 . + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#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 [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 [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 "); + + 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 / [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 /", 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 /", 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 (); +}