[gnome-builder/wip/chergert/unittests] testing: start on unit testing



commit 9653c6bb2a290caf33ef520bfad74de522186959
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 |   97 +++++
 src/libide/testing/ide-test-editor-addin.h |   29 ++
 src/libide/testing/ide-test-manager.c      |  573 ++++++++++++++++++++++++++++
 src/libide/testing/ide-test-manager.h      |   45 +++
 src/libide/testing/ide-test-panel.c        |  143 +++++++
 src/libide/testing/ide-test-panel.h        |   29 ++
 src/libide/testing/ide-test-panel.ui       |   15 +
 src/libide/testing/ide-test-private.h      |   29 ++
 src/libide/testing/ide-test-provider.c     |  147 +++++++
 src/libide/testing/ide-test-provider.h     |   51 +++
 src/libide/testing/ide-test.c              |  313 +++++++++++++++
 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 +
 21 files changed, 1648 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..c03c214
--- /dev/null
+++ b/src/libide/testing/ide-test-editor-addin.c
@@ -0,0 +1,97 @@
+/* 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 "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"
+
+struct _IdeTestEditorAddin
+{
+  GObject       parent_instance;
+
+  IdeTestPanel *panel;
+};
+
+static void
+ide_test_editor_addin_load (IdeEditorAddin       *addin,
+                            IdeEditorPerspective *editor)
+{
+  IdeTestEditorAddin *self = (IdeTestEditorAddin *)addin;
+  IdeEditorSidebar *sidebar;
+
+  g_assert (IDE_IS_TEST_EDITOR_ADDIN (self));
+  g_assert (IDE_IS_EDITOR_PERSPECTIVE (editor));
+
+  self->panel = g_object_new (IDE_TYPE_TEST_PANEL,
+                              "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"),
+                                  "applications-science-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..17eeefd
--- /dev/null
+++ b/src/libide/testing/ide-test-manager.c
@@ -0,0 +1,573 @@
+/* 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-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
+};
+
+enum {
+  COLUMN_GROUP,
+  COLUMN_TEST,
+};
+
+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,
+                              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,
+                      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,
+                      COLUMN_GROUP, NULL,
+                      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,
+                              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..12ccac7
--- /dev/null
+++ b/src/libide/testing/ide-test-panel.c
@@ -0,0 +1,143 @@
+/* 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 "testing/ide-test-manager.h"
+#include "testing/ide-test-panel.h"
+#include "testing/ide-test-private.h"
+
+struct _IdeTestPanel
+{
+  GtkBin             parent_instance;
+
+  IdeTestManager    *manager;
+
+  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_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);
+    }
+}
+
+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);
+}
+
+static void
+ide_test_panel_init (IdeTestPanel *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
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..297e08f
--- /dev/null
+++ b/src/libide/testing/ide-test-panel.ui
@@ -0,0 +1,15 @@
+<?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="visible">true</property>
+          </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..ca200ab
--- /dev/null
+++ b/src/libide/testing/ide-test-private.h
@@ -0,0 +1,29 @@
+/* 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-manager.h"
+
+G_BEGIN_DECLS
+
+GtkTreeModel *_ide_test_manager_get_model (IdeTestManager *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..b77bb09
--- /dev/null
+++ b/src/libide/testing/ide-test-provider.c
@@ -0,0 +1,147 @@
+/* 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"
+
+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_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;
+}
+
+static void
+ide_test_provider_init (IdeTestProvider *self)
+{
+}
+
+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));
+      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)
+            {
+              g_ptr_array_remove_index (priv->items, i);
+              g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0);
+              break;
+            }
+        }
+    }
+}
diff --git a/src/libide/testing/ide-test-provider.h b/src/libide/testing/ide-test-provider.h
new file mode 100644
index 0000000..cc3e5fd
--- /dev/null
+++ b/src/libide/testing/ide-test-provider.h
@@ -0,0 +1,51 @@
+/* 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 "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;
+
+  /*< private >*/
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+  gpointer _reserved5;
+  gpointer _reserved6;
+  gpointer _reserved7;
+  gpointer _reserved8;
+};
+
+void ide_test_provider_add    (IdeTestProvider *self,
+                               IdeTest         *test);
+void ide_test_provider_remove (IdeTestProvider *self,
+                               IdeTest         *test);
+
+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..2ac6e70
--- /dev/null
+++ b/src/libide/testing/ide-test.c
@@ -0,0 +1,313 @@
+/* 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 "ide-test.h"
+
+typedef struct
+{
+  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)
+{
+}
+
+/**
+ * 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 (priv->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 (priv->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 (priv->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


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