Skip to content

Instantly share code, notes, and snippets.

@codebrainz
Last active December 25, 2015 22:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save codebrainz/7047610 to your computer and use it in GitHub Desktop.
Save codebrainz/7047610 to your computer and use it in GitHub Desktop.
Add test harness for Geany's core code
diff --git a/.gitignore b/.gitignore
index 51cdd7b..678a68f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -103,3 +103,5 @@ Makefile.in
#-----------------------------------------------------------------------
/tests/**/*.trs
/tests/**/*.log
+/tests/*.log
+/src/test*.log
diff --git a/HACKING b/HACKING
index 7cfef4c..32a10c7 100644
--- a/HACKING
+++ b/HACKING
@@ -621,3 +621,123 @@ Whilst it is possible for the plugin prefix to be different to
the prefix of the libdir (which is why there are two settings),
it is probably better to keep the version of Geany and its plugins
together.
+
+
+Testing
+=======
+
+Since version 1.24 Geany has a fairly simplistic test harness using the
+GLib testing framework. When adding new functions that are self-contained
+enough to test, it's a good idea to write some test code to make sure the
+new function both works the way you expect and that it continues to keep
+working that way as the code evolves.
+
+The tests are broken into suites. There is one suite named "core" that
+contains all the other (sub-)suites. When the tests are run, all sub-suites
+are run consecutively.
+
+Building Geany For Testing
+--------------------------
+
+To enable testing support in Geany, a special ``configure`` option
+``--enable-testing`` must be supplied. Then when Geany is compiled the
+symbol ``ENABLE_TESTING`` is defined in ``config.h`` and all testing-related
+code is conditionally compiled based on that define.
+
+Running Geany in Test Mode
+--------------------------
+
+Once you have Geany compiled with testing support enabled, to actually run
+the tests you pass it the ``--run-tests`` argument when running it. This
+overrides Geany's normal command-line arguments and puts Geany into testing
+mode. You can also run the tests from the build system using ``make check``
+(hint: use ``-jN` option to ``make`` to run the tests in parallel).
+
+It is a good idea to run the test suite whenever you make changes to Geany's
+code to make sure you haven't broken any of the existing code that has tests.
+
+Adding Testing Support to an Existing File
+------------------------------------------
+
+There's a little bit of boilerplate required to add testing support to one
+of the files that doesn't already have it. This only needs to be done once
+for each file where tests are added. As an example, look at the bottom
+of ``utils.c`` and ``utils.h``.
+
+Assuming you want to add testing to a file named ``foo.c``, the basic
+process is:
+
+1. Include the ``testing.h`` header file at the top of ``foo.c``.
+2. Add a function ``foo_testing_init()`` to the bottom of ``foo.c`` and
+ prototype for it at the bottom of ``foo.h``.
+3. Inside ``foo_testing_init()``, use the function
+ ``testing_add_test_func()`` to register the file's test functions (see
+ `Adding a New Test Function`_ for more information on adding tests).
+4. Include ``foo.h`` at the top of ``testing.c``.
+5. Inside ``testing.c`` in the ``testing_init`` function, call your new
+ ``foo_testing_init()`` function to get the tests for this file registered.
+
+Adding a New Test Function
+--------------------------
+
+Once the file containing the functions to tests has the boilerplate in it
+(see `Adding Testing Support to an Existing File`_), adding tests is quite
+straightforward.
+
+Assuming the example of the file ``foo.c`` again, and a function to test
+named ``foo_add()`` which adds two numbers together, the process to add
+the test would go like this:
+
+1. At the bottom of ``foo.c`` (above the ``foo_testing_init()`` function),
+ add your new test function(s). Name the function
+ ``test_<function-under-test-name>``, for our example, you would add a new
+ function named ``test_foo_add()``.
+2. Inside the ``foo_testing_init()`` function you call
+ ``testing_add_test_func()`` to register the function. The first argument
+ to ``testing_add_test_func()`` is the name of the test suite. Typically
+ this would be the name of the file without the extension. The second argument
+ is a pointer to the function to call. For example, with our ``foo.c`` file,
+ you would call ``testing_add_test_func("foo", test_foo_add)``.
+
+Writing Tests
+-------------
+
+Writing good unit tests is a topic that is outside of the scope of this
+documentation but a few guidelines are:
+
+* When writing new functions, avoid global state (ie. global variables). Each
+ function should take everything it needs through its arguments and put out
+ everything either through the return value or output arguments (or both).
+ Depending on global state couples your code with the rest of the application
+ and the test code (and indeed the function under test) may fail under certain
+ conditions as the global state is changed from other parts of the code. This
+ makes for very brittle code. In addition, avoiding global variables makes
+ the code more readable when looking at a piece of code in isolation because
+ you don't have to looking through a bunch of places to find where they were
+ defined. *Don't take Geany's existing liberal use of global variables as an
+ indication that this is how new code should be written.*
+* When adding a new function to Geany, consider writing a failing test first
+ that verifies the behaviour you want the function to have once written.
+* When writing new functions, consider how you'd test them, and code them
+ with that in mind. This will more than likely improve your code by making it
+ more self-contained and modular. Ideally you would write the test first as
+ previously mentioned.
+* When writing a function that interacts with the user interface, consider
+ making a clear distinction between the code that touches the GUI and the
+ business code behind it. If the code is sufficently broken down into units,
+ you should be able to test most of it.
+* When fixing a bug, consider writing a test that verifies that verifies the
+ bug is fixed and cannot re-occur without failing a test.
+* Test all the assumptions your function makes.
+* Don't test anything outside of the function's implementation. Each test
+ should only test one unit, you don't want to be testing other functions
+ inside a single test, for that, write other test functions for the other
+ functions.
+* If your test requires opening documents and modifying them, it is strongly
+ encouraged to tear down the documents that were previously setup once the
+ test is over. This not only makes it easier for other tests to do the same,
+ but it prevents Geany from prompting about unsaved documents when closing,
+ which is won't allow the tests to be run to the end without human
+ interaction.
+
+For more information about testing see <http://en.wikipedia.org/wiki/Unit_testing>`_.
diff --git a/configure.ac b/configure.ac
index 796e47b..5ca5436 100644
--- a/configure.ac
+++ b/configure.ac
@@ -104,6 +104,8 @@ GEANY_CHECK_SOCKET
GEANY_CHECK_VTE
GEANY_CHECK_THE_FORCE dnl hehe
+GEANY_TESTING
+
# i18n
GEANY_I18N
diff --git a/m4/geany-testing.m4 b/m4/geany-testing.m4
new file mode 100644
index 0000000..dfe264b
--- /dev/null
+++ b/m4/geany-testing.m4
@@ -0,0 +1,19 @@
+dnl GEANY_TESTING
+dnl Checks whether to enable testing support using the `--enable-testing' option.
+dnl Defines ENABLE_TESTING in the config.h header and conditionally creates a
+dnl Make file variable with the same name.
+AC_DEFUN([GEANY_TESTING], [
+
+ AC_ARG_ENABLE([testing], [
+ AS_HELP_STRING([--enable-testing],
+ [Enable support for running Geany's core tests [default=no]])
+ ])
+
+ AS_IF([test "x$enable_testing" = "xyes"], [
+ AC_DEFINE([ENABLE_TESTING], [1],
+ [Define if testing code should be compiled in])
+ ])
+ AM_CONDITIONAL([ENABLE_TESTING], [test "x$enable_testing" = "xyes"])
+
+ GEANY_STATUS_ADD([Enable testing support], [$enable_testing])
+])
diff --git a/src/Makefile.am b/src/Makefile.am
index d83963f..3a236d9 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -43,6 +43,7 @@ SRCS = \
support.h \
symbols.c symbols.h \
templates.c templates.h \
+ testing.c testing.h \
toolbar.c toolbar.h \
tools.c tools.h \
sidebar.c sidebar.h \
@@ -85,7 +86,6 @@ AM_CPPFLAGS = \
# tell automake we have a C++ file so it uses the C++ linker we need for Scintilla
nodist_EXTRA_geany_SOURCES = dummy.cxx
-
if MINGW
# build Geany for Windows on non-Windows systems (cross-compile)
@@ -148,3 +148,8 @@ clean-local:
endif
+# support running tests using make check if testing support is enabled
+if ENABLE_TESTING
+check_PROGRAMS = geany
+TESTS = @top_srcdir@/tests/runner.sh
+endif
diff --git a/src/main.c b/src/main.c
index f958607..7d7de2c 100644
--- a/src/main.c
+++ b/src/main.c
@@ -80,6 +80,10 @@
# include "vte.h"
#endif
+#ifdef ENABLE_TESTING
+# include "testing.h"
+#endif
+
#ifndef N_
# define N_(String) (String)
#endif
@@ -143,6 +147,12 @@ static GOptionEntry entries[] =
#endif
{ "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose_mode, N_("Be verbose"), NULL },
{ "version", 'V', 0, G_OPTION_ARG_NONE, &show_version, N_("Show version and exit"), NULL },
+#ifdef ENABLE_TESTING
+ { "run-tests", 0, 0, G_OPTION_ARG_NONE, &cl_options.run_tests,
+ N_("Run the core tests. Note that this disables all other regular arguments "
+ "and provides a whole different set of arguments related to the test runner. "
+ "Run with `--run-tests --help' for help with testing-specific options."), NULL },
+#endif
{ "dummy", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &dummy, NULL, NULL }, /* for +NNN line number arguments */
{ NULL, 0, 0, 0, NULL, NULL, NULL }
};
@@ -535,6 +545,20 @@ static void parse_command_line_options(gint *argc, gchar ***argv)
* so we grab that here and replace it with a no-op */
for (i = 1; i < (*argc); i++)
{
+
+#ifdef ENABLE_TESTING
+ /* Override the normal command-line arguments when --run-tests passed */
+ if (g_strcmp0((*argv)[i], "--run-tests") == 0)
+ {
+ cl_options.run_tests = TRUE;
+ memmove(&(*argv)[i], &(*argv)[i+1], *argc - i);
+ *argc -= 1;
+ testing_init(argc, argv);
+ geany_debug("Entering testing mode");
+ break;
+ }
+#endif
+
if ((*argv)[i][0] != '+')
continue;
@@ -961,6 +985,10 @@ static void load_startup_files(gint argc, gchar **argv)
static gboolean send_startup_complete(gpointer data)
{
g_signal_emit_by_name(geany_object, "geany-startup-complete");
+#ifdef ENABLE_TESTING
+ if (cl_options.run_tests)
+ testing_run_all_tests();
+#endif
return FALSE;
}
@@ -1265,6 +1293,11 @@ void main_quit()
filetypes_free_types();
log_finalize();
+#ifdef ENABLE_TESTING
+ if (cl_options.run_tests)
+ testing_finalize();
+#endif
+
tm_workspace_free(TM_WORK_OBJECT(app->tm_workspace));
g_free(app->configdir);
g_free(app->datadir);
diff --git a/src/main.h b/src/main.h
index 30e07b0..6ed7c34 100644
--- a/src/main.h
+++ b/src/main.h
@@ -33,6 +33,9 @@ typedef struct
gboolean ignore_global_tags;
gboolean list_documents;
gboolean readonly;
+#ifdef ENABLE_TESTING
+ gboolean run_tests;
+#endif
}
CommandLineOptions;
diff --git a/src/testing.c b/src/testing.c
new file mode 100644
index 0000000..b4cb143
--- /dev/null
+++ b/src/testing.c
@@ -0,0 +1,48 @@
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#ifdef ENABLE_TESTING
+
+#include "testing.h"
+#include "utils.h"
+
+
+void testing_init(int *argc, char ***argv)
+{
+ g_test_init(argc, argv, NULL);
+ utils_testing_init();
+}
+
+
+void testing_finalize(void)
+{
+}
+
+
+void testing_add_test_func(const gchar *suite_name, GTestFunc func)
+{
+ gchar *path;
+ g_return_if_fail(suite_name != NULL);
+ g_return_if_fail(func != NULL);
+ path = g_strdup_printf("/core/%s", suite_name);
+ g_test_add_func(path, func);
+ g_free(path);
+}
+
+
+static gboolean on_test_suite_run(G_GNUC_UNUSED gpointer user_data)
+{
+ g_test_run_suite(g_test_get_root());
+ on_exit_clicked(NULL, NULL);
+ return FALSE;
+}
+
+
+void testing_run_all_tests(void)
+{
+ /* Allow the main window to get up and showing before starting test functions */
+ g_idle_add_full(G_PRIORITY_LOW, (GSourceFunc)on_test_suite_run, NULL, NULL);
+}
+
+#endif /* ENABLE_TESTING */
diff --git a/src/testing.h b/src/testing.h
new file mode 100644
index 0000000..723e24e
--- /dev/null
+++ b/src/testing.h
@@ -0,0 +1,18 @@
+#if !defined(GEANY_TESTING_H) && defined(ENABLE_TESTING)
+#define GEANY_TESTING_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+void testing_init(int *argc, char ***argv);
+
+void testing_finalize(void);
+
+void testing_add_test_func(const gchar *suite_name, GTestFunc func);
+
+void testing_run_all_tests(void);
+
+G_END_DECLS
+
+#endif /* !GEANY_TESTING_H && ENABLE_TESTING */
diff --git a/src/utils.c b/src/utils.c
index a2c3526..39ed610 100644
--- a/src/utils.c
+++ b/src/utils.c
@@ -56,6 +56,10 @@
#include "utils.h"
+#ifdef ENABLE_TESTING
+# include "testing.h"
+#endif
+
/**
* Tries to open the given URI in a browser.
@@ -2098,3 +2102,24 @@ gchar *utils_parse_and_format_build_date(const gchar *input)
return g_strdup(input);
}
+
+
+/****************************************************************************
+ * TESTING *
+ ****************************************************************************/
+#ifdef ENABLE_TESTING
+
+static void test_utils_is_uri(void)
+{
+ /* TODO: write actual test code :) */
+ g_assert(1);
+}
+
+
+void utils_testing_init(void)
+{
+ /* Add test cases here */
+ testing_add_test_func("utils", test_utils_is_uri);
+}
+
+#endif /* ENABLE_TESTING */
diff --git a/src/utils.h b/src/utils.h
index 4219e65..51c61c7 100644
--- a/src/utils.h
+++ b/src/utils.h
@@ -30,6 +30,7 @@
G_BEGIN_DECLS
#include <time.h>
+#include <gdk/gdk.h> /* for GdkColor */
/** Returns @c TRUE if @a ptr is @c NULL or @c *ptr is @c FALSE. */
@@ -284,6 +285,12 @@ GDate *utils_parse_date(const gchar *input);
gchar *utils_parse_and_format_build_date(const gchar *input);
+#ifdef ENABLE_TESTING
+
+void utils_testing_init(void);
+
+#endif /* ENABLE_TESTING */
+
G_END_DECLS
#endif
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 8246550..ab56a49 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -1,2 +1,4 @@
SUBDIRS = ctags
+
+dist_check_SCRIPTS = runner.sh
diff --git a/tests/runner.sh b/tests/runner.sh
new file mode 100755
index 0000000..c05aa32
--- /dev/null
+++ b/tests/runner.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+${top_builddir:-./..}/src/geany --run-tests --verbose
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment