/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* e-test-server-utils.c - Test scaffolding to run tests with in-tree data server. * * Copyright (C) 2012 Intel Corporation * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) version 3. * * 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with the program; if not, see * * Authors: Tristan Van Berkom */ #include "e-test-server-utils.h" #include #define ADDRESS_BOOK_SOURCE_UID "test-address-book" #define CALENDAR_SOURCE_UID "test-calendar" /* FIXME, currently we are unable to achieve server activation * twice in a single test case, so we're using one D-Bus server * throughout an entire test suite. * * When this is fixed we can migrate the D-Bus initialization * and teardown from e_test_server_utils_run() to * e_test_server_utils_setup() and e_test_server_utils_teardown() * and this will transparantly change the way tests run using * this test framework. */ #define GLOBAL_DBUS_DAEMON 1 #if GLOBAL_DBUS_DAEMON static GTestDBus *global_test_dbus = NULL; #endif /* The ESource identifier numerical component, this should * not be needed (and should probably be removed) once we * can get rid of the GLOBAL_DBUS_DAEMON hack. */ static gint global_test_source_id = 0; typedef struct { ETestServerFixture *fixture; ETestServerClosure *closure; } FixturePair; static gboolean test_installed_services (void) { static gint use_installed_services = -1; if (use_installed_services < 0) { if (g_getenv ("TEST_INSTALLED_SERVICES") != NULL) use_installed_services = 1; else use_installed_services = 0; } return use_installed_services; } static void setup_environment (void) { g_assert (g_setenv ("XDG_DATA_HOME", EDS_TEST_WORK_DIR, TRUE)); g_assert (g_setenv ("XDG_CACHE_HOME", EDS_TEST_WORK_DIR, TRUE)); g_assert (g_setenv ("XDG_CONFIG_HOME", EDS_TEST_WORK_DIR, TRUE)); g_assert (g_setenv ("GSETTINGS_SCHEMA_DIR", EDS_TEST_SCHEMA_DIR, TRUE)); g_assert (g_setenv ("EDS_CALENDAR_MODULES", EDS_TEST_CALENDAR_DIR, TRUE)); g_assert (g_setenv ("EDS_ADDRESS_BOOK_MODULES", EDS_TEST_ADDRESS_BOOK_DIR, TRUE)); g_assert (g_setenv ("EDS_REGISTRY_MODULES", EDS_TEST_REGISTRY_DIR, TRUE)); g_assert (g_setenv ("EDS_CAMEL_PROVIDER_DIR", EDS_TEST_CAMEL_DIR, TRUE)); g_assert (g_setenv ("GIO_USE_VFS", "local", TRUE)); g_assert (g_setenv ("EDS_TESTING", "1", TRUE)); g_assert (g_setenv ("GSETTINGS_BACKEND", "memory", TRUE)); g_unsetenv ("DISPLAY"); } static void delete_work_directory (void) { /* XXX Instead of complex error checking here, we should ideally use * a recursive GDir / g_unlink() function. * * We cannot use GFile and the recursive delete function without * corrupting our contained D-Bus environment with service files * from the OS. */ const gchar *argv[] = { "/bin/rm", "-rf", EDS_TEST_WORK_DIR, NULL }; gboolean spawn_succeeded; gint exit_status; spawn_succeeded = g_spawn_sync ( NULL, (gchar **) argv, NULL, 0, NULL, NULL, NULL, NULL, &exit_status, NULL); g_assert (spawn_succeeded); g_assert (WIFEXITED (exit_status)); g_assert_cmpint (WEXITSTATUS (exit_status), ==, 0); } static gboolean e_test_server_utils_bootstrap_timeout (FixturePair *pair) { g_error ("Timed out while waiting for ESource creation from the registry"); pair->fixture->timeout_source_id = 0; return FALSE; } static void registry_weak_notify (gpointer data, GObject *where_the_object_was) { ETestServerFixture *fixture = (ETestServerFixture *) data; fixture->registry_finalized = TRUE; } static void client_weak_notify (gpointer data, GObject *where_the_object_was) { ETestServerFixture *fixture = (ETestServerFixture *) data; fixture->client_finalized = TRUE; } static void registry_weak_notify_loop_quit (gpointer data, GObject *where_the_object_was) { ETestServerFixture *fixture = (ETestServerFixture *) data; fixture->registry_finalized = TRUE; g_main_loop_quit (fixture->loop); } static void client_weak_notify_loop_quit (gpointer data, GObject *where_the_object_was) { ETestServerFixture *fixture = (ETestServerFixture *) data; fixture->client_finalized = TRUE; g_main_loop_quit (fixture->loop); } static void add_weak_ref (ETestServerFixture *fixture, ETestServiceType service_type, gboolean loop_quit) { GObject *object; switch (service_type) { case E_TEST_SERVER_NONE: if (loop_quit) g_object_weak_ref ( G_OBJECT (fixture->registry), registry_weak_notify_loop_quit, fixture); else g_object_weak_ref ( G_OBJECT (fixture->registry), registry_weak_notify, fixture); break; case E_TEST_SERVER_ADDRESS_BOOK: case E_TEST_SERVER_DIRECT_ADDRESS_BOOK: case E_TEST_SERVER_CALENDAR: case E_TEST_SERVER_DEPRECATED_ADDRESS_BOOK: case E_TEST_SERVER_DEPRECATED_CALENDAR: /* They're all the same object pointer */ object = E_TEST_SERVER_UTILS_SERVICE (fixture, GObject); if (loop_quit) g_object_weak_ref (object, client_weak_notify_loop_quit, fixture); else g_object_weak_ref (object, client_weak_notify, fixture); break; } } static void remove_weak_ref (ETestServerFixture *fixture, ETestServiceType service_type, gboolean loop_quit) { GObject *object; switch (service_type) { case E_TEST_SERVER_NONE: if (loop_quit) g_object_weak_unref ( G_OBJECT (fixture->registry), registry_weak_notify_loop_quit, fixture); else g_object_weak_unref ( G_OBJECT (fixture->registry), registry_weak_notify, fixture); break; case E_TEST_SERVER_ADDRESS_BOOK: case E_TEST_SERVER_DIRECT_ADDRESS_BOOK: case E_TEST_SERVER_CALENDAR: case E_TEST_SERVER_DEPRECATED_ADDRESS_BOOK: case E_TEST_SERVER_DEPRECATED_CALENDAR: /* They're all the same object pointer */ object = E_TEST_SERVER_UTILS_SERVICE (fixture, GObject); if (loop_quit) g_object_weak_unref (object, client_weak_notify_loop_quit, fixture); else g_object_weak_unref (object, client_weak_notify, fixture); break; } } static void e_test_server_utils_client_ready (GObject *source_object, GAsyncResult *res, gpointer user_data) { FixturePair *pair = (FixturePair *) user_data; GError *error = NULL; switch (pair->closure->type) { case E_TEST_SERVER_ADDRESS_BOOK: pair->fixture->service.book_client = (EBookClient *) e_book_client_connect_finish (res, &error); if (!pair->fixture->service.book_client) g_error ("Unable to create the test book: %s", error->message); break; case E_TEST_SERVER_DIRECT_ADDRESS_BOOK: pair->fixture->service.book_client = (EBookClient *) e_book_client_connect_direct_finish (res, &error); if (!pair->fixture->service.book_client) g_error ("Unable to create the test book: %s", error->message); break; case E_TEST_SERVER_CALENDAR: pair->fixture->service.calendar_client = (ECalClient *) e_cal_client_connect_finish (res, &error); if (!pair->fixture->service.calendar_client) g_error ("Unable to create the test calendar: %s", error->message); break; case E_TEST_SERVER_DEPRECATED_ADDRESS_BOOK: case E_TEST_SERVER_DEPRECATED_CALENDAR: case E_TEST_SERVER_NONE: g_assert_not_reached (); } /* Track ref counts */ add_weak_ref (pair->fixture, pair->closure->type, FALSE); g_main_loop_quit (pair->fixture->loop); } static void e_test_server_utils_source_added (ESourceRegistry *registry, ESource *source, FixturePair *pair) { GError *error = NULL; if (g_strcmp0 (e_source_get_uid (source), pair->fixture->source_name) != 0) return; switch (pair->closure->type) { case E_TEST_SERVER_ADDRESS_BOOK: case E_TEST_SERVER_DIRECT_ADDRESS_BOOK: if (pair->closure->type == E_TEST_SERVER_DIRECT_ADDRESS_BOOK) { if (pair->closure->use_async_connect) e_book_client_connect_direct (source, NULL, e_test_server_utils_client_ready, pair); else pair->fixture->service.book_client = (EBookClient *) e_book_client_connect_direct_sync ( pair->fixture->registry, source, NULL, &error); } else { if (pair->closure->use_async_connect) e_book_client_connect (source, NULL, e_test_server_utils_client_ready, pair); else pair->fixture->service.book_client = (EBookClient *) e_book_client_connect_sync (source, NULL, &error); } if (!pair->closure->use_async_connect && !pair->fixture->service.book_client) g_error ("Unable to create the test book: %s", error->message); break; case E_TEST_SERVER_DEPRECATED_ADDRESS_BOOK: /* Dont bother testing the Async apis for deprecated APIs */ pair->fixture->service.book = e_book_new (source, &error); if (!pair->fixture->service.book) g_error ("Unable to create the test book: %s", error->message); if (!e_book_open (pair->fixture->service.book, FALSE, &error)) g_error ("Unable to open book: %s", error->message); break; case E_TEST_SERVER_CALENDAR: if (pair->closure->use_async_connect) { e_cal_client_connect ( source, pair->closure->calendar_source_type, NULL, e_test_server_utils_client_ready, pair); } else { pair->fixture->service.calendar_client = (ECalClient *) e_cal_client_connect_sync ( source, pair->closure->calendar_source_type, NULL, &error); if (!pair->fixture->service.calendar_client) g_error ("Unable to create the test calendar: %s", error->message); } break; case E_TEST_SERVER_DEPRECATED_CALENDAR: /* Dont bother testing the Async apis for deprecated APIs */ pair->fixture->service.calendar = e_cal_new (source, pair->closure->calendar_source_type); if (!pair->fixture->service.calendar) g_error ("Unable to create the test calendar"); if (!e_cal_open (pair->fixture->service.calendar, FALSE, &error)) g_error ("Unable to open calendar: %s", error->message); break; case E_TEST_SERVER_NONE: return; } /* Add the weak ref now if we just created it */ if (pair->closure->type != E_TEST_SERVER_NONE && pair->closure->use_async_connect == FALSE) add_weak_ref (pair->fixture, pair->closure->type, FALSE); if (!pair->closure->use_async_connect) g_main_loop_quit (pair->fixture->loop); } static gboolean e_test_server_utils_bootstrap_idle (FixturePair *pair) { ESourceBackend *backend = NULL; ESource *scratch = NULL; GError *error = NULL; pair->fixture->registry = e_source_registry_new_sync (NULL, &error); if (!pair->fixture->registry) g_error ("Unable to create the test registry: %s", error->message); /* Add weak ref for the registry */ add_weak_ref (pair->fixture, E_TEST_SERVER_NONE, FALSE); g_signal_connect ( pair->fixture->registry, "source-added", G_CALLBACK (e_test_server_utils_source_added), pair); /* Create an address book */ switch (pair->closure->type) { case E_TEST_SERVER_ADDRESS_BOOK: case E_TEST_SERVER_DIRECT_ADDRESS_BOOK: case E_TEST_SERVER_DEPRECATED_ADDRESS_BOOK: if (!pair->fixture->source_name) pair->fixture->source_name = g_strdup_printf ("%s-%d", ADDRESS_BOOK_SOURCE_UID, global_test_source_id++); scratch = e_source_new_with_uid (pair->fixture->source_name, NULL, &error); if (!scratch) g_error ("Failed to create scratch source for an addressbook: %s", error->message); /* Ensure Book type */ backend = e_source_get_extension (scratch, E_SOURCE_EXTENSION_ADDRESS_BOOK); e_source_backend_set_backend_name (backend, "local"); break; case E_TEST_SERVER_CALENDAR: case E_TEST_SERVER_DEPRECATED_CALENDAR: if (!pair->fixture->source_name) pair->fixture->source_name = g_strdup_printf ("%s-%d", CALENDAR_SOURCE_UID, global_test_source_id++); scratch = e_source_new_with_uid (pair->fixture->source_name, NULL, &error); if (!scratch) g_error ("Failed to create scratch source for a calendar: %s", error->message); /* Ensure Calendar type source (how to specify the backend here ?? */ backend = e_source_get_extension (scratch, E_SOURCE_EXTENSION_CALENDAR); e_source_backend_set_backend_name (backend, "local"); break; case E_TEST_SERVER_NONE: break; } if (scratch) { if (pair->closure->customize) pair->closure->customize (scratch, pair->closure); if (!e_source_registry_commit_source_sync (pair->fixture->registry, scratch, NULL, &error)) { /* Allow sources to carry from one test to the next, if the keep_work_directory * semantics are used then that's what we want (to reuse a source from the * previous test case). */ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS)) { ESource *source = e_source_registry_ref_source ( pair->fixture->registry, pair->fixture->source_name); g_clear_error (&error); g_assert (E_IS_SOURCE (source)); e_test_server_utils_source_added (pair->fixture->registry, source, pair); g_object_unref (source); } else g_error ("Unable to add new addressbook source to the registry: %s", error->message); } g_object_unref (scratch); } if (pair->closure->type != E_TEST_SERVER_NONE) pair->fixture->timeout_source_id = g_timeout_add_seconds (20, (GSourceFunc) e_test_server_utils_bootstrap_timeout, pair); else g_main_loop_quit (pair->fixture->loop); return FALSE; } /** * e_test_server_utils_setup: * @fixture: A #ETestServerFixture * @user_data: A #ETestServerClosure or derived structure provided by the test. * * A setup function for the #ETestServerFixture fixture */ void e_test_server_utils_setup (ETestServerFixture *fixture, gconstpointer user_data) { ETestServerClosure *closure = (ETestServerClosure *) user_data; FixturePair pair = { fixture, closure }; /* Create work directory */ g_assert (g_mkdir_with_parents (EDS_TEST_WORK_DIR, 0755) == 0); fixture->loop = g_main_loop_new (NULL, FALSE); if (!test_installed_services ()) { #if !GLOBAL_DBUS_DAEMON /* Create the global dbus-daemon for this test suite */ fixture->dbus = g_test_dbus_new (G_TEST_DBUS_NONE); /* Add the private directory with our in-tree service files */ g_test_dbus_add_service_dir (fixture->dbus, EDS_TEST_DBUS_SERVICE_DIR); /* Start the private D-Bus daemon */ g_test_dbus_up (fixture->dbus); #else fixture->dbus = global_test_dbus; #endif } g_idle_add ((GSourceFunc) e_test_server_utils_bootstrap_idle, &pair); g_main_loop_run (fixture->loop); /* This needs to be explicitly removed, otherwise the timeout source * stays in the default GMainContext and after running tests for 20 seconds * in the same test suite... the tests bail out. */ if (fixture->timeout_source_id) { g_source_remove (fixture->timeout_source_id); fixture->timeout_source_id = 0; } g_signal_handlers_disconnect_by_func (fixture->registry, e_test_server_utils_source_added, &pair); } static gboolean timeout_client_shutdown (gpointer user_data) { const gchar *message = (const gchar *)user_data; g_error ("%s", message); return FALSE; } /** * e_test_server_utils_teardown: * @fixture: A #ETestServerFixture * @user_data: A #ETestServerClosure or derived structure provided by the test. * * A teardown function for the #ETestServerFixture fixture */ void e_test_server_utils_teardown (ETestServerFixture *fixture, gconstpointer user_data) { ETestServerClosure *closure = (ETestServerClosure *) user_data; GError *error = NULL; /* Try to finalize the EClient */ switch (closure->type) { case E_TEST_SERVER_ADDRESS_BOOK: case E_TEST_SERVER_DIRECT_ADDRESS_BOOK: if (!closure->keep_work_directory && !e_client_remove_sync (E_CLIENT (fixture->service.book_client), NULL, &error)) { g_message ("Failed to remove test book: %s (ignoring)", error->message); g_clear_error (&error); } g_object_unref (fixture->service.book_client); fixture->service.book_client = NULL; break; case E_TEST_SERVER_DEPRECATED_ADDRESS_BOOK: if (!closure->keep_work_directory && !e_book_remove (fixture->service.book, &error)) { g_message ("Failed to remove test book: %s (ignoring)", error->message); g_clear_error (&error); } g_object_unref (fixture->service.book); fixture->service.book = NULL; break; case E_TEST_SERVER_CALENDAR: if (!closure->keep_work_directory && !e_client_remove_sync (E_CLIENT (fixture->service.calendar_client), NULL, &error)) { g_message ("Failed to remove test calendar: %s (ignoring)", error->message); g_clear_error (&error); } g_object_unref (fixture->service.calendar_client); fixture->service.calendar_client = NULL; break; case E_TEST_SERVER_DEPRECATED_CALENDAR: if (!closure->keep_work_directory && !e_cal_remove (fixture->service.calendar, &error)) { g_message ("Failed to remove test calendar: %s (ignoring)", error->message); g_clear_error (&error); } g_object_unref (fixture->service.calendar); fixture->service.calendar = NULL; case E_TEST_SERVER_NONE: break; } /* Give her a second chance */ if (closure->type != E_TEST_SERVER_NONE && fixture->client_finalized == FALSE) { guint timeout_id; timeout_id = g_timeout_add_seconds (10, timeout_client_shutdown, (gpointer)"Timed out waiting for EClient to finalize"); remove_weak_ref (fixture, closure->type, FALSE); add_weak_ref (fixture, closure->type, TRUE); g_main_loop_run (fixture->loop); g_source_remove (timeout_id); } /* Try to finalize the registry */ g_object_run_dispose (G_OBJECT (fixture->registry)); g_object_unref (fixture->registry); /* Give her a second chance */ if (fixture->registry_finalized == FALSE) { guint timeout_id; timeout_id = g_timeout_add_seconds (10, timeout_client_shutdown, (gpointer)"Timed out waiting for source registery to finalize"); remove_weak_ref (fixture, E_TEST_SERVER_NONE, FALSE); add_weak_ref (fixture, E_TEST_SERVER_NONE, TRUE); g_main_loop_run (fixture->loop); g_source_remove (timeout_id); } g_free (fixture->source_name); g_main_loop_unref (fixture->loop); fixture->registry = NULL; fixture->loop = NULL; fixture->service.generic = NULL; if (!test_installed_services ()) { #if !GLOBAL_DBUS_DAEMON /* Teardown the D-Bus Daemon * * Note that we intentionally leak the TestDBus daemon * in this case, presumably this is due to some leaked * GDBusConnection reference counting */ g_test_dbus_down (fixture->dbus); g_object_unref (fixture->dbus); fixture->dbus = NULL; #else fixture->dbus = NULL; #endif } /* Cleanup work directory * * XXX This is avoided for now since we are currently using * a separate ESource UID for each test, removing the work directory * would cause the cache-reaper module to spew error messages when * attempting to move missing removed ESources to the trash. * * This should probably be all completely removed once the * GLOBAL_DBUS_DAEMON clauses can be removed. */ /* if (!closure->keep_work_directory && !test_installed_services ()) */ /* delete_work_directory (); */ /* Destroy dynamically allocated closure */ if (closure->destroy_closure_func) closure->destroy_closure_func (closure); } gint e_test_server_utils_run (void) { return e_test_server_utils_run_full (0); } gint e_test_server_utils_run_full (ETestServerFlags flags) { gint tests_ret; /* Cleanup work directory */ if ((flags & E_TEST_SERVER_KEEP_WORK_DIRECTORY) == 0) delete_work_directory (); setup_environment (); #if GLOBAL_DBUS_DAEMON if (!test_installed_services ()) { /* Create the global dbus-daemon for this test suite */ global_test_dbus = g_test_dbus_new (G_TEST_DBUS_NONE); /* Add the private directory with our in-tree service files */ g_test_dbus_add_service_dir (global_test_dbus, EDS_TEST_DBUS_SERVICE_DIR); /* Start the private D-Bus daemon */ g_test_dbus_up (global_test_dbus); } #endif /* Run the GTest suite */ tests_ret = g_test_run (); #if GLOBAL_DBUS_DAEMON if (!test_installed_services ()) { /* Teardown the D-Bus Daemon * * Note that we intentionally leak the TestDBus daemon * in this case, presumably this is due to some leaked * GDBusConnection reference counting */ g_test_dbus_stop (global_test_dbus); /* g_object_unref (global_test_dbus); */ global_test_dbus = NULL; } #endif return tests_ret; }