/*
 * SPDX-FileCopyrightText: (C) 2012 Intel Corporation
 * SPDX-License-Identifier: LGPL-2.0-or-later
 * SPDX-FileContributor: Tristan Van Berkom <tristanvb@openismus.com>
 */

/**
 * SECTION: e-test-server-utils
 * @short_description: A utility for unit testing EDS
 *
 * This test fixture provides an encapsulated testing environment for test
 * cases to test #EBookClient and #ECalClient.
 *
 * The #ETestServerFixture should be used as a fixture and must be coupled
 * with the #ETestServerClosure to configure how the test fixture will operate.
 *
 * Both the #ETestServerFixture and #ETestServerClosure can be extended with
 * more complex test fixtures, which must remember to call e_test_server_utils_setup()
 * and e_test_server_utils_teardown() in thier fixture's setup and teardown routines.
 **/

#include "evolution-data-server-config.h"

#include "e-test-server-utils.h"

#ifdef HAVE_SYS_WAIT_H
#include <sys/wait.h>
#endif

#define ADDRESS_BOOK_SOURCE_UID "test-address-book"
#define CALENDAR_SOURCE_UID     "test-calendar"

#define FINALIZE_SECONDS         30

/* 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;

/*****************************************************************
 *                    Reference management                       *
 *****************************************************************
 *
 * We need to test that an EClient actually finalizes properly
 * at the end of every test, however these EClient's have a 
 * habit of finalizing asynchronously, not even in the same thread
 * in which they were created.
 *
 * This is why we do this crazy stuff below to ensure asyncrhonous
 * finalization of the client actually happens.
 */
static void
add_weak_ref (ETestServerFixture *fixture,
              ETestServiceType service_type)
{
	GObject *object;

	switch (service_type) {
	case E_TEST_SERVER_NONE:
		g_weak_ref_set (&fixture->registry_ref, fixture->registry);
		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:

		/* They're all the same object pointer */
		object = E_TEST_SERVER_UTILS_SERVICE (fixture, GObject);
		g_weak_ref_set (&fixture->client_ref, object);
		break;
	}
}

static gboolean
object_finalize_timeout (gpointer user_data)
{
	const gchar *message = (const gchar *) user_data;

	g_error ("%s", message);

	return FALSE;
}

static gboolean
object_unref_idle (gpointer user_data)
{
	g_object_unref (user_data);

	return FALSE;
}

static void
weak_notify_loop_quit (gpointer data,
                       GObject *where_the_object_was)
{
	ETestServerFixture *fixture = (ETestServerFixture *) data;

	g_main_loop_quit (fixture->loop);
}

static void
assert_object_finalized (ETestServerFixture *fixture,
                         ETestServiceType service_type)
{
	const gchar *message = NULL;
	GObject *object = NULL;
	GWeakRef *ref = NULL;

	switch (service_type) {
	case E_TEST_SERVER_NONE:
		message = "Timed out waiting for source registery to finalize";
		ref = &fixture->registry_ref;
		break;
	case E_TEST_SERVER_ADDRESS_BOOK:
	case E_TEST_SERVER_DIRECT_ADDRESS_BOOK:
	case E_TEST_SERVER_DEPRECATED_ADDRESS_BOOK:
		message = "Timed out waiting for addressbook client to finalize";
		ref = &fixture->client_ref;
		break;
	case E_TEST_SERVER_CALENDAR:
		message = "Timed out waiting for calendar client to finalize";
		ref = &fixture->client_ref;
		break;
	}

	/* Give her a second chance */
	object = g_weak_ref_get (ref);
	if (object) {
		guint timeout_id;

		/* Add a finalize callback while we have a strong reference */
		g_object_weak_ref (object, weak_notify_loop_quit, fixture);

		/* Fail the test if we reach the timeout */
		timeout_id = g_timeout_add_seconds (
			FINALIZE_SECONDS,
			object_finalize_timeout,
			(gpointer) message);

		/* We can't release the strong reference yet, it might try
		 * to quit the main loop before we've started it.
		 *
		 * Instead release it in the loop (no need to track the source id,
		 * it's guaranteed to be removed in the scope of this test).
		 */
		g_idle_add (object_unref_idle, object);

		/* Wait for asynchronous finalization, if this loop
		 * returns then we finalized properly, if the timeout
		 * is reached then we've aborted with an error message
		 */
		g_main_loop_run (fixture->loop);

		/* If we reached here, better remove the timeout source to
		 * avoid it timing out in following tests
		 */
		g_source_remove (timeout_id);
	}
}

/*****************************************************************
 * Bootstrapping, manage work directory and create test ESource  *
 *****************************************************************/
typedef struct {
	ETestServerFixture *fixture;
	ETestServerClosure *closure;
	guint retries;
} 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 gchar *
generate_source_name (void)
{
	gchar *source_name = NULL;

	if (test_installed_services ()) {
		gchar buffer[128] = "eds-source-XXXXXX";
		gint  fd;

		fd = g_mkstemp (buffer);
		if (fd < 0)
			g_error ("Failed to generate source ID with temporary file");
		close (fd);

		source_name = g_strdup (buffer);

	} else {
		source_name = g_strdup_printf (
			"%s-%d",
			ADDRESS_BOOK_SOURCE_UID,
			global_test_source_id++);
	}

	return source_name;
}

static const gchar *args_build_dir = NULL;

static void
eds_test_utils_read_args (gint argc,
			  gchar *argv[])
{
	gint ii;

	for (ii = 0; ii < argc; ii++) {
		if (g_strcmp0 (argv[ii], "--build-dir") == 0) {
			if (ii + 1 < argc)
				args_build_dir = argv[ii + 1];
			break;
		}
	}

	g_assert_nonnull (args_build_dir);
	g_assert_true (g_file_test (args_build_dir, G_FILE_TEST_IS_DIR));
}

#define EDS_TEST_WORK_DIR_SUFFIX "tests/test-server-utils/cache"
#define EDS_TEST_DBUS_SERVICE_DIR_SUFFIX  "tests/test-server-utils/services"

static gchar *
eds_test_utils_create_build_path (const gchar *suffix)
{
	g_assert_nonnull (args_build_dir);
	g_assert_nonnull (suffix);

	return g_strconcat (args_build_dir, G_DIR_SEPARATOR_S, suffix, NULL);
}

static void
eds_test_utils_setenv (const gchar *envvar,
		       const gchar *suffix)
{
	gchar *path;

	path = eds_test_utils_create_build_path (suffix);

	g_assert_true (g_setenv (envvar, path, TRUE));

	g_free (path);
}

static void
setup_environment (gint argc,
		   gchar *argv[])
{
	GString *libs_dir;
	const gchar *libs_dir_env;

	eds_test_utils_read_args (argc, argv);

	libs_dir_env = g_getenv ("LD_LIBRARY_PATH");

	libs_dir = g_string_new ("");

	#define add_lib_path(x) G_STMT_START { \
		if (libs_dir->len) \
			g_string_append_c (libs_dir, ':'); \
		g_string_append_printf (libs_dir, "%s" G_DIR_SEPARATOR_S "%s", args_build_dir, x); \
		} G_STMT_END

	add_lib_path ("/addressbook/libebook");
	add_lib_path ("/addressbook/libebook-contacts");
	add_lib_path ("/addressbook/libedata-book");
	add_lib_path ("/calendar/libecal");
	add_lib_path ("/calendar/libedata-cal");
	add_lib_path ("/camel");
	add_lib_path ("/libebackend");
	add_lib_path ("/libedataserver");
	add_lib_path ("/libedataserverui");
	add_lib_path ("/private");
	add_lib_path ("/../tests/test-server-utils");

	#undef add_lib_path

	if (libs_dir_env && *libs_dir_env) {
		if (libs_dir->len)
			g_string_append_c (libs_dir, ':');
		g_string_append (libs_dir, libs_dir_env);
	}

	g_assert_true (g_setenv ("LD_LIBRARY_PATH", libs_dir->str, TRUE));
	eds_test_utils_setenv ("XDG_DATA_HOME", EDS_TEST_WORK_DIR_SUFFIX);
	eds_test_utils_setenv ("XDG_CACHE_HOME", EDS_TEST_WORK_DIR_SUFFIX);
	eds_test_utils_setenv ("XDG_CONFIG_HOME", EDS_TEST_WORK_DIR_SUFFIX);
	eds_test_utils_setenv ("GSETTINGS_SCHEMA_DIR", "data");
	eds_test_utils_setenv ("EDS_CALENDAR_MODULES", "src/calendar/backends/file");
	eds_test_utils_setenv ("EDS_ADDRESS_BOOK_MODULES", "src/addressbook/backends/file");
	eds_test_utils_setenv ("EDS_REGISTRY_MODULES", "src/modules/cache-reaper");
	eds_test_utils_setenv ("EDS_CAMEL_PROVIDER_DIR", "src/camel/providers/local");
	eds_test_utils_setenv ("EDS_SUBPROCESS_CAL_PATH", "src/calendar/libedata-cal/evolution-calendar-factory-subprocess");
	eds_test_utils_setenv ("EDS_SUBPROCESS_BOOK_PATH", "src/addressbook/libedata-book/evolution-addressbook-factory-subprocess");
	g_assert_true (g_setenv ("GIO_USE_VFS", "local", TRUE));
	g_assert_true (g_setenv ("EDS_TESTING", "1", TRUE));
	g_assert_true (g_setenv ("GSETTINGS_BACKEND", "memory", TRUE));

	g_unsetenv ("DISPLAY");

	g_string_free (libs_dir, TRUE);
}

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.
	 */
	gchar *workdir = eds_test_utils_create_build_path (EDS_TEST_WORK_DIR_SUFFIX);
	const gchar *argv[] = { "/bin/rm", "-rf", workdir, 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_true (spawn_succeeded);
	#ifndef G_OS_WIN32
	g_assert_true (WIFEXITED (exit_status));
	g_assert_cmpint (WEXITSTATUS (exit_status), ==, 0);
	#else
	g_assert_cmpint (exit_status, ==, 0);
	#endif

	g_free (workdir);
}

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 gboolean e_test_server_utils_retry_open_client_cb (gpointer user_data);

static void
e_test_server_utils_client_ready (GObject *source_object,
                                  GAsyncResult *res,
                                  gpointer user_data)
{
	FixturePair *pair = (FixturePair *) user_data;
	gboolean need_retry = FALSE;
	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 (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) &&
		    pair->retries < 3)
			need_retry = TRUE;
		else 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 (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) &&
		    pair->retries < 3)
			need_retry = TRUE;
		else 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 (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) &&
		    pair->retries < 3)
			need_retry = TRUE;
		else 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_NONE:
		g_assert_not_reached ();
	}

	g_clear_error (&error);

	if (need_retry) {
		pair->retries++;
		g_timeout_add_seconds (1, e_test_server_utils_retry_open_client_cb, pair);
		return;
	}

	/* Track ref counts now that we have a client */
	add_weak_ref (pair->fixture, pair->closure->type);

	g_main_loop_quit (pair->fixture->loop);
}

static void
e_test_server_utils_source_added (ESourceRegistry *registry,
                                  ESource *source,
                                  FixturePair *pair)
{
	gboolean need_retry = FALSE;
	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, (guint32) -1, 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, (guint32) -1, NULL, &error);
		} else {

			if (pair->closure->use_async_connect)
				e_book_client_connect (source, (guint32) -1, NULL, e_test_server_utils_client_ready, pair);
			else
				pair->fixture->service.book_client = (EBookClient *)
					e_book_client_connect_sync (source, (guint32) -1, NULL, &error);
		}

		if (!pair->closure->use_async_connect &&
		    !pair->fixture->service.book_client) {
			if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) &&
			    pair->retries < 3)
				need_retry = TRUE;
			else
				g_error ("Unable to create the test book: %s", error ? error->message : "Unknown error");
		}

		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) {
			if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) &&
			    pair->retries < 3)
				need_retry = TRUE;
			else
				g_error ("Unable to create the test book: %s", error->message);

			break;
		}

		if (!e_book_open (pair->fixture->service.book, FALSE, &error)) {
			if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) &&
			    pair->retries < 3)
				need_retry = TRUE;
			else
				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, (guint32) -1,
				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, (guint32) -1, NULL, &error);
			if (!pair->fixture->service.calendar_client) {
				if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) &&
				    pair->retries < 3)
					need_retry = TRUE;
				else
					g_error ("Unable to create the test calendar: %s", error->message);
			}
		}

		break;

	case E_TEST_SERVER_NONE:
		return;
	}

	g_clear_error (&error);

	if (need_retry) {
		pair->retries++;
		g_timeout_add_seconds (1, e_test_server_utils_retry_open_client_cb, pair);
		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);

	if (!pair->closure->use_async_connect)
		g_main_loop_quit (pair->fixture->loop);
}

static gboolean
e_test_server_utils_retry_open_client_cb (gpointer user_data)
{
	FixturePair *pair = user_data;
	ESource *source;

	source = e_source_registry_ref_source (pair->fixture->registry, pair->fixture->source_name);

	g_assert_true (E_IS_SOURCE (source));

	e_test_server_utils_source_added (pair->fixture->registry, source, pair);
	g_object_unref (source);

	return FALSE;
}

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);

	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 = generate_source_name ();

		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:

		if (!pair->fixture->source_name)
			pair->fixture->source_name = generate_source_name ();

		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_true (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;
}

/*****************************************************************
 *                  Fixture setup and teardown                   *
 *****************************************************************/
/**
 * e_test_server_utils_setup:
 * @fixture: An #ETestServerFixture
 * @user_data: An #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, 0 };

	/* Create work directory */
	if (!test_installed_services ()) {
		gchar *workdir = eds_test_utils_create_build_path (EDS_TEST_WORK_DIR_SUFFIX);

		g_assert_true (g_mkdir_with_parents (workdir, 0755) == 0);

		g_free (workdir);
	}

	/* Init refs */
	g_weak_ref_init (&fixture->registry_ref, NULL);
	g_weak_ref_init (&fixture->client_ref, NULL);

	fixture->loop = g_main_loop_new (NULL, FALSE);

	if (!test_installed_services ()) {
#if !GLOBAL_DBUS_DAEMON
		gchar *servicedir = eds_test_utils_create_build_path (EDS_TEST_DBUS_SERVICE_DIR_SUFFIX);

		/* 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, servicedir);

		/* Start the private D-Bus daemon */
		g_test_dbus_up (fixture->dbus);

		g_free (servicedir);
#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);
}

/**
 * e_test_server_utils_teardown:
 * @fixture: An #ETestServerFixture
 * @user_data: An #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_NONE:
		break;
	}

	/* Assert that our EClient has finalized */
	if (closure->type != E_TEST_SERVER_NONE)
		assert_object_finalized (fixture, closure->type);

	/* Try to finalize the registry now */
	g_object_run_dispose (G_OBJECT (fixture->registry));
	g_object_unref (fixture->registry);

	/* Assert that the registry finalizes */
	assert_object_finalized (fixture, E_TEST_SERVER_NONE);

	g_free (fixture->source_name);
	g_main_loop_unref (fixture->loop);
	fixture->registry = NULL;
	fixture->loop = NULL;
	fixture->service.generic = NULL;

	/* Clear refs */
	g_weak_ref_clear (&fixture->registry_ref);
	g_weak_ref_clear (&fixture->client_ref);

	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 (gint argc,
			 gchar *argv[])
{
	return e_test_server_utils_run_full (argc, argv, 0);
}

void
e_test_server_utils_prepare_run (gint argc,
				 gchar *argv[],
				 ETestServerFlags flags)
{
	/* Cleanup work directory */
	if (!test_installed_services ()) {
		/* Do this first, to have 'args_build_dir' set */
		setup_environment (argc, argv);

		if ((flags & E_TEST_SERVER_KEEP_WORK_DIRECTORY) == 0)
			delete_work_directory ();
	}

#if GLOBAL_DBUS_DAEMON
	if (!test_installed_services ()) {
		gchar *servicedir = eds_test_utils_create_build_path (EDS_TEST_DBUS_SERVICE_DIR_SUFFIX);

		/* 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, servicedir);

		/* Start the private D-Bus daemon */
		g_test_dbus_up (global_test_dbus);

		g_free (servicedir);
	}
#endif
}

void
e_test_server_utils_finish_run (void)
{
#if GLOBAL_DBUS_DAEMON
	if (!test_installed_services ()) {
		GWeakRef weak;
		GDBusConnection *connection;
		gint ii;

		/* Spin the main context briefly here, thus any pending sources
		   catching on the connection have a change to finish before
		   the test is finished. */
		connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL);
		if (connection != NULL) {
			g_weak_ref_init (&weak, connection);
			g_object_unref (connection);
		} else {
			g_weak_ref_init (&weak, NULL);
		}

		for (ii = 0; ii < 20; ii++) {
			while (g_main_context_pending (NULL)) {
				g_main_context_iteration (NULL, FALSE);
			}

			connection = g_weak_ref_get (&weak);
			if (connection != NULL) {
				g_object_unref (connection);
			} else {
				break;
			}

			g_usleep (G_USEC_PER_SEC / 20);
		}

		g_weak_ref_clear (&weak);

		/* Teardown the D-Bus Daemon */
		g_test_dbus_down (global_test_dbus);
		g_object_unref (global_test_dbus);
		global_test_dbus = NULL;
	}
#endif
}

gint
e_test_server_utils_run_full (gint argc,
			      gchar *argv[],
			      ETestServerFlags flags)
{
	gint tests_ret;

	e_test_server_utils_prepare_run (argc, argv, flags);

	/* Run the GTest suite */
	tests_ret = g_test_run ();

	e_test_server_utils_finish_run ();

	return tests_ret;
}
