[gnome-builder/wip/chergert/unittests] testing: start on unit testing
- From: Christian Hergert <chergert src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-builder/wip/chergert/unittests] testing: start on unit testing
- Date: Fri, 20 Oct 2017 09:53:35 +0000 (UTC)
commit 36541591ee53644295be0c75dce09f0a56491007
Author: Christian Hergert <chergert redhat com>
Date: Fri Oct 13 17:40:19 2017 -0700
testing: start on unit testing
src/libide/ide-context.c | 50 +++-
src/libide/ide-context.h | 1 +
src/libide/ide-types.h | 4 +
src/libide/ide.h | 2 +
src/libide/libide.gresource.xml | 5 +
src/libide/meson.build | 1 +
src/libide/testing/ide-test-editor-addin.c | 106 +++++
src/libide/testing/ide-test-editor-addin.h | 29 ++
src/libide/testing/ide-test-manager.c | 569 +++++++++++++++++++++++++++
src/libide/testing/ide-test-manager.h | 45 +++
src/libide/testing/ide-test-panel.c | 269 +++++++++++++
src/libide/testing/ide-test-panel.h | 29 ++
src/libide/testing/ide-test-panel.ui | 26 ++
src/libide/testing/ide-test-private.h | 41 ++
src/libide/testing/ide-test-provider.c | 233 +++++++++++
src/libide/testing/ide-test-provider.h | 72 ++++
src/libide/testing/ide-test.c | 338 ++++++++++++++++
src/libide/testing/ide-test.h | 55 +++
src/libide/testing/meson.build | 23 ++
src/libide/testing/testing-plugin.c | 28 ++
src/libide/testing/testing.plugin | 9 +
src/plugins/meson/gbp-meson-test-provider.c | 348 ++++++++++++++++
src/plugins/meson/gbp-meson-test-provider.h | 29 ++
src/plugins/meson/gbp-meson-test.c | 196 +++++++++
src/plugins/meson/gbp-meson-test.h | 34 ++
src/plugins/meson/meson-plugin.c | 2 +
src/plugins/meson/meson.build | 4 +
27 files changed, 2547 insertions(+), 1 deletions(-)
---
diff --git a/src/libide/ide-context.c b/src/libide/ide-context.c
index 9e5a826..6ad0ced 100644
--- a/src/libide/ide-context.c
+++ b/src/libide/ide-context.c
@@ -39,6 +39,7 @@
#include "debugger/ide-debug-manager.h"
#include "devices/ide-device-manager.h"
#include "doap/ide-doap.h"
+#include "documentation/ide-documentation.h"
#include "plugins/ide-extension-util.h"
#include "projects/ide-project-files.h"
#include "projects/ide-project-item.h"
@@ -49,7 +50,7 @@
#include "search/ide-search-engine.h"
#include "search/ide-search-provider.h"
#include "snippets/ide-source-snippets-manager.h"
-#include "documentation/ide-documentation.h"
+#include "testing/ide-test-manager.h"
#include "transfers/ide-transfer-manager.h"
#include "util/ide-async-helper.h"
#include "util/ide-settings.h"
@@ -78,6 +79,7 @@ struct _IdeContext
IdeRuntimeManager *runtime_manager;
IdeSearchEngine *search_engine;
IdeSourceSnippetsManager *snippets_manager;
+ IdeTestManager *test_manager;
IdeProject *project;
GFile *project_file;
gchar *root_build_dir;
@@ -534,6 +536,7 @@ ide_context_finalize (GObject *object)
g_clear_object (&self->project_file);
g_clear_object (&self->recent_manager);
g_clear_object (&self->runtime_manager);
+ g_clear_object (&self->test_manager);
g_clear_object (&self->unsaved_files);
g_clear_object (&self->vcs);
@@ -817,6 +820,10 @@ ide_context_init (IdeContext *self)
"context", self,
NULL);
+ self->test_manager = g_object_new (IDE_TYPE_TEST_MANAGER,
+ "context", self,
+ NULL);
+
self->unsaved_files = g_object_new (IDE_TYPE_UNSAVED_FILES,
"context", self,
NULL);
@@ -1118,6 +1125,28 @@ ide_context_init_snippets (gpointer source_object,
}
static void
+ide_context_init_tests (gpointer source_object,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ IdeContext *self = source_object;
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(GError) error = NULL;
+
+ g_return_if_fail (IDE_IS_CONTEXT (self));
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_priority (task, G_PRIORITY_LOW);
+ g_task_set_source_tag (task, ide_context_init_tests);
+
+ if (!g_initable_init (G_INITABLE (self->test_manager), cancellable, &error))
+ g_task_return_error (task, g_steal_pointer (&error));
+ else
+ g_task_return_boolean (task, TRUE);
+}
+
+static void
ide_context_service_added (PeasExtensionSet *set,
PeasPluginInfo *info,
PeasExtension *exten,
@@ -1648,6 +1677,7 @@ ide_context_init_async (GAsyncInitable *initable,
ide_context_init_build_manager,
ide_context_init_run_manager,
ide_context_init_diagnostics_manager,
+ ide_context_init_tests,
ide_context_init_loaded,
NULL);
}
@@ -2320,6 +2350,24 @@ ide_context_get_debug_manager (IdeContext *self)
}
/**
+ * ide_context_get_test_manager:
+ * @self: An #IdeTestManager
+ *
+ * Gets the test manager for the #IdeContext.
+ *
+ * Returns: (transfer none): An #IdeTestManager
+ *
+ * Since: 3.28
+ */
+IdeTestManager *
+ide_context_get_test_manager (IdeContext *self)
+{
+ g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+
+ return self->test_manager;
+}
+
+/**
* ide_context_add_pausable:
* @self: an #IdeContext
* @pausable: an #IdePausable
diff --git a/src/libide/ide-context.h b/src/libide/ide-context.h
index e2a3351..5b5161e 100644
--- a/src/libide/ide-context.h
+++ b/src/libide/ide-context.h
@@ -47,6 +47,7 @@ IdeSettings *ide_context_get_settings (IdeContext
const gchar *schema_id,
const gchar *relative_path);
IdeSourceSnippetsManager *ide_context_get_snippets_manager (IdeContext *self);
+IdeTestManager *ide_context_get_test_manager (IdeContext *self);
IdeUnsavedFiles *ide_context_get_unsaved_files (IdeContext *self);
IdeVcs *ide_context_get_vcs (IdeContext *self);
const gchar *ide_context_get_root_build_dir (IdeContext *self);
diff --git a/src/libide/ide-types.h b/src/libide/ide-types.h
index 49a43de..72a05b6 100644
--- a/src/libide/ide-types.h
+++ b/src/libide/ide-types.h
@@ -129,6 +129,10 @@ typedef struct _IdeSubprocessLauncher IdeSubprocessLauncher;
typedef struct _IdeSymbol IdeSymbol;
typedef struct _IdeSymbolResolver IdeSymbolResolver;
+typedef struct _IdeTest IdeTest;
+typedef struct _IdeTestManager IdeTestManager;
+typedef struct _IdeTestProvider IdeTestProvider;
+
typedef struct _IdeTransferManager IdeTransferManager;
typedef struct _IdeTransfer IdeTransfer;
diff --git a/src/libide/ide.h b/src/libide/ide.h
index a12d853..61e2023 100644
--- a/src/libide/ide.h
+++ b/src/libide/ide.h
@@ -165,6 +165,8 @@ G_BEGIN_DECLS
#include "symbols/ide-tags-builder.h"
#include "template/ide-project-template.h"
#include "template/ide-template-provider.h"
+#include "testing/ide-test.h"
+#include "testing/ide-test-provider.h"
#include "threading/ide-thread-pool.h"
#include "transfers/ide-pkcon-transfer.h"
#include "transfers/ide-transfer.h"
diff --git a/src/libide/libide.gresource.xml b/src/libide/libide.gresource.xml
index a060e6a..8a34d85 100644
--- a/src/libide/libide.gresource.xml
+++ b/src/libide/libide.gresource.xml
@@ -81,6 +81,7 @@
<file preprocess="xml-stripblanks"
alias="ide-preferences-language-row.ui">preferences/ide-preferences-language-row.ui</file>
<file preprocess="xml-stripblanks"
alias="ide-preferences-window.ui">preferences/ide-preferences-window.ui</file>
<file preprocess="xml-stripblanks" alias="ide-run-button.ui">runner/ide-run-button.ui</file>
+ <file preprocess="xml-stripblanks" alias="ide-test-panel.ui">testing/ide-test-panel.ui</file>
<file preprocess="xml-stripblanks" alias="ide-transfer-row.ui">transfers/ide-transfer-row.ui</file>
<file preprocess="xml-stripblanks"
alias="ide-transfers-button.ui">transfers/ide-transfers-button.ui</file>
<file preprocess="xml-stripblanks"
alias="ide-shortcuts-window.ui">keybindings/ide-shortcuts-window.ui</file>
@@ -107,6 +108,10 @@
<file alias="directory.plugin">directory/directory.plugin</file>
</gresource>
+ <gresource prefix="/org/gnome/builder/plugins/testing">
+ <file alias="testing.plugin">testing/testing.plugin</file>
+ </gresource>
+
<gresource prefix="/org/gnome/builder/plugins/webkit">
<file alias="webkit.plugin">webkit/webkit.plugin</file>
</gresource>
diff --git a/src/libide/meson.build b/src/libide/meson.build
index 5e1473f..67fc7af 100644
--- a/src/libide/meson.build
+++ b/src/libide/meson.build
@@ -87,6 +87,7 @@ subdir('sourceview')
subdir('subprocess')
subdir('symbols')
subdir('template')
+subdir('testing')
subdir('threading')
subdir('transfers')
subdir('util')
diff --git a/src/libide/testing/ide-test-editor-addin.c b/src/libide/testing/ide-test-editor-addin.c
new file mode 100644
index 0000000..570aef0
--- /dev/null
+++ b/src/libide/testing/ide-test-editor-addin.c
@@ -0,0 +1,106 @@
+/* ide-test-editor-addin.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-test-editor-addin"
+
+#include <glib/gi18n.h>
+
+#include "ide-context.h"
+
+#include "editor/ide-editor-addin.h"
+#include "editor/ide-editor-perspective.h"
+#include "editor/ide-editor-sidebar.h"
+#include "testing/ide-test-editor-addin.h"
+#include "testing/ide-test-panel.h"
+#include "util/ide-gtk.h"
+
+struct _IdeTestEditorAddin
+{
+ GObject parent_instance;
+
+ IdeTestPanel *panel;
+};
+
+static void
+ide_test_editor_addin_load (IdeEditorAddin *addin,
+ IdeEditorPerspective *editor)
+{
+ IdeTestEditorAddin *self = (IdeTestEditorAddin *)addin;
+ IdeEditorSidebar *sidebar;
+ IdeTestManager *manager;
+ IdeContext *context;
+
+ g_assert (IDE_IS_TEST_EDITOR_ADDIN (self));
+ g_assert (IDE_IS_EDITOR_PERSPECTIVE (editor));
+
+ context = ide_widget_get_context (GTK_WIDGET (editor));
+ manager = ide_context_get_test_manager (context);
+
+ self->panel = g_object_new (IDE_TYPE_TEST_PANEL,
+ "manager", manager,
+ "visible", TRUE,
+ NULL);
+ g_signal_connect (self->panel,
+ "destroy",
+ G_CALLBACK (gtk_widget_destroy),
+ &self->panel);
+
+ sidebar = ide_editor_perspective_get_sidebar (editor);
+
+ ide_editor_sidebar_add_section (sidebar,
+ "tests",
+ _("Unit Tests"),
+ "builder-unit-tests-symbolic",
+ NULL,
+ NULL,
+ GTK_WIDGET (self->panel),
+ 400);
+}
+
+static void
+ide_test_editor_addin_unload (IdeEditorAddin *addin,
+ IdeEditorPerspective *editor)
+{
+ IdeTestEditorAddin *self = (IdeTestEditorAddin *)addin;
+
+ g_assert (IDE_IS_TEST_EDITOR_ADDIN (self));
+ g_assert (IDE_IS_EDITOR_PERSPECTIVE (editor));
+
+ if (self->panel)
+ gtk_widget_destroy (GTK_WIDGET (self->panel));
+}
+
+static void
+editor_addin_iface_init (IdeEditorAddinInterface *iface)
+{
+ iface->load = ide_test_editor_addin_load;
+ iface->unload = ide_test_editor_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (IdeTestEditorAddin, ide_test_editor_addin, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (IDE_TYPE_EDITOR_ADDIN, editor_addin_iface_init))
+
+static void
+ide_test_editor_addin_class_init (IdeTestEditorAddinClass *klass)
+{
+}
+
+static void
+ide_test_editor_addin_init (IdeTestEditorAddin *self)
+{
+}
diff --git a/src/libide/testing/ide-test-editor-addin.h b/src/libide/testing/ide-test-editor-addin.h
new file mode 100644
index 0000000..64fcf1d
--- /dev/null
+++ b/src/libide/testing/ide-test-editor-addin.h
@@ -0,0 +1,29 @@
+/* ide-test-editor-addin.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TEST_EDITOR_ADDIN (ide_test_editor_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeTestEditorAddin, ide_test_editor_addin, IDE, TEST_EDITOR_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/libide/testing/ide-test-manager.c b/src/libide/testing/ide-test-manager.c
new file mode 100644
index 0000000..84dd098
--- /dev/null
+++ b/src/libide/testing/ide-test-manager.c
@@ -0,0 +1,569 @@
+/* ide-test-manager.c
+ *
+ * Copyright © 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-test-manager"
+
+#include <dazzle.h>
+#include <libpeas/peas.h>
+
+#include "ide-debug.h"
+
+#include "testing/ide-test-manager.h"
+#include "testing/ide-test-private.h"
+#include "testing/ide-test-provider.h"
+
+/**
+ * SECTION:ide-test-manager
+ * @title: IdeTestManager
+ * @short_description: Unit test discover and execution manager
+ *
+ * The #IdeTestManager is responsible for loading unit test provider
+ * plugins (via the #IdeTestProvider interface) and running those unit
+ * tests on behalf of the user.
+ *
+ * You can access the test manager using ide_context_get_text_manager()
+ * using the #IdeContext for the loaded project.
+ *
+ * Since: 3.28
+ */
+
+struct _IdeTestManager
+{
+ IdeObject parent_instance;
+ PeasExtensionSet *providers;
+ GPtrArray *tests_by_provider;
+ GtkTreeStore *tests_store;
+ guint busy : 1;
+};
+
+typedef struct
+{
+ IdeTestProvider *provider;
+ GPtrArray *tests;
+} TestsByProvider;
+
+enum {
+ PROP_0,
+ PROP_BUSY,
+ N_PROPS
+};
+
+static void initable_iface_init (GInitableIface *iface);
+static void ide_test_manager_actions_run_all (IdeTestManager *self,
+ GVariant *param);
+
+DZL_DEFINE_ACTION_GROUP (IdeTestManager, ide_test_manager, {
+ { "run-all", ide_test_manager_actions_run_all },
+})
+
+G_DEFINE_TYPE_WITH_CODE (IdeTestManager, ide_test_manager, IDE_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init)
+ G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP,
+ ide_test_manager_init_action_group))
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+tests_by_provider_free (gpointer data)
+{
+ TestsByProvider *info = data;
+
+ g_clear_pointer (&info->tests, g_ptr_array_unref);
+ g_clear_object (&info->provider);
+ g_slice_free (TestsByProvider, info);
+}
+
+static void
+ide_test_manager_dispose (GObject *object)
+{
+ IdeTestManager *self = (IdeTestManager *)object;
+
+ g_clear_object (&self->providers);
+ g_clear_object (&self->tests_store);
+ g_clear_pointer (&self->tests_by_provider, g_ptr_array_unref);
+
+ G_OBJECT_CLASS (ide_test_manager_parent_class)->dispose (object);
+}
+
+static void
+ide_test_manager_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ IdeTestManager *self = IDE_TEST_MANAGER (object);
+
+ switch (prop_id)
+ {
+ case PROP_BUSY:
+ g_value_set_boolean (value, self->busy);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_test_manager_class_init (IdeTestManagerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = ide_test_manager_dispose;
+ object_class->get_property = ide_test_manager_get_property;
+
+ /**
+ * IdeTestManager:busy:
+ *
+ * The "busy" property indicates if the #IdeTestManager is currently
+ * processing various background unit tests.
+ *
+ * Since: 3.28
+ */
+ properties [PROP_BUSY] =
+ g_param_spec_boolean ("busy",
+ "Busy",
+ "If the test manager is busy processing",
+ FALSE,
+ (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_test_manager_init (IdeTestManager *self)
+{
+ self->tests_by_provider = g_ptr_array_new_with_free_func (tests_by_provider_free);
+ self->tests_store = gtk_tree_store_new (2, G_TYPE_STRING, IDE_TYPE_TEST);
+}
+
+static void
+ide_test_manager_locate_group (IdeTestManager *self,
+ GtkTreeIter *iter,
+ const gchar *group)
+{
+ g_assert (IDE_IS_TEST_MANAGER (self));
+ g_assert (iter != NULL);
+
+ if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (self->tests_store), iter))
+ {
+ do
+ {
+ g_autofree gchar *row_group = NULL;
+
+ gtk_tree_model_get (GTK_TREE_MODEL (self->tests_store), iter,
+ IDE_TEST_COLUMN_GROUP, &row_group,
+ -1);
+
+ if (ide_str_equal0 (row_group, group))
+ return;
+ }
+ while (gtk_tree_model_iter_next (GTK_TREE_MODEL (self->tests_store), iter));
+ }
+
+ /* TODO: Sort groups by name? */
+
+ gtk_tree_store_append (self->tests_store, iter, NULL);
+ gtk_tree_store_set (self->tests_store, iter,
+ IDE_TEST_COLUMN_GROUP, group,
+ -1);
+}
+
+static void
+ide_test_manager_add_test (IdeTestManager *self,
+ const TestsByProvider *info,
+ guint position,
+ IdeTest *test)
+{
+ const gchar *group;
+ GtkTreeIter iter;
+ GtkTreeIter parent;
+
+ IDE_ENTRY;
+
+ g_assert (IDE_IS_TEST_MANAGER (self));
+ g_assert (info != NULL);
+ g_assert (IDE_IS_TEST (test));
+
+ g_ptr_array_insert (info->tests, position, g_object_ref (test));
+
+ group = ide_test_get_group (test);
+
+ ide_test_manager_locate_group (self, &parent, group);
+ gtk_tree_store_append (self->tests_store, &iter, &parent);
+ gtk_tree_store_set (self->tests_store, &iter,
+ IDE_TEST_COLUMN_GROUP, NULL,
+ IDE_TEST_COLUMN_TEST, test,
+ -1);
+
+ IDE_EXIT;
+}
+
+static void
+ide_test_manager_remove_test (IdeTestManager *self,
+ const TestsByProvider *info,
+ IdeTest *test)
+{
+ const gchar *group;
+ GtkTreeIter iter;
+ GtkTreeIter parent;
+
+ IDE_ENTRY;
+
+ g_assert (IDE_IS_TEST_MANAGER (self));
+ g_assert (info != NULL);
+ g_assert (IDE_IS_TEST (test));
+
+ ide_test_manager_locate_group (self, &parent, group);
+
+ if (gtk_tree_model_iter_children (GTK_TREE_MODEL (self->tests_store), &iter, &parent))
+ {
+ do
+ {
+ g_autoptr(IdeTest) row = NULL;
+
+ gtk_tree_model_get (GTK_TREE_MODEL (self->tests_store), &iter,
+ IDE_TEST_COLUMN_TEST, &row,
+ -1);
+
+ if (row == test)
+ {
+ gtk_tree_store_remove (self->tests_store, &iter);
+ break;
+ }
+ }
+ while (gtk_tree_model_iter_next (GTK_TREE_MODEL (self->tests_store), &iter));
+ }
+
+ g_ptr_array_remove (info->tests, test);
+
+ IDE_EXIT;
+}
+
+static void
+ide_test_manager_provider_items_changed (IdeTestManager *self,
+ guint position,
+ guint removed,
+ guint added,
+ IdeTestProvider *provider)
+{
+ IDE_ENTRY;
+
+ g_assert (IDE_IS_TEST_MANAGER (self));
+ g_assert (IDE_IS_TEST_PROVIDER (provider));
+
+ for (guint i = 0; i < self->tests_by_provider->len; i++)
+ {
+ const TestsByProvider *info = g_ptr_array_index (self->tests_by_provider, i);
+
+ if (info->provider == provider)
+ {
+ /* Remove tests from cache that were deleted */
+ for (guint j = 0; j < removed; j++)
+ {
+ IdeTest *test = g_ptr_array_index (info->tests, position);
+ ide_test_manager_remove_test (self, info, test);
+ }
+
+ /* Add tests to cache that were added */
+ for (guint j = 0; j < added; j++)
+ {
+ g_autoptr(IdeTest) test = NULL;
+
+ test = g_list_model_get_item (G_LIST_MODEL (provider), position + j);
+ ide_test_manager_add_test (self, info, position + j, test);
+ }
+ }
+ }
+
+ IDE_EXIT;
+}
+
+static void
+ide_test_manager_provider_added (PeasExtensionSet *set,
+ PeasPluginInfo *plugin_info,
+ PeasExtension *exten,
+ gpointer user_data)
+{
+ IdeTestManager *self = user_data;
+ IdeTestProvider *provider = (IdeTestProvider *)exten;
+ TestsByProvider *tests;
+ guint len;
+
+ IDE_ENTRY;
+
+ g_assert (PEAS_IS_EXTENSION_SET (set));
+ g_assert (plugin_info != NULL);
+ g_assert (IDE_IS_TEST_PROVIDER (provider));
+ g_assert (G_IS_LIST_MODEL (provider));
+ g_assert (IDE_IS_TEST_MANAGER (self));
+
+ tests = g_slice_new0 (TestsByProvider);
+ tests->provider = g_object_ref (provider);
+ tests->tests = g_ptr_array_new_with_free_func (g_object_unref);
+ g_ptr_array_add (self->tests_by_provider, tests);
+
+ g_signal_connect_swapped (provider,
+ "items-changed",
+ G_CALLBACK (ide_test_manager_provider_items_changed),
+ self);
+
+ len = g_list_model_get_n_items (G_LIST_MODEL (provider));
+ ide_test_manager_provider_items_changed (self, 0, 0, len, provider);
+
+ IDE_EXIT;
+}
+
+static void
+ide_test_manager_provider_removed (PeasExtensionSet *set,
+ PeasPluginInfo *plugin_info,
+ PeasExtension *exten,
+ gpointer user_data)
+{
+ IdeTestManager *self = user_data;
+ IdeTestProvider *provider = (IdeTestProvider *)exten;
+
+ IDE_ENTRY;
+
+ g_assert (PEAS_IS_EXTENSION_SET (set));
+ g_assert (plugin_info != NULL);
+ g_assert (IDE_IS_TEST_PROVIDER (provider));
+ g_assert (IDE_IS_TEST_MANAGER (self));
+
+ for (guint i = 0; i < self->tests_by_provider->len; i++)
+ {
+ const TestsByProvider *info = g_ptr_array_index (self->tests_by_provider, i);
+
+ if (info->provider == provider)
+ {
+ g_ptr_array_remove_index (self->tests_by_provider, i);
+ break;
+ }
+ }
+
+ g_signal_handlers_disconnect_by_func (provider,
+ G_CALLBACK (ide_test_manager_provider_items_changed),
+ self);
+
+ IDE_EXIT;
+}
+
+static gboolean
+ide_test_manager_initiable_init (GInitable *initable,
+ GCancellable *cancellable,
+ GError **error)
+{
+ IdeTestManager *self = (IdeTestManager *)initable;
+ IdeContext *context;
+
+ IDE_ENTRY;
+
+ g_assert (IDE_IS_TEST_MANAGER (self));
+ g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ context = ide_object_get_context (IDE_OBJECT (self));
+
+ self->providers = peas_extension_set_new (peas_engine_get_default (),
+ IDE_TYPE_TEST_PROVIDER,
+ "context", context,
+ NULL);
+
+ g_signal_connect (self->providers,
+ "extension-added",
+ G_CALLBACK (ide_test_manager_provider_added),
+ self);
+
+ g_signal_connect (self->providers,
+ "extension-removed",
+ G_CALLBACK (ide_test_manager_provider_removed),
+ self);
+
+ peas_extension_set_foreach (self->providers,
+ ide_test_manager_provider_added,
+ self);
+
+ IDE_RETURN (TRUE);
+}
+
+static void
+initable_iface_init (GInitableIface *iface)
+{
+ iface->init = ide_test_manager_initiable_init;
+}
+
+/**
+ * ide_test_manager_run_all_async:
+ * @self: An #IdeTestManager
+ * @cancellable: (nullable): A #GCancellable or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: user data for @callback
+ *
+ * Executes all tests in an undefined order.
+ *
+ * Upon completion, @callback will be executed which must call
+ * ide_test_manager_run_all_finish() to get the result.
+ *
+ * Note that the individual test result information will be attached
+ * to the specific #IdeTest instances.
+ *
+ * Since: 3.28
+ */
+void
+ide_test_manager_run_all_async (IdeTestManager *self,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = NULL;
+
+ IDE_ENTRY;
+
+ g_return_if_fail (IDE_IS_TEST_MANAGER (self));
+ g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_priority (task, G_PRIORITY_LOW);
+ g_task_set_source_tag (task, ide_test_manager_run_all_async);
+
+ g_task_return_boolean (task, TRUE);
+
+ IDE_EXIT;
+}
+
+/**
+ * ide_test_manager_run_all_finish:
+ * @self: An #IdeTestManager
+ * @result: A #GAsyncResult
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to execute all unit tests.
+ *
+ * A return value of %TRUE does not indicate that all tests succeeded,
+ * only that all tests were executed. Individual test failures will be
+ * attached to the #IdeTest instances.
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.28
+ */
+gboolean
+ide_test_manager_run_all_finish (IdeTestManager *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ gboolean ret;
+
+ IDE_ENTRY;
+
+ g_return_val_if_fail (IDE_IS_TEST_MANAGER (self), FALSE);
+ g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+ ret = g_task_propagate_boolean (G_TASK (result), error);
+
+ IDE_RETURN (ret);
+}
+
+/**
+ * ide_test_manager_run_async:
+ * @self: An #IdeTestManager
+ * @test: An #IdeTest
+ * @cancellable: (nullable): A #GCancellable, or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: user data for @callback
+ *
+ * Executes a single unit test, asynchronously.
+ *
+ * The caller can access the result of the operation from @callback
+ * by calling ide_test_manager_run_finish() with the provided result.
+ *
+ * Since: 3.28
+ */
+void
+ide_test_manager_run_async (IdeTestManager *self,
+ IdeTest *test,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = NULL;
+
+ IDE_ENTRY;
+
+ g_return_if_fail (IDE_IS_TEST_MANAGER (self));
+ g_return_if_fail (IDE_IS_TEST (test));
+ g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_priority (task, G_PRIORITY_LOW);
+ g_task_set_source_tag (task, ide_test_manager_run_async);
+
+ g_task_return_boolean (task, TRUE);
+
+ IDE_EXIT;
+}
+
+/**
+ * ide_test_manager_run_finish:
+ * @self: An #IdeTestManager
+ * @result: The #GAsyncResult provided to callback
+ * @error: A location for a #GError, or %NULL
+ *
+ * Completes a request to ide_test_manager_run_finish().
+ *
+ * When this function returns %TRUE, it does not indicate that the test
+ * succeeded; only that the test was executed. Thest #IdeTest instance
+ * itself will contain information about the success of the test.
+ *
+ * Returns: %TRUE if the test was executed; otherwise %FALSE
+ * and @error is set.
+ *
+ * Since: 3.28
+ */
+gboolean
+ide_test_manager_run_finish (IdeTestManager *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ gboolean ret;
+
+ IDE_ENTRY;
+
+ g_return_val_if_fail (IDE_IS_TEST_MANAGER (self), FALSE);
+ g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+ ret = g_task_propagate_boolean (G_TASK (result), error);
+
+ IDE_RETURN (ret);
+}
+
+static void
+ide_test_manager_actions_run_all (IdeTestManager *self,
+ GVariant *param)
+{
+ g_assert (IDE_IS_TEST_MANAGER (self));
+
+ ide_test_manager_run_all_async (self, NULL, NULL, NULL);
+}
+
+GtkTreeModel *
+_ide_test_manager_get_model (IdeTestManager *self)
+{
+ g_return_val_if_fail (IDE_IS_TEST_MANAGER (self), NULL);
+
+ return GTK_TREE_MODEL (self->tests_store);
+}
diff --git a/src/libide/testing/ide-test-manager.h b/src/libide/testing/ide-test-manager.h
new file mode 100644
index 0000000..18f6ccd
--- /dev/null
+++ b/src/libide/testing/ide-test-manager.h
@@ -0,0 +1,45 @@
+/* ide-test-manager.h
+ *
+ * Copyright © 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ */
+
+#pragma once
+
+#include "ide-object.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TEST_MANAGER (ide_test_manager_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeTestManager, ide_test_manager, IDE, TEST_MANAGER, IdeObject)
+
+void ide_test_manager_run_async (IdeTestManager *self,
+ IdeTest *test,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gboolean ide_test_manager_run_finish (IdeTestManager *self,
+ GAsyncResult *result,
+ GError **error);
+void ide_test_manager_run_all_async (IdeTestManager *self,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gboolean ide_test_manager_run_all_finish (IdeTestManager *self,
+ GAsyncResult *result,
+ GError **error);
+
+G_END_DECLS
diff --git a/src/libide/testing/ide-test-panel.c b/src/libide/testing/ide-test-panel.c
new file mode 100644
index 0000000..bfd2b31
--- /dev/null
+++ b/src/libide/testing/ide-test-panel.c
@@ -0,0 +1,269 @@
+/* ide-test-panel.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-test-panel"
+
+#include "ide-debug.h"
+
+#include "testing/ide-test.h"
+#include "testing/ide-test-manager.h"
+#include "testing/ide-test-panel.h"
+#include "testing/ide-test-private.h"
+
+struct _IdeTestPanel
+{
+ GtkBin parent_instance;
+
+ /* Owned references */
+ IdeTestManager *manager;
+
+ /* Template references */
+ GtkScrolledWindow *scroller;
+ GtkTreeView *tree_view;
+};
+
+enum {
+ PROP_0,
+ PROP_MANAGER,
+ N_PROPS
+};
+
+G_DEFINE_TYPE (IdeTestPanel, ide_test_panel, GTK_TYPE_BIN)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_test_panel_row_activated (IdeTestPanel *self,
+ GtkTreePath *path,
+ GtkTreeViewColumn *column,
+ GtkTreeView *tree_view)
+{
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+
+ IDE_ENTRY;
+
+ g_assert (IDE_IS_TEST_PANEL (self));
+ g_assert (path != NULL);
+ g_assert (GTK_IS_TREE_VIEW_COLUMN (column));
+ g_assert (GTK_IS_TREE_VIEW (tree_view));
+
+ model = gtk_tree_view_get_model (tree_view);
+
+ if (gtk_tree_model_get_iter (model, &iter, path))
+ {
+ g_autoptr(IdeTest) test = NULL;
+
+ gtk_tree_model_get (model, &iter,
+ IDE_TEST_COLUMN_TEST, &test,
+ -1);
+
+ if (test != NULL)
+ {
+ IdeTestProvider *provider = _ide_test_get_provider (test);
+ IdeBuildPipeline *pipeline = NULL;
+
+ /* TODO: Everything... */
+
+ ide_test_provider_run_async (provider,
+ test,
+ pipeline,
+ NULL,
+ NULL,
+ NULL);
+ }
+ }
+
+ IDE_EXIT;
+}
+
+static void
+ide_test_panel_cell_data_func (GtkCellLayout *layout,
+ GtkCellRenderer *cell,
+ GtkTreeModel *model,
+ GtkTreeIter *iter,
+ gpointer user_data)
+{
+ g_autofree gchar *title = NULL;
+ g_autoptr(IdeTest) test = NULL;
+
+ g_assert (GTK_IS_TREE_VIEW_COLUMN (layout));
+ g_assert (GTK_IS_CELL_RENDERER_TEXT (cell));
+ g_assert (GTK_IS_TREE_MODEL (model));
+ g_assert (iter != NULL);
+ g_assert (IDE_IS_TEST_PANEL (user_data));
+
+ gtk_tree_model_get (model, iter,
+ IDE_TEST_COLUMN_GROUP, &title,
+ IDE_TEST_COLUMN_TEST, &test,
+ -1);
+
+ if (title)
+ g_object_set (cell, "text", title, NULL);
+ else if (test)
+ g_object_set (cell, "text", ide_test_get_display_name (test), NULL);
+ else
+ g_object_set (cell, "text", NULL, NULL);
+
+ /* TODO: extract test info/failures/etc */
+}
+
+static void
+ide_test_panel_row_inserted (IdeTestPanel *self,
+ GtkTreePath *path,
+ GtkTreeIter *iter,
+ GtkTreeModel *model)
+{
+ g_assert (IDE_IS_TEST_PANEL (self));
+ g_assert (path != NULL);
+ g_assert (iter != NULL);
+ g_assert (GTK_IS_TREE_MODEL (model));
+
+ if (self->tree_view != NULL)
+ gtk_tree_view_expand_to_path (self->tree_view, path);
+}
+
+static void
+ide_test_panel_constructed (GObject *object)
+{
+ IdeTestPanel *self = (IdeTestPanel *)object;
+
+ g_assert (IDE_IS_TEST_PANEL (self));
+
+ G_OBJECT_CLASS (ide_test_panel_parent_class)->constructed (object);
+
+ if (self->manager != NULL)
+ {
+ GtkTreeModel *model;
+
+ model = _ide_test_manager_get_model (self->manager);
+ gtk_tree_view_set_model (self->tree_view, model);
+ g_signal_connect_object (model,
+ "row-inserted",
+ G_CALLBACK (ide_test_panel_row_inserted),
+ self,
+ G_CONNECT_SWAPPED);
+ }
+}
+
+static void
+ide_test_panel_destroy (GtkWidget *widget)
+{
+ IdeTestPanel *self = (IdeTestPanel *)widget;
+
+ g_assert (IDE_IS_TEST_PANEL (self));
+
+ g_clear_object (&self->manager);
+
+ GTK_WIDGET_CLASS (ide_test_panel_parent_class)->destroy (widget);
+}
+
+static void
+ide_test_panel_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ IdeTestPanel *self = IDE_TEST_PANEL (object);
+
+ switch (prop_id)
+ {
+ case PROP_MANAGER:
+ g_value_set_object (value, self->manager);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_test_panel_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ IdeTestPanel *self = IDE_TEST_PANEL (object);
+
+ switch (prop_id)
+ {
+ case PROP_MANAGER:
+ self->manager = g_value_dup_object (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_test_panel_class_init (IdeTestPanelClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->constructed = ide_test_panel_constructed;
+ object_class->get_property = ide_test_panel_get_property;
+ object_class->set_property = ide_test_panel_set_property;
+
+ widget_class->destroy = ide_test_panel_destroy;
+
+ properties [PROP_MANAGER] =
+ g_param_spec_object ("manager",
+ "Manager",
+ "The test manager for the panel",
+ IDE_TYPE_TEST_MANAGER,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/builder/ui/ide-test-panel.ui");
+ gtk_widget_class_bind_template_child (widget_class, IdeTestPanel, scroller);
+ gtk_widget_class_bind_template_child (widget_class, IdeTestPanel, tree_view);
+ gtk_widget_class_bind_template_callback (widget_class, ide_test_panel_row_activated);
+}
+
+static void
+ide_test_panel_init (IdeTestPanel *self)
+{
+ GtkCellRenderer *cell;
+ GtkTreeViewColumn *column;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ column = g_object_new (GTK_TYPE_TREE_VIEW_COLUMN,
+ "visible", TRUE,
+ NULL);
+ cell = g_object_new (GTK_TYPE_CELL_RENDERER_PIXBUF,
+ "icon-name", "builder-unit-tests-symbolic",
+ "xpad", 3,
+ NULL);
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), cell, FALSE);
+ cell = g_object_new (GTK_TYPE_CELL_RENDERER_TEXT,
+ "ellipsize", PANGO_ELLIPSIZE_END,
+ "xalign", 0.0f,
+ "ypad", 6,
+ NULL);
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), cell, TRUE);
+ gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (column),
+ cell,
+ ide_test_panel_cell_data_func,
+ self,
+ NULL);
+ gtk_tree_view_append_column (self->tree_view, column);
+}
diff --git a/src/libide/testing/ide-test-panel.h b/src/libide/testing/ide-test-panel.h
new file mode 100644
index 0000000..92d8455
--- /dev/null
+++ b/src/libide/testing/ide-test-panel.h
@@ -0,0 +1,29 @@
+/* ide-test-panel.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TEST_PANEL (ide_test_panel_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeTestPanel, ide_test_panel, IDE, TEST_PANEL, GtkBin)
+
+G_END_DECLS
diff --git a/src/libide/testing/ide-test-panel.ui b/src/libide/testing/ide-test-panel.ui
new file mode 100644
index 0000000..8039ed1
--- /dev/null
+++ b/src/libide/testing/ide-test-panel.ui
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="IdeTestPanel" parent="GtkBin">
+ <child>
+ <object class="GtkScrolledWindow" id="scroller">
+ <property name="visible">true</property>
+ <child>
+ <object class="GtkTreeView" id="tree_view">
+ <property name="activate-on-single-click">true</property>
+ <property name="headers-visible">false</property>
+ <property name="visible">true</property>
+ <signal name="row-activated" handler="ide_test_panel_row_activated" swapped="true"
object="IdeTestPanel"/>
+ <style>
+ <class name="i-wanna-be-listbox"/>
+ </style>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection">
+ <property name="mode">none</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/libide/testing/ide-test-private.h b/src/libide/testing/ide-test-private.h
new file mode 100644
index 0000000..e99daa6
--- /dev/null
+++ b/src/libide/testing/ide-test-private.h
@@ -0,0 +1,41 @@
+/* ide-test-private.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "ide-test.h"
+#include "ide-test-manager.h"
+#include "ide-test-provider.h"
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+ IDE_TEST_COLUMN_GROUP,
+ IDE_TEST_COLUMN_TEST,
+} IdeTestColumn;
+
+G_GNUC_INTERNAL GtkTreeModel *_ide_test_manager_get_model (IdeTestManager *self);
+G_GNUC_INTERNAL void _ide_test_set_provider (IdeTest *self,
+ IdeTestProvider *provider);
+G_GNUC_INTERNAL IdeTestProvider *_ide_test_get_provider (IdeTest *self);
+
+
+G_END_DECLS
diff --git a/src/libide/testing/ide-test-provider.c b/src/libide/testing/ide-test-provider.c
new file mode 100644
index 0000000..8556715
--- /dev/null
+++ b/src/libide/testing/ide-test-provider.c
@@ -0,0 +1,233 @@
+/* ide-test-provider.c
+ *
+ * Copyright © 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-test-provider"
+
+#include "testing/ide-test-provider.h"
+#include "testing/ide-test-private.h"
+
+typedef struct
+{
+ GPtrArray *items;
+} IdeTestProviderPrivate;
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_ABSTRACT_TYPE_WITH_CODE (IdeTestProvider, ide_test_provider, IDE_TYPE_OBJECT,
+ G_ADD_PRIVATE (IdeTestProvider)
+ G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static void
+ide_test_provider_real_run_async (IdeTestProvider *self,
+ IdeTest *test,
+ IdeBuildPipeline *pipeline,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_task_report_new_error (self, callback, user_data,
+ ide_test_provider_real_run_async,
+ G_IO_ERROR,
+ G_IO_ERROR_NOT_SUPPORTED,
+ "%s is missing test runner implementation",
+ G_OBJECT_TYPE_NAME (self));
+}
+
+static gboolean
+ide_test_provider_real_run_finish (IdeTestProvider *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_test_provider_dispose (GObject *object)
+{
+ IdeTestProvider *self = (IdeTestProvider *)object;
+ IdeTestProviderPrivate *priv = ide_test_provider_get_instance_private (self);
+
+ if (priv->items != NULL && priv->items->len > 0)
+ {
+ guint len = priv->items->len;
+
+ g_ptr_array_remove_range (priv->items, 0, len);
+ g_list_model_items_changed (G_LIST_MODEL (self), 0, len, 0);
+ }
+
+ g_clear_pointer (&priv->items, g_ptr_array_unref);
+
+ G_OBJECT_CLASS (ide_test_provider_parent_class)->finalize (object);
+}
+
+static void
+ide_test_provider_class_init (IdeTestProviderClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = ide_test_provider_dispose;
+
+ klass->run_async = ide_test_provider_real_run_async;
+ klass->run_finish = ide_test_provider_real_run_finish;
+}
+
+static void
+ide_test_provider_init (IdeTestProvider *self)
+{
+ IdeTestProviderPrivate *priv = ide_test_provider_get_instance_private (self);
+
+ priv->items = g_ptr_array_new_with_free_func (g_object_unref);
+}
+
+static GType
+ide_test_provider_get_item_type (GListModel *model)
+{
+ return IDE_TYPE_TEST;
+}
+
+static guint
+ide_test_provider_get_n_items (GListModel *model)
+{
+ IdeTestProvider *self = (IdeTestProvider *)model;
+ IdeTestProviderPrivate *priv = ide_test_provider_get_instance_private (self);
+
+ g_assert (IDE_IS_TEST_PROVIDER (self));
+
+ return priv->items ? priv->items->len : 0;
+}
+
+static gpointer
+ide_test_provider_get_item (GListModel *model,
+ guint position)
+{
+ IdeTestProvider *self = (IdeTestProvider *)model;
+ IdeTestProviderPrivate *priv = ide_test_provider_get_instance_private (self);
+
+ g_assert (IDE_IS_TEST_PROVIDER (self));
+
+ if (priv->items != NULL)
+ {
+ if (position < priv->items->len)
+ return g_object_ref (g_ptr_array_index (priv->items, position));
+ }
+
+ return NULL;
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+ iface->get_item = ide_test_provider_get_item;
+ iface->get_n_items = ide_test_provider_get_n_items;
+ iface->get_item_type = ide_test_provider_get_item_type;
+}
+
+void
+ide_test_provider_add (IdeTestProvider *self,
+ IdeTest *test)
+{
+ IdeTestProviderPrivate *priv = ide_test_provider_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_TEST_PROVIDER (self));
+ g_return_if_fail (IDE_IS_TEST (test));
+
+ if (priv->items != NULL)
+ {
+ g_ptr_array_add (priv->items, g_object_ref (test));
+ _ide_test_set_provider (test, self);
+ g_list_model_items_changed (G_LIST_MODEL (self), priv->items->len - 1, 0, 1);
+ }
+}
+
+void
+ide_test_provider_remove (IdeTestProvider *self,
+ IdeTest *test)
+{
+ IdeTestProviderPrivate *priv = ide_test_provider_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_TEST_PROVIDER (self));
+ g_return_if_fail (IDE_IS_TEST (test));
+
+ if (priv->items != NULL)
+ {
+ for (guint i = 0; i < priv->items->len; i++)
+ {
+ IdeTest *element = g_ptr_array_index (priv->items, i);
+
+ if (element == test)
+ {
+ _ide_test_set_provider (test, NULL);
+ g_ptr_array_remove_index (priv->items, i);
+ g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0);
+ break;
+ }
+ }
+ }
+}
+
+void
+ide_test_provider_clear (IdeTestProvider *self)
+{
+ IdeTestProviderPrivate *priv = ide_test_provider_get_instance_private (self);
+ g_autoptr(GPtrArray) ar = NULL;
+
+ g_return_if_fail (IDE_IS_TEST_PROVIDER (self));
+
+ ar = priv->items;
+ priv->items = g_ptr_array_new_with_free_func (g_object_unref);
+
+ for (guint i = 0; i < ar->len; i++)
+ {
+ IdeTest *test = g_ptr_array_index (ar, i);
+ _ide_test_set_provider (test, NULL);
+ }
+
+ g_list_model_items_changed (G_LIST_MODEL (self), 0, ar->len, 0);
+}
+
+void
+ide_test_provider_run_async (IdeTestProvider *self,
+ IdeTest *test,
+ IdeBuildPipeline *pipeline,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_if_fail (IDE_IS_TEST_PROVIDER (self));
+ g_return_if_fail (IDE_IS_TEST (test));
+ g_return_if_fail (IDE_IS_BUILD_PIPELINE (pipeline));
+ g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ IDE_TEST_PROVIDER_GET_CLASS (self)->run_async (self,
+ test,
+ pipeline,
+ cancellable,
+ callback,
+ user_data);
+}
+
+gboolean
+ide_test_provider_run_finish (IdeTestProvider *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (IDE_IS_TEST_PROVIDER (self), FALSE);
+ g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+ return IDE_TEST_PROVIDER_GET_CLASS (self)->run_finish (self, result, error);
+}
diff --git a/src/libide/testing/ide-test-provider.h b/src/libide/testing/ide-test-provider.h
new file mode 100644
index 0000000..7ae4615
--- /dev/null
+++ b/src/libide/testing/ide-test-provider.h
@@ -0,0 +1,72 @@
+/* ide-test-provider.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ */
+
+#pragma once
+
+#include "ide-object.h"
+
+#include "buildsystem/ide-build-pipeline.h"
+#include "testing/ide-test.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TEST_PROVIDER (ide_test_provider_get_type ())
+
+G_DECLARE_DERIVABLE_TYPE (IdeTestProvider, ide_test_provider, IDE, TEST_PROVIDER, IdeObject)
+
+struct _IdeTestProviderClass
+{
+ IdeObjectClass parent_class;
+
+ void (*run_async) (IdeTestProvider *self,
+ IdeTest *test,
+ IdeBuildPipeline *pipeline,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+ gboolean (*run_finish) (IdeTestProvider *self,
+ GAsyncResult *result,
+ GError **error);
+
+ /*< private >*/
+ gpointer _reserved1;
+ gpointer _reserved2;
+ gpointer _reserved3;
+ gpointer _reserved4;
+ gpointer _reserved5;
+ gpointer _reserved6;
+ gpointer _reserved7;
+ gpointer _reserved8;
+};
+
+void ide_test_provider_clear (IdeTestProvider *self);
+void ide_test_provider_add (IdeTestProvider *self,
+ IdeTest *test);
+void ide_test_provider_remove (IdeTestProvider *self,
+ IdeTest *test);
+void ide_test_provider_run_async (IdeTestProvider *self,
+ IdeTest *test,
+ IdeBuildPipeline *pipeline,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gboolean ide_test_provider_run_finish (IdeTestProvider *self,
+ GAsyncResult *result,
+ GError **error);
+
+G_END_DECLS
diff --git a/src/libide/testing/ide-test.c b/src/libide/testing/ide-test.c
new file mode 100644
index 0000000..467199a
--- /dev/null
+++ b/src/libide/testing/ide-test.c
@@ -0,0 +1,338 @@
+/* ide-test.c
+ *
+ * Copyright © 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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 "testing/ide-test.h"
+#include "testing/ide-test-private.h"
+#include "testing/ide-test-provider.h"
+
+typedef struct
+{
+ IdeTestProvider *provider;
+ gchar *display_name;
+ gchar *group;
+ gchar *id;
+} IdeTestPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeTest, ide_test, G_TYPE_OBJECT)
+
+enum {
+ PROP_0,
+ PROP_DISPLAY_NAME,
+ PROP_GROUP,
+ PROP_ID,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+IdeTest *
+ide_test_new (void)
+{
+ return g_object_new (IDE_TYPE_TEST, NULL);
+}
+
+static void
+ide_test_finalize (GObject *object)
+{
+ IdeTest *self = (IdeTest *)object;
+ IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+ g_clear_pointer (&priv->group, g_free);
+ g_clear_pointer (&priv->id, g_free);
+ g_clear_pointer (&priv->display_name, g_free);
+
+ G_OBJECT_CLASS (ide_test_parent_class)->finalize (object);
+}
+
+static void
+ide_test_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ IdeTest *self = IDE_TEST (object);
+
+ switch (prop_id)
+ {
+ case PROP_ID:
+ g_value_set_string (value, ide_test_get_id (self));
+ break;
+
+ case PROP_GROUP:
+ g_value_set_string (value, ide_test_get_group (self));
+ break;
+
+ case PROP_DISPLAY_NAME:
+ g_value_set_string (value, ide_test_get_display_name (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_test_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ IdeTest *self = IDE_TEST (object);
+
+ switch (prop_id)
+ {
+ case PROP_GROUP:
+ ide_test_set_group (self, g_value_get_string (value));
+ break;
+
+ case PROP_ID:
+ ide_test_set_id (self, g_value_get_string (value));
+ break;
+
+ case PROP_DISPLAY_NAME:
+ ide_test_set_display_name (self, g_value_get_string (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_test_class_init (IdeTestClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = ide_test_finalize;
+ object_class->get_property = ide_test_get_property;
+ object_class->set_property = ide_test_set_property;
+
+ /**
+ * IdeTest:display_name:
+ *
+ * The "display-name" property contains the display name of the test as
+ * the user is expected to read in UI elements.
+ *
+ * Since: 3.28
+ */
+ properties [PROP_DISPLAY_NAME] =
+ g_param_spec_string ("display-name",
+ "Name",
+ "The display_name of the unit test",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeTest:id:
+ *
+ * The "id" property contains the unique identifier of the test.
+ *
+ * Since: 3.28
+ */
+ properties [PROP_ID] =
+ g_param_spec_string ("id",
+ "Id",
+ "The unique identifier of the test",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeTest:group:
+ *
+ * The "group" property contains the name of the gruop the test belongs
+ * to, if any.
+ *
+ * Since: 3.28
+ */
+ properties [PROP_GROUP] =
+ g_param_spec_string ("group",
+ "Group",
+ "The name of the group the test belongs to, if any",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_test_init (IdeTest *self)
+{
+}
+
+IdeTestProvider *
+_ide_test_get_provider (IdeTest *self)
+{
+ IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+ g_return_val_if_fail (IDE_IS_TEST (self), NULL);
+
+ return priv->provider;
+}
+
+void
+_ide_test_set_provider (IdeTest *self,
+ IdeTestProvider *provider)
+{
+ IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_TEST (self));
+ g_return_if_fail (!provider || IDE_IS_TEST_PROVIDER (provider));
+
+ priv->provider = provider;
+}
+
+/**
+ * ide_test_get_display_name:
+ * @self: An #IdeTest
+ *
+ * Gets the "display-name" property of the test.
+ *
+ * Returns: (nullable): The display_name of the test or %NULL
+ *
+ * Since: 3.28
+ */
+const gchar *
+ide_test_get_display_name (IdeTest *self)
+{
+ IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+ g_return_val_if_fail (IDE_IS_TEST (self), NULL);
+
+ return priv->display_name;
+}
+
+/**
+ * ide_test_set_display_name:
+ * @self: An #IdeTest
+ * @display_name: (nullable): The display_name of the test, or %NULL to unset
+ *
+ * Sets the "display-name" property of the unit test.
+ *
+ * Since: 3.28
+ */
+void
+ide_test_set_display_name (IdeTest *self,
+ const gchar *display_name)
+{
+ IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_TEST (self));
+
+ if (g_strcmp0 (display_name, priv->display_name) != 0)
+ {
+ g_free (priv->display_name);
+ priv->display_name = g_strdup (display_name);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DISPLAY_NAME]);
+ }
+}
+
+/**
+ * ide_test_get_group:
+ * @self: a #IdeTest
+ *
+ * Gets the "group" property.
+ *
+ * The group name is used to group tests together.
+ *
+ * Returns: (nullable): The group name or %NULL.
+ *
+ * Since: 3.28
+ */
+const gchar *
+ide_test_get_group (IdeTest *self)
+{
+ IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+ g_return_val_if_fail (IDE_IS_TEST (self), NULL);
+
+ return priv->group;
+}
+
+/**
+ * ide_test_set_group:
+ * @self: a #IdeTest
+ * @group: (nullable): the name of the group or %NULL
+ *
+ * Sets the #IdeTest:group property.
+ *
+ * The group property is used to group related tests together.
+ *
+ * Since: 3.28
+ */
+void
+ide_test_set_group (IdeTest *self,
+ const gchar *group)
+{
+ IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_TEST (self));
+
+ if (g_strcmp0 (group, priv->group) != 0)
+ {
+ g_free (priv->group);
+ priv->group = g_strdup (group);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_GROUP]);
+ }
+}
+
+/**
+ * ide_test_get_id:
+ * @self: a #IdeTest
+ *
+ * Gets the #IdeTest:id property.
+ *
+ * Returns: (nullable): The id of the test, or %NULL if it has not been set.
+ *
+ * Since: 3.28
+ */
+const gchar *
+ide_test_get_id (IdeTest *self)
+{
+ IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+ g_return_val_if_fail (IDE_IS_TEST (self), NULL);
+
+ return priv->id;
+}
+
+/**
+ * ide_test_set_id:
+ * @self: a #IdeTest
+ * @id: (nullable): the id of the test or %NULL
+ *
+ * Sets the #IdeTest:id property.
+ *
+ * The id property is used to uniquely identify the test.
+ *
+ * Since: 3.28
+ */
+void
+ide_test_set_id (IdeTest *self,
+ const gchar *id)
+{
+ IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_TEST (self));
+
+ if (g_strcmp0 (id, priv->id) != 0)
+ {
+ g_free (priv->id);
+ priv->id = g_strdup (id);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ID]);
+ }
+}
diff --git a/src/libide/testing/ide-test.h b/src/libide/testing/ide-test.h
new file mode 100644
index 0000000..6e1702b
--- /dev/null
+++ b/src/libide/testing/ide-test.h
@@ -0,0 +1,55 @@
+/* ide-test.h
+ *
+ * Copyright © 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TEST (ide_test_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (IdeTest, ide_test, IDE, TEST, GObject)
+
+struct _IdeTestClass
+{
+ GObjectClass parent;
+
+ /*< private >*/
+ gpointer _reserved1;
+ gpointer _reserved2;
+ gpointer _reserved3;
+ gpointer _reserved4;
+ gpointer _reserved5;
+ gpointer _reserved6;
+ gpointer _reserved7;
+ gpointer _reserved8;
+};
+
+IdeTest *ide_test_new (void);
+const gchar *ide_test_get_display_name (IdeTest *self);
+void ide_test_set_display_name (IdeTest *self,
+ const gchar *display_name);
+const gchar *ide_test_get_group (IdeTest *self);
+void ide_test_set_group (IdeTest *self,
+ const gchar *group);
+const gchar *ide_test_get_id (IdeTest *self);
+void ide_test_set_id (IdeTest *self,
+ const gchar *id);
+
+G_END_DECLS
diff --git a/src/libide/testing/meson.build b/src/libide/testing/meson.build
new file mode 100644
index 0000000..640ebe5
--- /dev/null
+++ b/src/libide/testing/meson.build
@@ -0,0 +1,23 @@
+testing_headers = [
+ 'ide-test.h',
+ 'ide-test-manager.h',
+ 'ide-test-provider.h',
+]
+
+testing_sources = [
+ 'ide-test.c',
+ 'ide-test-manager.c',
+ 'ide-test-provider.c',
+]
+
+testing_private_sources = [
+ 'ide-test-panel.c',
+ 'ide-test-editor-addin.c',
+ 'testing-plugin.c',
+]
+
+libide_public_headers += files(testing_headers)
+libide_public_sources += files(testing_sources)
+libide_private_sources += files(testing_private_sources)
+
+install_headers(testing_headers, subdir: join_paths(libide_header_subdir, 'testing'))
diff --git a/src/libide/testing/testing-plugin.c b/src/libide/testing/testing-plugin.c
new file mode 100644
index 0000000..6db5872
--- /dev/null
+++ b/src/libide/testing/testing-plugin.c
@@ -0,0 +1,28 @@
+/* testing-plugin.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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 <libpeas/peas.h>
+
+#include "editor/ide-editor-addin.h"
+#include "testing/ide-test-editor-addin.h"
+
+void
+ide_test_register_types (PeasObjectModule *module)
+{
+ peas_object_module_register_extension_type (module, IDE_TYPE_EDITOR_ADDIN, IDE_TYPE_TEST_EDITOR_ADDIN);
+}
diff --git a/src/libide/testing/testing.plugin b/src/libide/testing/testing.plugin
new file mode 100644
index 0000000..89802f2
--- /dev/null
+++ b/src/libide/testing/testing.plugin
@@ -0,0 +1,9 @@
+[Plugin]
+Module=testing
+Name=Testing
+Description=Unit testing for Builder
+Authors=Christian Hergert <christian hergert me>
+Copyright=Copyright © 2017 Christian Hergert
+Builtin=true
+Hidden=true
+Embedded=ide_test_register_types
diff --git a/src/plugins/meson/gbp-meson-test-provider.c b/src/plugins/meson/gbp-meson-test-provider.c
new file mode 100644
index 0000000..b2d6411
--- /dev/null
+++ b/src/plugins/meson/gbp-meson-test-provider.c
@@ -0,0 +1,348 @@
+/* gbp-meson-test-provider.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ */
+
+#define G_LOG_DOMAIN "gbp-meson-test-provider"
+
+#include <json-glib/json-glib.h>
+
+#include "gbp-meson-test.h"
+#include "gbp-meson-test-provider.h"
+
+struct _GbpMesonTestProvider
+{
+ IdeTestProvider parent_instance;
+ GCancellable *build_cancellable;
+ guint reload_source;
+};
+
+G_DEFINE_TYPE (GbpMesonTestProvider, gbp_meson_test_provider, IDE_TYPE_TEST_PROVIDER)
+
+static void
+gbp_meson_test_provider_load_json (GbpMesonTestProvider *self,
+ JsonNode *root)
+{
+ JsonArray *array;
+ guint length;
+
+ g_assert (GBP_IS_MESON_TEST_PROVIDER (self));
+ g_assert (root != NULL);
+
+ if (!JSON_NODE_HOLDS_ARRAY (root) || !(array = json_node_get_array (root)))
+ return;
+
+ ide_test_provider_clear (IDE_TEST_PROVIDER (self));
+
+ length = json_array_get_length (array);
+
+ for (guint i = 0; i < length; i++)
+ {
+ g_autoptr(GPtrArray) cmd = g_ptr_array_new_with_free_func (g_free);
+ g_autoptr(IdeEnvironment) env = ide_environment_new ();
+ g_autoptr(IdeTest) test = NULL;
+ const gchar *name;
+ const gchar *group = NULL;
+ JsonObject *obj;
+ JsonArray *sub_array;
+ JsonNode *sub_element;
+ JsonNode *element;
+ JsonNode *member;
+ guint timeout = 0;
+
+ if (NULL == (element = json_array_get_element (array, i)) ||
+ !JSON_NODE_HOLDS_OBJECT (element) ||
+ NULL == (obj = json_node_get_object (element)) ||
+ NULL == (member = json_object_get_member (obj, "name")) ||
+ !JSON_NODE_HOLDS_VALUE (member) ||
+ NULL == (name = json_node_get_string (member)))
+ continue;
+
+ if (NULL != (member = json_object_get_member (obj, "timeout")) &&
+ JSON_NODE_HOLDS_VALUE (member))
+ timeout = json_node_get_int (member);
+
+ if (NULL != (member = json_object_get_member (obj, "suite")) &&
+ JSON_NODE_HOLDS_ARRAY (member) &&
+ NULL != (sub_array = json_node_get_array (member)) &&
+ json_array_get_length (sub_array) > 0 &&
+ NULL != (sub_element = json_array_get_element (sub_array, 0)) &&
+ JSON_NODE_HOLDS_VALUE (sub_element))
+ group = json_node_get_string (sub_element);
+
+ if (NULL != (member = json_object_get_member (obj, "cmd")) &&
+ JSON_NODE_HOLDS_ARRAY (member) &&
+ NULL != (sub_array = json_node_get_array (member)))
+ {
+ guint cmdlen = json_array_get_length (sub_array);
+
+ for (guint j = 0; j < cmdlen; j++)
+ {
+ sub_element = json_array_get_element (sub_array, j);
+ if (JSON_NODE_HOLDS_VALUE (sub_element))
+ {
+ const gchar *str = json_node_get_string (sub_element);
+
+ if (str)
+ g_ptr_array_add (cmd, g_strdup (str));
+ }
+ }
+ }
+
+ if (NULL != (member = json_object_get_member (obj, "env")) &&
+ JSON_NODE_HOLDS_OBJECT (member) &&
+ NULL != (obj = json_node_get_object (member)))
+ {
+ JsonObjectIter iter;
+ const gchar *key;
+ JsonNode *value;
+
+ json_object_iter_init (&iter, obj);
+
+ while (json_object_iter_next (&iter, &key, &value))
+ {
+ if (JSON_NODE_HOLDS_VALUE (value))
+ ide_environment_setenv (env, key, json_node_get_string (value));
+ }
+ }
+
+ g_ptr_array_add (cmd, NULL);
+
+ test = g_object_new (GBP_TYPE_MESON_TEST,
+ "command", (gchar **)cmd->pdata,
+ "display-name", name,
+ "env", env,
+ "group", group,
+ "id", name,
+ "timeout", timeout,
+ NULL);
+
+ ide_test_provider_add (IDE_TEST_PROVIDER (self), test);
+ }
+}
+
+static void
+gbp_meson_test_provider_communicate_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ IdeSubprocess *subprocess = (IdeSubprocess *)object;
+ g_autoptr(GbpMesonTestProvider) self = user_data;
+ g_autoptr(JsonParser) parser = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autofree gchar *stdout_buf = NULL;
+ JsonNode *root;
+
+ g_assert (IDE_IS_SUBPROCESS (subprocess));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (GBP_IS_MESON_TEST_PROVIDER (self));
+
+ if (!ide_subprocess_communicate_utf8_finish (subprocess, result, &stdout_buf, NULL, &error))
+ IDE_GOTO (failure);
+
+ parser = json_parser_new ();
+
+ if (!json_parser_load_from_data (parser, stdout_buf, -1, &error) ||
+ NULL == (root = json_parser_get_root (parser)))
+ IDE_GOTO (failure);
+
+ gbp_meson_test_provider_load_json (self, root);
+
+failure:
+ if (error != NULL)
+ g_message ("%s", error->message);
+}
+
+static void
+gbp_meson_test_provider_do_reload (GbpMesonTestProvider *self,
+ IdeBuildPipeline *pipeline)
+{
+ g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+ g_autoptr(IdeSubprocess) subprocess = NULL;
+ g_autoptr(GError) error = NULL;
+ const gchar *builddir;
+
+ IDE_ENTRY;
+
+ g_assert (GBP_IS_MESON_TEST_PROVIDER (self));
+ g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+ if (NULL == (launcher = ide_build_pipeline_create_launcher (pipeline, &error)))
+ IDE_GOTO (failure);
+
+ ide_subprocess_launcher_set_flags (launcher, G_SUBPROCESS_FLAGS_STDOUT_PIPE);
+
+ builddir = ide_build_pipeline_get_builddir (pipeline);
+ ide_subprocess_launcher_set_cwd (launcher, builddir);
+
+ ide_subprocess_launcher_push_argv (launcher, "meson");
+ ide_subprocess_launcher_push_argv (launcher, "introspect");
+ ide_subprocess_launcher_push_argv (launcher, "--tests");
+
+ if (NULL == (subprocess = ide_subprocess_launcher_spawn (launcher, NULL, &error)))
+ IDE_GOTO (failure);
+
+ ide_subprocess_communicate_utf8_async (subprocess,
+ NULL,
+ NULL,
+ gbp_meson_test_provider_communicate_cb,
+ g_object_ref (self));
+
+failure:
+ if (error != NULL)
+ g_message ("%s", error->message);
+
+ IDE_EXIT;
+}
+
+static void
+gbp_meson_test_provider_build_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ IdeBuildPipeline *pipeline = (IdeBuildPipeline *)object;
+ g_autoptr(GbpMesonTestProvider) self = user_data;
+ g_autoptr(GError) error = NULL;
+
+ IDE_ENTRY;
+
+ g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (GBP_IS_MESON_TEST_PROVIDER (self));
+
+ if (!ide_build_pipeline_build_finish (pipeline, result, &error))
+ g_message ("%s", error->message);
+ else
+ gbp_meson_test_provider_do_reload (self, pipeline);
+
+ IDE_EXIT;
+}
+
+static gboolean
+gbp_meson_test_provider_reload (gpointer user_data)
+{
+ GbpMesonTestProvider *self = user_data;
+ IdeBuildPipeline *pipeline;
+ IdeBuildManager *build_manager;
+ IdeContext *context;
+
+ IDE_ENTRY;
+
+ g_assert (GBP_IS_MESON_TEST_PROVIDER (self));
+
+ self->reload_source = 0;
+
+ /* Cancel any other builds in-flight */
+ g_cancellable_cancel (self->build_cancellable);
+ g_clear_object (&self->build_cancellable);
+
+ /*
+ * Get access to the pipeline so we can create a launcher to
+ * introspect meson from within the build environment.
+ */
+ context = ide_object_get_context (IDE_OBJECT (self));
+ build_manager = ide_context_get_build_manager (context);
+ pipeline = ide_build_manager_get_pipeline (build_manager);
+ if (pipeline == NULL)
+ IDE_RETURN (G_SOURCE_REMOVE);
+
+ /*
+ * Make sure that the build pipeline has advanced enough for
+ * us to continue processing the tests.
+ */
+ self->build_cancellable = g_cancellable_new ();
+ /*
+ * TODO: We want to try to avoid the pipeline build like this in
+ * the future because it advances the pipeline when the project
+ * is opened. Instead, we might want a CONFIGURE stage that auto
+ * generates the info, and then watch that GFile.
+ *
+ * But to do that well, we need to coordinate with the panel to
+ * be lazy about fetching unit tests until the panel is displayed.
+ */
+ ide_build_pipeline_build_async (pipeline,
+ IDE_BUILD_PHASE_CONFIGURE,
+ self->build_cancellable,
+ gbp_meson_test_provider_build_cb,
+ g_object_ref (self));
+
+ IDE_RETURN (G_SOURCE_REMOVE);
+}
+
+static void
+gbp_meson_test_provider_notify_pipeline (GbpMesonTestProvider *self,
+ GParamSpec *pspec,
+ IdeBuildManager *build_manager)
+{
+ g_assert (GBP_IS_MESON_TEST_PROVIDER (self));
+ g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+
+ ide_clear_source (&self->reload_source);
+ self->reload_source = gdk_threads_add_timeout_full (G_PRIORITY_LOW,
+ 2000,
+ gbp_meson_test_provider_reload,
+ self,
+ NULL);
+}
+
+static void
+gbp_meson_test_provider_constructed (GObject *object)
+{
+ GbpMesonTestProvider *self = (GbpMesonTestProvider *)object;
+ IdeBuildManager *build_manager;
+ IdeContext *context;
+
+ g_assert (GBP_IS_MESON_TEST_PROVIDER (self));
+
+ G_OBJECT_CLASS (gbp_meson_test_provider_parent_class)->constructed (object);
+
+ context = ide_object_get_context (IDE_OBJECT (self));
+ build_manager = ide_context_get_build_manager (context);
+
+ g_signal_connect_object (build_manager,
+ "notify::pipeline",
+ G_CALLBACK (gbp_meson_test_provider_notify_pipeline),
+ self,
+ G_CONNECT_SWAPPED);
+
+ gbp_meson_test_provider_notify_pipeline (self, NULL, build_manager);
+}
+
+static void
+gbp_meson_test_provider_dispose (GObject *object)
+{
+ GbpMesonTestProvider *self = (GbpMesonTestProvider *)object;
+
+ ide_clear_source (&self->reload_source);
+ g_cancellable_cancel (self->build_cancellable);
+ g_clear_object (&self->build_cancellable);
+
+ G_OBJECT_CLASS (gbp_meson_test_provider_parent_class)->dispose (object);
+}
+
+static void
+gbp_meson_test_provider_class_init (GbpMesonTestProviderClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->constructed = gbp_meson_test_provider_constructed;
+ object_class->dispose = gbp_meson_test_provider_dispose;
+}
+
+static void
+gbp_meson_test_provider_init (GbpMesonTestProvider *self)
+{
+}
diff --git a/src/plugins/meson/gbp-meson-test-provider.h b/src/plugins/meson/gbp-meson-test-provider.h
new file mode 100644
index 0000000..40e111a
--- /dev/null
+++ b/src/plugins/meson/gbp-meson-test-provider.h
@@ -0,0 +1,29 @@
+/* gbp-meson-test-provider.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ */
+
+#pragma once
+
+#include <ide.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_MESON_TEST_PROVIDER (gbp_meson_test_provider_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpMesonTestProvider, gbp_meson_test_provider, GBP, MESON_TEST_PROVIDER,
IdeTestProvider)
+
+G_END_DECLS
diff --git a/src/plugins/meson/gbp-meson-test.c b/src/plugins/meson/gbp-meson-test.c
new file mode 100644
index 0000000..fe00e4f
--- /dev/null
+++ b/src/plugins/meson/gbp-meson-test.c
@@ -0,0 +1,196 @@
+/* gbp-meson-test.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ */
+
+#define G_LOG_DOMAIN "gbp-meson-test"
+
+#include "gbp-meson-test.h"
+
+struct _GbpMesonTest
+{
+ IdeTest parent_instance;
+ IdeEnvironment *env;
+ gchar **command;
+ GFile *workdir;
+ guint timeout;
+};
+
+enum {
+ PROP_0,
+ PROP_COMMAND,
+ PROP_ENV,
+ PROP_TIMEOUT,
+ PROP_WORKDIR,
+ N_PROPS
+};
+
+G_DEFINE_TYPE (GbpMesonTest, gbp_meson_test, IDE_TYPE_TEST)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+gbp_meson_test_finalize (GObject *object)
+{
+ GbpMesonTest *self = (GbpMesonTest *)object;
+
+ g_clear_pointer (&self->command, g_strfreev);
+ g_clear_object (&self->env);
+ g_clear_object (&self->workdir);
+
+ G_OBJECT_CLASS (gbp_meson_test_parent_class)->finalize (object);
+}
+
+static void
+gbp_meson_test_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GbpMesonTest *self = GBP_MESON_TEST (object);
+
+ switch (prop_id)
+ {
+ case PROP_COMMAND:
+ g_value_set_boxed (value, self->command);
+ break;
+
+ case PROP_ENV:
+ g_value_set_object (value, self->env);
+ break;
+
+ case PROP_TIMEOUT:
+ g_value_set_uint (value, self->timeout);
+ break;
+
+ case PROP_WORKDIR:
+ g_value_set_object (value, self->workdir);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gbp_meson_test_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GbpMesonTest *self = GBP_MESON_TEST (object);
+
+ switch (prop_id)
+ {
+ case PROP_COMMAND:
+ self->command = g_value_dup_boxed (value);
+ break;
+
+ case PROP_ENV:
+ self->env = g_value_dup_object (value);
+ break;
+
+ case PROP_TIMEOUT:
+ self->timeout = g_value_get_uint (value);
+ break;
+
+ case PROP_WORKDIR:
+ self->workdir = g_value_dup_object (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gbp_meson_test_class_init (GbpMesonTestClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = gbp_meson_test_finalize;
+ object_class->get_property = gbp_meson_test_get_property;
+ object_class->set_property = gbp_meson_test_set_property;
+
+ properties [PROP_COMMAND] =
+ g_param_spec_boxed ("command",
+ "Command",
+ "The command to execute for the test",
+ G_TYPE_STRV,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_ENV] =
+ g_param_spec_object ("env",
+ "Environment",
+ "The environment for the test",
+ IDE_TYPE_ENVIRONMENT,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_TIMEOUT] =
+ g_param_spec_uint ("timeout",
+ "Timeout",
+ "The timeout in seconds, or 0 for none",
+ 0,
+ G_MAXUINT,
+ 0,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_WORKDIR] =
+ g_param_spec_object ("workdir",
+ "Workdir",
+ "The working directory to run the test",
+ G_TYPE_FILE,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gbp_meson_test_init (GbpMesonTest *self)
+{
+}
+
+const gchar * const *
+gbp_meson_test_get_command (GbpMesonTest *self)
+{
+ g_return_val_if_fail (GBP_IS_MESON_TEST (self), NULL);
+
+ return (const gchar * const *)self->command;
+}
+
+GFile *
+gbp_meson_test_get_workdir (GbpMesonTest *self)
+{
+ g_return_val_if_fail (GBP_IS_MESON_TEST (self), NULL);
+
+ return self->workdir;
+}
+
+guint
+gbp_meson_test_get_timeout (GbpMesonTest *self)
+{
+ g_return_val_if_fail (GBP_IS_MESON_TEST (self), 0);
+
+ return self->timeout;
+}
+
+IdeEnvironment *
+gbp_meson_test_get_env (GbpMesonTest *self)
+{
+ g_return_val_if_fail (GBP_IS_MESON_TEST (self), NULL);
+
+ return self->env;
+}
diff --git a/src/plugins/meson/gbp-meson-test.h b/src/plugins/meson/gbp-meson-test.h
new file mode 100644
index 0000000..f0308b6
--- /dev/null
+++ b/src/plugins/meson/gbp-meson-test.h
@@ -0,0 +1,34 @@
+/* gbp-meson-test.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ */
+
+#pragma once
+
+#include <ide.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_MESON_TEST (gbp_meson_test_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpMesonTest, gbp_meson_test, GBP, MESON_TEST, IdeTest)
+
+const gchar * const *gbp_meson_test_get_command (GbpMesonTest *self);
+GFile *gbp_meson_test_get_workdir (GbpMesonTest *self);
+guint gbp_meson_test_get_timeout (GbpMesonTest *self);
+IdeEnvironment *gbp_meson_test_get_env (GbpMesonTest *self);
+
+G_END_DECLS
diff --git a/src/plugins/meson/meson-plugin.c b/src/plugins/meson/meson-plugin.c
index 36bd525..7090063 100644
--- a/src/plugins/meson/meson-plugin.c
+++ b/src/plugins/meson/meson-plugin.c
@@ -21,10 +21,12 @@
#include "gbp-meson-build-system.h"
#include "gbp-meson-pipeline-addin.h"
+#include "gbp-meson-test-provider.h"
void
gbp_meson_register_types (PeasObjectModule *module)
{
peas_object_module_register_extension_type (module, IDE_TYPE_BUILD_PIPELINE_ADDIN,
GBP_TYPE_MESON_PIPELINE_ADDIN);
peas_object_module_register_extension_type (module, IDE_TYPE_BUILD_SYSTEM, GBP_TYPE_MESON_BUILD_SYSTEM);
+ peas_object_module_register_extension_type (module, IDE_TYPE_TEST_PROVIDER, GBP_TYPE_MESON_TEST_PROVIDER);
}
diff --git a/src/plugins/meson/meson.build b/src/plugins/meson/meson.build
index 763ab8e..44c3b92 100644
--- a/src/plugins/meson/meson.build
+++ b/src/plugins/meson/meson.build
@@ -14,6 +14,10 @@ meson_sources = [
'gbp-meson-build-target.h',
'gbp-meson-pipeline-addin.c',
'gbp-meson-pipeline-addin.h',
+ 'gbp-meson-test-provider.c',
+ 'gbp-meson-test-provider.h',
+ 'gbp-meson-test.c',
+ 'gbp-meson-test.h',
]
gnome_builder_plugins_sources += files(meson_sources)
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]