[gjs/wip/coverage-2: 4/9] Added GjsCoverage object (and thus code coverage support)



commit 9a55185f32b0d574d0dcdbf5be811b5d241b972f
Author: Sam Spilsbury <smspillaz gmail com>
Date:   Thu Dec 26 10:31:18 2013 -0800

    Added GjsCoverage object (and thus code coverage support)
    
    This object effectively just takes a connection to the single step
    handler and records how many times we hit each line.
    
    To prevent clutter, files considered to be part of the coverage report
    must be specified inclusively, anything not part of those paths will not
    be included in the coverage report.
    
    The coverage tool is integrated in to gjs-console, you can use it by
    specifying -C or --coverage-path to add a file to generate coverage reports
    for. If paths are specified, then --coverage-output is mandatory in order
    to specify a directory to write coverage reports to. In order to handle
    writing reports for URIs that GVfs might understand but lcov would not,
    we need to copy the covered files into this directory.
    
    Coverage reports are append-only by default. Just remove the report
    if you need to zero the counters.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=721246

 Makefile-test.am                               |   12 +-
 Makefile.am                                    |    8 +-
 configure.ac                                   |    3 +-
 gjs/console.cpp                                |   41 +
 gjs/coverage.cpp                               | 1243 ++++++++++++++++++++++
 gjs/coverage.h                                 |   86 ++
 test/gjs-test-coverage.cpp                     | 1325 ++++++++++++++++++++++++
 test/gjs-test-coverage/loadedJSFromResource.js |    1 +
 test/gjs-tests-add-funcs.h                     |    1 +
 test/gjs-tests.cpp                             |    1 +
 test/mock-js-resources.gresource.xml           |    6 +
 11 files changed, 2722 insertions(+), 5 deletions(-)
---
diff --git a/Makefile-test.am b/Makefile-test.am
index 9d6902f..19f3ef6 100644
--- a/Makefile-test.am
+++ b/Makefile-test.am
@@ -15,6 +15,15 @@ gjs_tests_CPPFLAGS =                         \
        $(gjs_directory_defines)                \
        -I$(top_srcdir)/test
 
+mock_js_resources_files = $(shell glib-compile-resources --sourcedir=$(srcdir) --generate-dependencies 
$(srcdir)/test/mock-js-resources.gresource.xml)
+mock-js-resources.h: $(srcdir)/test/mock-js-resources.gresource.xml $(modules_resource_files)
+       $(AM_V_GEN) glib-compile-resources --target=$@ --sourcedir=$(srcdir) --sourcedir=$(builddir) 
--generate --c-name mock_js_resources $<
+mock-js-resources.c: $(srcdir)/test/mock-js-resources.gresource.xml $(modules_resource_files)
+       $(AM_V_GEN) glib-compile-resources --target=$@ --sourcedir=$(srcdir) --sourcedir=$(builddir) 
--generate --c-name mock_js_resources $<
+
+EXTRA_DIST += $(mock_js_resources_files) $(srcdir)/test/mock-js-resources.gresource.xml \
+    $(srcdir)/test/gjs-test-coverage/loadedJSFromResource.js
+
 ## -rdynamic makes backtraces work
 gjs_tests_LDFLAGS = -rdynamic
 gjs_tests_LDADD =              \
@@ -25,6 +34,8 @@ gjs_tests_SOURCES =           \
        test/gjs-tests.cpp \
        test/gjs-test-reflected-script.cpp \
        test/gjs-test-debug-hooks.cpp 
+       test/gjs-test-coverage.cpp \
+       mock-js-resources.c
 
 check-local: gjs-tests
        @test -z "${TEST_PROGS}" || ${GTESTER} --verbose ${TEST_PROGS} ${TEST_PROGS_OPTIONS}
@@ -36,7 +47,6 @@ TESTS_ENVIRONMENT =                                                   \
        GJS_DEBUG_OUTPUT=test_user_data/logs/gjs.log                    \
        BUILDDIR=.                                                      \
        GJS_USE_UNINSTALLED_FILES=1                                     \
-       GJS_PATH=$(top_srcdir)/modules/                                 \
        GJS_TEST_TIMEOUT=420                                            \
        GI_TYPELIB_PATH=$(builddir)                                     \
        LD_LIBRARY_PATH="$(LD_LIBRARY_PATH):$(FIREFOX_JS_LIBDIR)"       \
diff --git a/Makefile.am b/Makefile.am
index 3b870c2..e7834ba 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -38,6 +38,7 @@ nobase_gjs_module_include_HEADERS =   \
        gjs/native.h    \
        gjs/reflected-script.h \
        gjs/debug-hooks.h \
+       gjs/coverage.h \
        gi/ns.h         \
        gi/object.h     \
        gi/foreign.h    \
@@ -112,6 +113,7 @@ libgjs_la_SOURCES =         \
        gjs/gi.cpp              \
        gjs/reflected-script.cpp \
        gjs/debug-hooks.cpp \
+       gjs/coverage.cpp \
        gjs/jsapi-private.cpp   \
        gjs/jsapi-util.cpp      \
        gjs/jsapi-dynamic-class.cpp \
@@ -211,10 +213,10 @@ bin_PROGRAMS += gjs-console
 
 gjs_console_CPPFLAGS =                 \
        $(AM_CPPFLAGS)          \
-        $(GOBJECT_CFLAGS)
+        $(GJS_CONSOLE_CFLAGS)
 gjs_console_LDADD =            \
-       $(JS_LIBS)              \
-       $(GOBJECT_LIBS)         \
+        $(JS_LIBS)             \
+        $(GJS_CONSOLE_LIBS)    \
        libgjs.la
 gjs_console_LDFLAGS = -rdynamic
 gjs_console_SOURCES = gjs/console.cpp
diff --git a/configure.ac b/configure.ac
index c8a298b..4a4efc4 100644
--- a/configure.ac
+++ b/configure.ac
@@ -74,6 +74,7 @@ AC_CHECK_FUNCS(mallinfo)
 GOBJECT_INTROSPECTION_REQUIRE([1.38.0])
 
 common_packages="gmodule-2.0 gthread-2.0 gio-2.0 >= glib_required_version mozjs-24"
+gjs_console_packages="gio-2.0 >= glib_required_version gobject-2.0 >= glib_required_version"
 gjs_packages="gobject-introspection-1.0 libffi $common_packages"
 gjs_cairo_packages="cairo cairo-gobject $common_packages"
 gjs_gdbus_packages="gobject-2.0 >= glib_required_version gio-2.0"
@@ -81,12 +82,12 @@ gjs_gtk_packages="gtk+-3.0"
 # gjs-tests links against everything
 gjstests_packages="$gjstests_packages $gjs_packages"
 
-PKG_CHECK_MODULES([GOBJECT], [gobject-2.0 >= glib_required_version])
 PKG_CHECK_MODULES([GJS], [$gjs_packages])
 PKG_CHECK_MODULES([GJS_GDBUS], [$gjs_gdbus_packages])
 PKG_CHECK_MODULES([GJSTESTS], [$gjstests_packages])
 
 # Optional cairo dep (enabled by default)
+PKG_CHECK_MODULES([GJS_CONSOLE], [$gjs_console_packages])
 AC_ARG_WITH(cairo,
            AS_HELP_STRING([--without-cairo], [Use cairo @<:@default=yes@:>@]),
            [], [with_cairo=yes])
diff --git a/gjs/console.cpp b/gjs/console.cpp
index d96bd45..d25d890 100644
--- a/gjs/console.cpp
+++ b/gjs/console.cpp
@@ -27,12 +27,17 @@
 #include <locale.h>
 
 #include <gjs/gjs.h>
+#include <gjs/coverage.h>
 
 static char **include_path = NULL;
+static char **coverage_paths = NULL;
+static char *coverage_output_path = NULL;
 static char *command = NULL;
 
 static GOptionEntry entries[] = {
     { "command", 'c', 0, G_OPTION_ARG_STRING, &command, "Program passed in as a string", "COMMAND" },
+    { "coverage-path", 'C', 0, G_OPTION_ARG_STRING_ARRAY, &coverage_paths, "Add the directory DIR to the 
list of directories to generate coverage info for", "DIR" },
+    { "coverage-output", 0, 0, G_OPTION_ARG_STRING, &coverage_output_path, "Write coverage output to a 
directory DIR. This option is mandatory when using --coverage-path", "DIR", },
     { "include-path", 'I', 0, G_OPTION_ARG_STRING_ARRAY, &include_path, "Add the directory DIR to the list 
of directories to search for js files.", "DIR" },
     { NULL }
 };
@@ -51,12 +56,35 @@ print_help (GOptionContext *context,
   exit (0);
 }
 
+static GValue *
+init_array_parameter(GArray      *array,
+                     guint       index,
+                     const gchar *name,
+                     GType       type)
+{
+    if (index >= array->len)
+        g_array_set_size(array, index + 1);
+
+    GParameter *param = &(g_array_index(array, GParameter, index));
+    param->name = name;
+    g_value_init(&param->value, type);
+    return &param->value;
+}
+
+static void
+clear_array_parameter_value(gpointer value)
+{
+    GParameter *parameter = (GParameter *) value;
+    g_value_unset(&parameter->value);
+}
+
 int
 main(int argc, char **argv)
 {
     GOptionContext *context;
     GError *error = NULL;
     GjsContext *js_context;
+    GjsCoverage *coverage = NULL;
     char *script;
     const char *filename;
     const char *program_name;
@@ -111,6 +139,14 @@ main(int argc, char **argv)
                                             "program-name", program_name,
                                             NULL);
 
+    if (coverage_paths) {
+        if (!coverage_output_path)
+            g_error("--coverage-output-path is required when taking coverage statistics");
+
+        coverage = gjs_coverage_new(gjs_context_get_debug_hooks(js_context),
+                                    (const gchar **) coverage_paths);
+    }
+
     /* prepare command line arguments */
     if (!gjs_context_define_string_array(js_context, "ARGV",
                                          argc - 1, (const char**)argv + 1,
@@ -130,6 +166,11 @@ main(int argc, char **argv)
         goto out;
     }
 
+    if (coverage) {
+        gjs_coverage_write_statistics(coverage, coverage_output_path);
+        g_clear_object(&coverage);
+    }
+
  out:
     g_object_unref(js_context);
     g_free(script);
diff --git a/gjs/coverage.cpp b/gjs/coverage.cpp
new file mode 100644
index 0000000..ad4eacf
--- /dev/null
+++ b/gjs/coverage.cpp
@@ -0,0 +1,1243 @@
+/*
+ * Copyright © 2014 Endless Mobile, Inc.
+ *
+ * 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 2 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, write to the Free Software Foundation,
+ * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Authored By: Sam Spilsbury <sam endlessm com>
+ */
+#include <stdio.h>
+#include <string.h>
+
+#include <fcntl.h>
+
+#include <gio/gio.h>
+#include <gjs/gjs.h>
+#include <gjs/debug-hooks.h>
+#include <gjs/reflected-script.h>
+#include <gjs/coverage.h>
+
+typedef struct _GjsCoverageBranchData GjsCoverageBranchData;
+
+struct _GjsCoveragePrivate {
+    GHashTable    *file_statistics;
+    GjsDebugHooks *debug_hooks;
+    gchar         **covered_paths;
+
+    /* A separate context where reflection is performed. We don't
+     * want to use the main context because we don't want to
+     * modify its state while it is being debugged.
+     *
+     * A single context is shared across all reflections because
+     * the reflection functions are effectively const.
+     *
+     * This is created on-demand when debugging is enabled. GjsContext
+     * creates an instance of us by default so we obviously don't want
+     * to recurse into creating an instance of GjsContext by default.
+     */
+    GjsContext *reflection_context;
+
+    guint         new_scripts_connection;
+    guint         single_step_connection;
+    guint         frame_step_connection;
+
+    /* If we hit a branch and the next single-step line will
+     * activate one of the branch alternatives then this will
+     * be set to that branch
+     *
+     * XXX: This isn't necessarily safe in the presence of
+     * multiple execution contexts which are connected
+     * to the same GjsCoveragePrivate's single step hook */
+    GjsCoverageBranchData *active_branch;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE(GjsCoverage,
+                           gjs_coverage,
+                           G_TYPE_OBJECT)
+
+enum {
+    PROP_0,
+    PROP_DEBUG_HOOKS,
+    PROP_COVERAGE_PATHS,
+    PROP_N
+};
+
+static GParamSpec *properties[PROP_N] = { NULL, };
+
+struct _GjsCoverageBranchData {
+    GArray       *branch_alternatives;
+    GArray       *branch_alternatives_taken;
+    unsigned int branch_point;
+    unsigned int last_branch_exit;
+    gboolean     branch_hit;
+};
+
+static unsigned int
+determine_highest_unsigned_int(GArray *array)
+{
+    unsigned int highest = 0;
+    unsigned int i = 0;
+
+    for (; i < array->len; ++i) {
+        unsigned int value = g_array_index(array, unsigned int, i);
+        if (highest < value)
+            highest = value;
+    }
+
+    return highest;
+}
+
+static void
+gjs_coverage_branch_info_init(GjsCoverageBranchData              *data,
+                              const GjsReflectedScriptBranchInfo *info)
+{
+    g_assert(data->branch_alternatives == NULL);
+    g_assert(data->branch_alternatives_taken == NULL);
+    g_assert(data->branch_point == 0);
+    g_assert(data->last_branch_exit == 0);
+    g_assert(data->branch_hit == 0);
+
+    unsigned int n_branches;
+    const unsigned int *alternatives =
+        gjs_reflected_script_branch_info_get_branch_alternatives(info, &n_branches);
+
+    /* We need to copy the alternatives as there's a case where we might outlive
+     * the reflected script.
+     *
+     * Another potential option here would be to expose the GArray or
+     * GjsReflectedScript here. However, it doesn't make sense for this structure
+     * to have ownership of either */
+    data->branch_alternatives = g_array_sized_new(FALSE, TRUE, sizeof(unsigned int), n_branches);
+    g_array_set_size(data->branch_alternatives, n_branches);
+
+    memcpy(data->branch_alternatives->data,
+           alternatives,
+           sizeof(unsigned int) * n_branches);
+
+    data->branch_alternatives_taken =
+        g_array_new(FALSE, TRUE, sizeof(unsigned int));
+    g_array_set_size(data->branch_alternatives_taken, n_branches);
+    data->branch_point = gjs_reflected_script_branch_info_get_branch_point(info);
+    data->last_branch_exit = determine_highest_unsigned_int(data->branch_alternatives);
+    data->branch_hit = FALSE;
+}
+
+static void
+gjs_coverage_branch_info_clear(gpointer data_ptr)
+{
+    GjsCoverageBranchData *data = (GjsCoverageBranchData *) data_ptr;
+
+    if (data->branch_alternatives_taken) {
+        g_array_unref(data->branch_alternatives_taken);
+        data->branch_alternatives_taken = NULL;
+    }
+
+    if (data->branch_alternatives) {
+        g_array_unref(data->branch_alternatives);
+        data->branch_alternatives = NULL;
+    }
+}
+
+static char *
+create_function_lookup_key(const gchar  *name,
+                           unsigned int  line,
+                           unsigned int  n_param)
+{
+    return g_strdup_printf("%s:%i:%i",
+                           name ? name : "(anonymous)",
+                           line,
+                           n_param);
+}
+
+typedef struct _GjsCoverageFileStatistics {
+    /* 1-1 with line numbers for O(N) lookup */
+    GArray     *lines;
+    GArray     *branches;
+
+    /* Hash buckets for O(logn) lookup */
+    GHashTable *functions;
+} GjsCoverageFileStatistics;
+
+GjsCoverageFileStatistics *
+gjs_coverage_file_statistics_new(GArray *all_lines,
+                                 GArray *all_branches,
+                                 GHashTable *all_functions)
+{
+    GjsCoverageFileStatistics *file_stats = g_new0(GjsCoverageFileStatistics, 1);
+    file_stats->lines = all_lines;
+    file_stats->branches = all_branches;
+    file_stats->functions = all_functions;
+    return file_stats;
+}
+
+void
+gjs_coverage_file_statistics_destroy(gpointer data)
+{
+    GjsCoverageFileStatistics *file_stats = (GjsCoverageFileStatistics *) data;
+    g_array_unref(file_stats->lines);
+    g_array_unref(file_stats->branches);
+    g_hash_table_unref(file_stats->functions);
+    g_free(file_stats);
+}
+
+static void
+increment_line_hits(GArray       *line_counts,
+                    unsigned int  line_no)
+{
+    g_assert(line_no <= line_counts->len);
+
+    int *line_hit_count = &(g_array_index(line_counts, int, line_no));
+
+    /* If this happens it is not a huge problem - though it does
+     * mean that infoReflect.js is not doing its job, so we should
+     * print a debug message about it in case someone is interested.
+     *
+     * The reason why we don't have a proper warning is because it
+     * is difficult to determine what the SpiderMonkey program counter
+     * will actually pass over, especially function declarations for some
+     * reason:
+     *
+     *     function f(a,b) {
+     *         a = 1;
+     *     }
+     *
+     * In some cases, the declaration itself will be executed
+     * but in other cases it won't be. Reflect.parse tells us that
+     * the only two expressions on that line are a FunctionDeclaration
+     * and BlockStatement, neither of which would ordinarily be
+     * executed */
+    if (*line_hit_count == -1) {
+        g_debug("Executed line %i which we thought was not executable", line_no);
+        *line_hit_count = 0;
+    }
+
+    ++(*line_hit_count);
+}
+
+static void
+increment_hits_on_branch(GjsCoverageBranchData *branch,
+                         unsigned int           line)
+{
+    if (!branch)
+        return;
+
+    g_assert (branch->branch_alternatives->len == branch->branch_alternatives_taken->len);
+
+    unsigned int i;
+    for (i = 0; i < branch->branch_alternatives->len; ++i) {
+
+        if (g_array_index (branch->branch_alternatives, unsigned int, i) == line) {
+            unsigned int *hit_count = &(g_array_index(branch->branch_alternatives_taken,
+                                                      unsigned int,
+                                                      i));
+            ++(*hit_count);
+        }
+    }
+}
+
+/* Return a valid GjsCoverageBranchData if this line actually
+ * contains a valid branch (eg GjsReflectedScriptBranchInfo is set) */
+static GjsCoverageBranchData *
+find_active_branch(GArray                *branches,
+                   unsigned int           line,
+                   GjsCoverageBranchData *active_branch)
+{
+    g_assert(line <= branches->len);
+
+    GjsCoverageBranchData *branch = &(g_array_index(branches, GjsCoverageBranchData, line));
+    if (branch->branch_point) {
+        branch->branch_hit = TRUE;
+        return branch;
+    }
+
+    /* We shouldn't return NULL until we're actually outside the
+     * active branch, since we might be in a case statement where
+     * we need to check every possible option before jumping to an
+     * exit */
+    if (active_branch) {
+        if (line <= active_branch->last_branch_exit)
+            return active_branch;
+
+        return NULL;
+    }
+
+    return NULL;
+}
+
+static void
+gjs_coverage_single_step_interrupt_hook(GjsDebugHooks   *hooks,
+                                        GjsContext      *context,
+                                        GjsLocationInfo *info,
+                                        gpointer         user_data)
+{
+    GjsCoverage *coverage = (GjsCoverage *) user_data;
+    GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
+    const GjsFrameInfo *frame = gjs_location_info_get_current_frame(info);
+
+    const char *filename = frame->current_function.filename;
+    unsigned int line_no = frame->current_line;
+    GHashTable  *statistics_table = priv->file_statistics;
+    GjsCoverageFileStatistics *statistics =
+        (GjsCoverageFileStatistics *) g_hash_table_lookup(statistics_table,
+                                                          filename);
+    /* We don't care about this file, even if we're single-stepping it */
+    if (!statistics)
+        return;
+
+    /* Line counters */
+    increment_line_hits(statistics->lines, line_no);
+
+    /* Branch counters. First increment branch hits for the active
+     * branch and then find a new potentially active branch */
+    increment_hits_on_branch(priv->active_branch, line_no);
+    priv->active_branch = find_active_branch(statistics->branches,
+                                             line_no,
+                                             priv->active_branch);
+}
+
+static void
+gjs_coverage_frame_execution_hook(GjsDebugHooks   *hooks,
+                                  GjsContext      *context,
+                                  GjsLocationInfo *info,
+                                  GjsFrameState    state,
+                                  gpointer         user_data)
+{
+    /* We don't care about after-hits */
+    if (state != GJS_FRAME_ENTRY)
+        return;
+
+    const GjsFrameInfo *frame = gjs_location_info_get_current_frame(info);
+    const char *function_name = frame->current_function.function_name;
+    unsigned int line = frame->current_function.line;
+    unsigned int n_params = frame->current_function.n_args;
+
+    GHashTable *all_statistics = (GHashTable *) user_data;
+    GjsCoverageFileStatistics *file_statistics =
+        (GjsCoverageFileStatistics *) g_hash_table_lookup(all_statistics,
+                                                          frame->current_function.filename);
+
+    /* We don't care about this script */
+    if (!file_statistics)
+        return;
+
+    /* Not a function, so we don't care */
+    if (!function_name)
+        return;
+
+    /* If you have a function name longer than this then there's a real problem.
+     * We are using the stack here since this is a rather hot path and allocation
+     * is something that we should avoid doing here */
+    gchar key_buffer[1024];
+    if (snprintf(key_buffer,
+                 1024,
+                 "%s:%i:%i",
+                 function_name ? function_name : "f",
+                 line,
+                 n_params) >= 1024) {
+        g_warning("Failed to create function key, the function name %s is too long!",
+                   function_name ? function_name : "f");
+        return;
+    }
+
+    if (!g_hash_table_contains(file_statistics->functions, key_buffer)) {
+        g_debug("Entered unknown function %s:%i:%i",
+                function_name,
+                line,
+                n_params);
+    }
+
+    unsigned int hit_count = GPOINTER_TO_INT(g_hash_table_lookup(file_statistics->functions, function_name));
+    ++hit_count;
+
+    /* The GHashTable API requires that we copy the key again, in both the
+     * insert and replace case */
+    g_hash_table_replace(file_statistics->functions,
+                         g_strdup(key_buffer),
+                         GINT_TO_POINTER(hit_count));
+}
+
+/*
+ * The created array is a 1-1 representation of the hitcount in the filename. Each
+ * element refers to an individual line. In order to avoid confusion, our array
+ * is zero indexed, but the zero'th line is always ignored and the first element
+ * refers to the first line of the file.
+ *
+ * A value of -1 for an element means that the line is non-executable and never actually
+ * reached. A value of 0 means that it was executable but never reached. A positive value
+ * indicates the hit count.
+ *
+ * We care about non-executable lines because we don't want to report coverage misses for
+ * lines that could have never been executed anyways.
+ *
+ * The reason for using a 1-1 mapping as opposed to an array of key-value pairs for executable
+ * lines is:
+ *   1. Lookup speed is O(1) instead of O(log(n))
+ *   2. There's a possibility we might hit a line which we thought was non-executable, in which
+ *      case we can neatly handle the error by marking that line executable. A hit on a line
+ *      we thought was non-executable is not as much of a problem as noise generated by
+ *      ostensible "misses" which could in fact never be executed.
+ *
+ */
+static GArray *
+create_line_coverage_statistics_from_reflection(GjsReflectedScript *reflected_script)
+{
+    unsigned int line_count = gjs_reflected_script_get_n_lines(reflected_script);
+    GArray *line_statistics = g_array_new(TRUE, FALSE, sizeof(int));
+
+    /* We are ignoring the zeroth line, so we want line_count + 1 */
+    g_array_set_size(line_statistics, line_count + 1);
+
+    if (line_count)
+        memset(line_statistics->data, -1, sizeof(int) * line_statistics->len);
+
+    unsigned int       n_expression_lines;
+    const unsigned int *executable_lines =
+        gjs_reflected_script_get_expression_lines(reflected_script,
+                                                  &n_expression_lines);
+
+    /* In order to determine which lines are executable to start off with, we take
+     * the array of executable lines provided to us with gjs_debug_script_info_get_executable_lines
+     * and change the array value of each line to zero. If these lines are never executed then
+     * they will be considered a coverage miss */
+    if (executable_lines) {
+        unsigned int i;
+        for (i = 0; i < n_expression_lines; ++i)
+            g_array_index(line_statistics, int, executable_lines[i]) = 0;
+    }
+
+    return line_statistics;
+}
+
+/* As above, we are creating a 1-1 representation of script lines to potential branches
+ * where each element refers to a 1-index line (with the zero'th ignored).
+ *
+ * Each element is a GjsCoverageBranchData which, if the line at the element
+ * position describes a branch, will be populated with a GjsReflectedScriptBranchInfo
+ * and an array of unsigned each specifying the hit-count for each potential branch
+ * in the branch info */
+static GArray *
+create_branch_coverage_statistics_from_reflection(GjsReflectedScript *reflected_script)
+{
+    unsigned int line_count = gjs_reflected_script_get_n_lines(reflected_script);
+    GArray *branch_statistics = g_array_new(FALSE, TRUE, sizeof(GjsCoverageBranchData));
+    g_array_set_size(branch_statistics, line_count + 1);
+    g_array_set_clear_func(branch_statistics, gjs_coverage_branch_info_clear);
+
+    const GjsReflectedScriptBranchInfo **branch_info_iterator =
+        gjs_reflected_script_get_branches(reflected_script);
+
+    if (*branch_info_iterator) {
+        do {
+            unsigned int branch_point =
+                gjs_reflected_script_branch_info_get_branch_point(*branch_info_iterator);
+
+            g_assert(branch_point <= branch_statistics->len);
+
+            GjsCoverageBranchData *branch_data (&(g_array_index(branch_statistics,
+                                                                GjsCoverageBranchData,
+                                                                branch_point)));
+            gjs_coverage_branch_info_init(branch_data, *branch_info_iterator);
+        } while (*(++branch_info_iterator));
+    }
+
+    return branch_statistics;
+}
+
+static GHashTable *
+create_function_coverage_statistics_from_reflection(GjsReflectedScript *reflected_script)
+{
+    GHashTable *functions = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
+    const GjsReflectedScriptFunctionInfo **function_info_iterator =
+        gjs_reflected_script_get_functions(reflected_script);
+
+    if (*function_info_iterator) {
+        do {
+            const char   *name = gjs_reflected_script_function_info_get_name(*function_info_iterator);
+            unsigned int line = gjs_reflected_script_function_info_get_line_number(*function_info_iterator);
+            unsigned int n_params = gjs_reflected_script_function_info_get_n_params(*function_info_iterator);
+
+            char *key = create_function_lookup_key(name, line, n_params);
+            g_hash_table_insert(functions, key, GINT_TO_POINTER(0));
+        } while (*(++function_info_iterator));
+    }
+
+    return functions;
+}
+
+static GjsCoverageFileStatistics *
+create_statistics_from_reflection(GjsReflectedScript *reflected_script)
+{
+    GArray *line_coverage_statistics =
+        create_line_coverage_statistics_from_reflection(reflected_script);
+    GArray *branch_coverage_statistics =
+        create_branch_coverage_statistics_from_reflection(reflected_script);
+    GHashTable *function_coverage_statistics =
+        create_function_coverage_statistics_from_reflection(reflected_script);
+
+    g_assert(line_coverage_statistics);
+    g_assert(branch_coverage_statistics);
+    g_assert(function_coverage_statistics);
+
+    return gjs_coverage_file_statistics_new(line_coverage_statistics,
+                                            branch_coverage_statistics,
+                                            function_coverage_statistics);
+
+}
+
+static GjsCoverageFileStatistics *
+new_statistics_for_filename(GjsContext *reflection_context,
+                            const char *filename)
+{
+    GjsReflectedScript *reflected_script =
+        gjs_reflected_script_new(filename, reflection_context);
+    GjsCoverageFileStatistics *stats =
+        create_statistics_from_reflection(reflected_script);
+    g_object_unref(reflected_script);
+
+    return stats;
+}
+
+static void
+gjs_coverage_new_script_available_hook(GjsDebugHooks      *reg,
+                                       GjsContext         *context,
+                                       GjsDebugScriptInfo *info,
+                                       gpointer            user_data)
+{
+    const gchar *filename = gjs_debug_script_info_get_filename(info);
+    GjsCoverage *coverage = (GjsCoverage *) user_data;
+    GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
+    GHashTable  *file_statistics = priv->file_statistics;
+
+    if (g_hash_table_contains(file_statistics, filename)) {
+        GjsCoverageFileStatistics *statistics =
+            (GjsCoverageFileStatistics *) g_hash_table_lookup(file_statistics,
+                                                              filename);
+
+        if (!statistics) {
+            statistics = new_statistics_for_filename(priv->reflection_context, filename);
+
+            /* If create_statistics_for_filename returns NULL then we can
+             * just bail out here, the stats print function will handle
+             * the NULL case */
+            if (!statistics)
+                return;
+
+            g_hash_table_insert(file_statistics,
+                                g_strdup(filename),
+                                statistics);
+        }
+    }
+}
+
+static void
+write_string_into_stream(GOutputStream *stream,
+                         const gchar   *string)
+{
+    g_output_stream_write(stream, (gconstpointer) string, strlen(string) * sizeof(gchar), NULL, NULL);
+}
+
+static void
+write_source_file_header(GOutputStream *stream,
+                         const gchar   *source_file_path)
+{
+    write_string_into_stream(stream, "SF:");
+    write_string_into_stream(stream, source_file_path);
+    write_string_into_stream(stream, "\n");
+}
+
+typedef struct _FunctionHitCountData {
+    GOutputStream *stream;
+    unsigned int  *n_functions_found;
+    unsigned int  *n_functions_hit;
+} FunctionHitCountData;
+
+static void
+write_function_hit_count(GOutputStream *stream,
+                         const char    *function_name,
+                         unsigned int   hit_count,
+                         unsigned int  *n_functions_found,
+                         unsigned int  *n_functions_hit)
+{
+    char *line = g_strdup_printf("FNDA:%i,%s\n",
+                                 hit_count,
+                                 function_name);
+
+    (*n_functions_found)++;
+
+    if (hit_count > 0)
+        (*n_functions_hit)++;
+
+    write_string_into_stream(stream, line);
+    g_free(line);
+}
+
+static void
+write_functions_hit_counts(GOutputStream *stream,
+                           GHashTable    *functions,
+                           unsigned int  *n_functions_found,
+                           unsigned int  *n_functions_hit)
+{
+    GHashTableIter iter;
+    g_hash_table_iter_init(&iter, functions);
+
+    gpointer key, value;
+    while (g_hash_table_iter_next(&iter, &key, &value)) {
+        const char    *function_key = (const char *) key;
+        unsigned int  hit_count = GPOINTER_TO_INT(value);
+
+        write_function_hit_count(stream,
+                                 function_key,
+                                 hit_count,
+                                 n_functions_found,
+                                 n_functions_hit);
+    }
+}
+
+static void
+write_function_foreach_func(gpointer key,
+                            gpointer value,
+                            gpointer user_data)
+{
+    GOutputStream *stream = (GOutputStream *) user_data;
+    const char    *function_key = (const char *) key;
+
+    write_string_into_stream(stream, "FN:");
+    write_string_into_stream(stream, function_key);
+    write_string_into_stream(stream, "\n");
+}
+
+static void
+write_functions(GOutputStream *data_stream,
+                GHashTable    *functions)
+{
+    g_hash_table_foreach(functions, write_function_foreach_func, data_stream);
+}
+
+static void
+write_uint32_into_stream(GOutputStream *stream,
+                         unsigned int   integer)
+{
+    char buf[32];
+    g_snprintf(buf, 32, "%u", integer);
+    g_output_stream_write(stream, (gconstpointer) buf, strlen(buf) * sizeof(char), NULL, NULL);
+}
+
+static void
+write_int32_into_stream(GOutputStream *stream,
+                        int            integer)
+{
+    char buf[32];
+    g_snprintf(buf, 32, "%i", integer);
+    g_output_stream_write(stream, (gconstpointer) buf, strlen(buf) * sizeof(char), NULL, NULL);
+}
+
+static void
+write_function_coverage(GOutputStream *data_stream,
+                        unsigned int  n_found_functions,
+                        unsigned int  n_hit_functions)
+{
+    write_string_into_stream(data_stream, "FNF:");
+    write_uint32_into_stream(data_stream, n_found_functions);
+    write_string_into_stream(data_stream, "\n");
+
+    write_string_into_stream(data_stream, "FNH:");
+    write_uint32_into_stream(data_stream, n_hit_functions);
+    write_string_into_stream(data_stream, "\n");
+}
+
+static void
+for_each_element_in_array(GArray   *array,
+                          GFunc     func,
+                          gpointer  user_data)
+{
+    const gsize element_size = g_array_get_element_size(array);
+    unsigned int i;
+    char         *current_array_pointer = (char *) array->data;
+
+    for (i = 0; i < array->len; ++i, current_array_pointer += element_size)
+        (*func)(current_array_pointer, user_data);
+}
+
+typedef struct _WriteAlternativeData {
+    unsigned int  *n_branch_alternatives_found;
+    unsigned int  *n_branch_alternatives_hit;
+    GOutputStream *output_stream;
+    gpointer      *all_alternatives;
+    gboolean      branch_point_was_hit;
+} WriteAlternativeData;
+
+typedef struct _WriteBranchInfoData {
+    unsigned int *n_branch_alternatives_found;
+    unsigned int *n_branch_alternatives_hit;
+    GOutputStream *output_stream;
+} WriteBranchInfoData;
+
+static void
+write_individual_branch(gpointer branch_ptr,
+                        gpointer user_data)
+{
+    GjsCoverageBranchData *branch = (GjsCoverageBranchData *) branch_ptr;
+    WriteBranchInfoData   *data = (WriteBranchInfoData *) user_data;
+
+    /* This line is not a branch, don't write anything */
+    if (!branch->branch_point)
+        return;
+
+    unsigned int i = 0;
+    for (; i < branch->branch_alternatives_taken->len; ++i) {
+        unsigned int alternative_counter = g_array_index(branch->branch_alternatives_taken,
+                                                         unsigned int,
+                                                         i);
+        unsigned int branch_point = branch->branch_point;
+        char         *hit_count_string = NULL;
+
+        if (!branch->branch_hit)
+            hit_count_string = g_strdup_printf("-");
+        else
+            hit_count_string = g_strdup_printf("%i", alternative_counter);
+
+        char *branch_alternative_line = g_strdup_printf("BRDA:%i,0,%i,%s\n",
+                                                        branch_point,
+                                                        i,
+                                                        hit_count_string);
+
+        write_string_into_stream(data->output_stream, branch_alternative_line);
+        g_free(hit_count_string);
+        g_free(branch_alternative_line);
+
+        ++(*data->n_branch_alternatives_found);
+
+        if (alternative_counter > 0)
+            ++(*data->n_branch_alternatives_hit);
+    }
+}
+
+static void
+write_branch_coverage(GOutputStream *stream,
+                      GArray        *branches,
+                      unsigned int  *n_branch_alternatives_found,
+                      unsigned int  *n_branch_alternatives_hit)
+
+{
+    /* Write individual branches and pass-out the totals */
+    WriteBranchInfoData data = {
+        n_branch_alternatives_found,
+        n_branch_alternatives_hit,
+        stream
+    };
+
+    for_each_element_in_array(branches,
+                              write_individual_branch,
+                              &data);
+}
+
+static void
+write_branch_totals(GOutputStream *stream,
+                    unsigned int   n_branch_alternatives_found,
+                    unsigned int   n_branch_alternatives_hit)
+{
+    write_string_into_stream(stream, "BRF:");
+    write_uint32_into_stream(stream, n_branch_alternatives_found);
+    write_string_into_stream(stream, "\n");
+
+    write_string_into_stream(stream, "BRH:");
+    write_uint32_into_stream(stream, n_branch_alternatives_hit);
+    write_string_into_stream(stream, "\n");
+}
+
+static void
+write_line_coverage(GOutputStream *stream,
+                    GArray        *stats,
+                    unsigned int  *lines_hit_count,
+                    unsigned int  *executable_lines_count)
+{
+    unsigned int i = 0;
+    for (i = 0; i < stats->len; ++i) {
+        int hit_count_for_line = g_array_index(stats, int, i);
+
+        if (hit_count_for_line == -1)
+            continue;
+
+        write_string_into_stream(stream, "DA:");
+        write_uint32_into_stream(stream, i);
+        write_string_into_stream(stream, ",");
+        write_int32_into_stream(stream, hit_count_for_line);
+        write_string_into_stream(stream, "\n");
+
+        if (hit_count_for_line > 0)
+          ++(*lines_hit_count);
+
+        ++(*executable_lines_count);
+    }
+}
+
+static void
+write_line_totals(GOutputStream *stream,
+                  unsigned int   lines_hit_count,
+                  unsigned int   executable_lines_count)
+{
+    write_string_into_stream(stream, "LH:");
+    write_uint32_into_stream(stream, lines_hit_count);
+    write_string_into_stream(stream, "\n");
+
+    write_string_into_stream(stream, "LF:");
+    write_uint32_into_stream(stream, executable_lines_count);
+    write_string_into_stream(stream, "\n");
+}
+
+static void
+write_end_of_record(GOutputStream *stream)
+{
+    write_string_into_stream(stream, "end_of_record\n");
+}
+
+static void
+copy_source_file_to_coverage_output(const char *source,
+                                    const char *destination)
+{
+    GFile *source_file = g_file_new_for_commandline_arg(source);
+    GFile *destination_file = g_file_new_for_commandline_arg(destination);
+    GError *error = NULL;
+
+    /* We also need to recursively make the directory we
+     * want to copy to, as g_file_copy doesn't do that */
+    gchar *destination_dirname = g_path_get_dirname(destination);
+    g_mkdir_with_parents(destination_dirname, S_IRWXU);
+
+    if (!g_file_copy(source_file,
+                     destination_file,
+                     G_FILE_COPY_OVERWRITE,
+                     NULL,
+                     NULL,
+                     NULL,
+                     &error)) {
+        g_critical("Failed to copy source file %s to destination %s: %s\n",
+                   source,
+                   destination,
+                   error->message);
+    }
+
+    g_clear_error(&error);
+
+    g_free(destination_dirname);
+    g_object_unref(destination_file);
+    g_object_unref(source_file);
+}
+
+typedef struct _StatisticsPrintUserData {
+    GjsContext        *reflection_context;
+    GFileOutputStream *ostream;
+    const gchar       *output_directory;
+} StatisticsPrintUserData;
+
+/* This function will strip a URI scheme and return
+ * the string with the URI scheme stripped or NULL
+ * if the path was not a valid URI
+ */
+static const char *
+strip_uri_scheme(const char *potential_uri)
+{
+    char *uri_header = g_uri_parse_scheme(potential_uri);
+
+    if (uri_header) {
+        gsize offset = strlen(uri_header);
+        g_free(uri_header);
+
+        /* g_uri_parse_scheme only parses the name
+         * of the scheme, we also need to strip the
+         * characters '://' */
+        return potential_uri + offset + 3;
+    }
+
+    return NULL;
+}
+
+/* This function will return a string of pathname
+ * components from the first directory indicating
+ * where two directories diverge. For instance:
+ *
+ * child_path: /a/b/c/d/e
+ * parent_path: /a/b/d/
+ *
+ * Will return: c/d/e
+ *
+ * If the directories are not at all similar then
+ * the full dirname of the child_path effectively
+ * be returned.
+ *
+ * As a special case, child paths that are a URI
+ * automatically return the full URI path with
+ * the URI scheme stripped out.
+ */
+static char *
+find_diverging_child_components(const char *child_path,
+                                const char *parent_path)
+{
+    const char *stripped_uri = strip_uri_scheme(child_path);
+
+    if (stripped_uri)
+        return g_strdup(stripped_uri);
+
+    char **child_path_components = g_strsplit(child_path, "/", -1);
+    char **parent_path_components = g_strsplit(parent_path, "/", -1);
+    char **child_path_component_iterator = child_path_components;
+    char **parent_path_component_iterator = parent_path_components;
+
+    for (; *child_path_component_iterator != NULL &&
+           *parent_path_component_iterator != NULL;
+           ++child_path_component_iterator,
+           ++parent_path_component_iterator) {
+        if (g_strcmp0(*child_path_component_iterator,
+                      *parent_path_component_iterator))
+            break;
+    }
+
+    /* Paste the child path components back together */
+    char *diverged = g_strjoinv("/", child_path_component_iterator);
+
+    g_strfreev(child_path_components);
+    g_strfreev(parent_path_components);
+
+    return diverged;
+}
+
+/* The coverage output directory could be a relative path
+ * so we need to get an absolute path */
+static char *
+get_absolute_path(const char *path)
+{
+    char *absolute_path = NULL;
+
+    if (!g_path_is_absolute(path)) {
+        char *current_dir = g_get_current_dir();
+        absolute_path = g_build_filename(current_dir,
+                                                path,
+                                                NULL);
+        g_free(current_dir);
+    } else {
+        absolute_path = g_strdup(path);
+    }
+
+    return absolute_path;
+}
+
+static void
+print_statistics_for_files(gpointer key,
+                           gpointer value,
+                           gpointer user_data)
+{
+    StatisticsPrintUserData   *statistics_print_data = (StatisticsPrintUserData *) user_data;
+    const char                *filename = (const char *) key;
+    GjsCoverageFileStatistics *stats = (GjsCoverageFileStatistics *) value;
+
+    /* If there is no statistics for this file, then we should
+     * compile the script and print statistics for it now */
+    if (!stats)
+        stats = new_statistics_for_filename(statistics_print_data->reflection_context,
+                                                             filename);
+
+    /* Still couldn't create statistics, bail out */
+    if (!stats)
+        return;
+
+    /* get_appropriate_tracefile_ref will automatically set the write
+     * pointer to the correct place in the file */
+    GOutputStream *ostream = G_OUTPUT_STREAM(statistics_print_data->ostream);
+
+    char *absolute_output_directory = get_absolute_path(statistics_print_data->output_directory);
+    char *diverged_paths =
+        find_diverging_child_components(filename,
+                                        absolute_output_directory);
+    char *destination_filename = g_build_filename(absolute_output_directory,
+                                                  diverged_paths,
+                                                  NULL);
+
+    copy_source_file_to_coverage_output(filename, destination_filename);
+
+    write_source_file_header(ostream, (const char *) destination_filename);
+    write_functions(ostream,
+                    stats->functions);
+
+    unsigned int functions_hit_count = 0;
+    unsigned int functions_found_count = 0;
+
+    write_functions_hit_counts(ostream,
+                               stats->functions,
+                               &functions_found_count,
+                               &functions_hit_count);
+    write_function_coverage(ostream,
+                            functions_found_count,
+                            functions_hit_count);
+
+    unsigned int branches_hit_count = 0;
+    unsigned int branches_found_count = 0;
+
+    write_branch_coverage(ostream,
+                          stats->branches,
+                          &branches_found_count,
+                          &branches_hit_count);
+    write_branch_totals(ostream,
+                        branches_found_count,
+                        branches_hit_count);
+
+    unsigned int lines_hit_count = 0;
+    unsigned int executable_lines_count = 0;
+
+    write_line_coverage(ostream,
+                        stats->lines,
+                        &lines_hit_count,
+                        &executable_lines_count);
+    write_line_totals(ostream,
+                      lines_hit_count,
+                      executable_lines_count);
+    write_end_of_record(ostream);
+
+    /* If value was initially NULL, then we should unref stats here */
+    if (!value)
+        gjs_coverage_file_statistics_destroy(stats);
+
+    g_free(diverged_paths);
+    g_free(destination_filename);
+    g_free(absolute_output_directory);
+}
+
+void
+gjs_coverage_write_statistics(GjsCoverage *coverage,
+                              const char  *output_directory)
+{
+    GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
+    GError *error = NULL;
+    GFileOutputStream *ostream = NULL;
+
+    /* Create output_directory if it doesn't exist */
+    g_mkdir_with_parents(output_directory, 0755);
+
+    char  *output_file_path = g_build_filename(output_directory,
+                                               "coverage.lcov",
+                                               NULL);
+    GFile *output_file = g_file_new_for_commandline_arg(output_file_path);
+    g_free (output_file_path);
+
+    /* Remove our new script hook so that we don't get spurious calls
+     * to it whilst compiling new scripts */
+    gjs_debug_hooks_remove_script_load_hook(priv->debug_hooks, priv->new_scripts_connection);
+    priv->new_scripts_connection = 0;
+
+    ostream = g_file_append_to(output_file,
+                               G_FILE_CREATE_NONE,
+                               NULL,
+                               &error);
+
+    if (!ostream) {
+        char *output_file_path = g_file_get_path(output_file);
+        g_warning("Unable to open output file %s: %s",
+                  output_file_path,
+                  error->message);
+        g_free(output_file_path);
+        g_error_free(error);
+    }
+
+    /* print_statistics_for_files can handle the NULL
+     * case just fine, so there's no need to return if
+     * output_file is NULL */
+    StatisticsPrintUserData data = {
+        priv->reflection_context,
+        ostream,
+        output_directory
+    };
+
+    g_hash_table_foreach(priv->file_statistics,
+                         print_statistics_for_files,
+                         &data);
+
+    g_object_unref(ostream);
+    g_object_unref(output_file);
+
+    /* Re-insert our new script hook in case we need it again */
+    priv->new_scripts_connection =
+        gjs_debug_hooks_add_script_load_hook(priv->debug_hooks,
+                                             gjs_coverage_new_script_available_hook,
+                                             coverage);
+}
+
+static void
+destroy_coverage_statistics_if_if_nonnull(gpointer statistics)
+{
+    if (statistics)
+        gjs_coverage_file_statistics_destroy(statistics);
+}
+
+static void
+gjs_coverage_init(GjsCoverage *self)
+{
+    GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(self);
+
+    priv->reflection_context =  gjs_reflected_script_create_reflection_context();
+    priv->file_statistics = g_hash_table_new_full(g_str_hash,
+                                                  g_str_equal,
+                                                  g_free,
+                                                  destroy_coverage_statistics_if_if_nonnull);
+    priv->active_branch = NULL;
+}
+
+static void
+gjs_coverage_constructed(GObject *object)
+{
+    G_OBJECT_CLASS(gjs_coverage_parent_class)->constructed(object);
+
+    GjsCoverage *coverage = GJS_DEBUG_COVERAGE(object);
+    GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
+
+    /* Take the list of covered paths and add them to the coverage report */
+    if (priv->covered_paths) {
+        const char **iterator = (const char **) priv->covered_paths;
+
+        do {
+            /* At the moment we just  a key with no value to the
+             * filename statistics. We'll create a proper source file
+             * map once we get a new script callback (to avoid lots
+             * of recompiling) and also create a source map on
+             * coverage data generation if we didn't already have one */
+            g_hash_table_insert(priv->file_statistics,
+                                g_strdup((*iterator)),
+                                NULL);
+        } while (*(++iterator));
+    }
+
+    /* Add hook for new scripts and singlestep execution */
+    priv->new_scripts_connection =
+        gjs_debug_hooks_add_script_load_hook(priv->debug_hooks,
+                                             gjs_coverage_new_script_available_hook,
+                                             coverage);
+
+    priv->single_step_connection =
+        gjs_debug_hooks_add_singlestep_hook(priv->debug_hooks,
+                                            gjs_coverage_single_step_interrupt_hook,
+                                            coverage);
+
+    priv->frame_step_connection =
+        gjs_debug_hooks_add_frame_step_hook(priv->debug_hooks,
+                                            gjs_coverage_frame_execution_hook,
+                                            priv->file_statistics);
+}
+
+static void
+gjs_coverage_set_property(GObject      *object,
+                          unsigned int  prop_id,
+                          const GValue *value,
+                          GParamSpec   *pspec)
+{
+    GjsCoverage *coverage = GJS_DEBUG_COVERAGE(object);
+    GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
+    switch (prop_id) {
+    case PROP_DEBUG_HOOKS:
+        priv->debug_hooks = GJS_DEBUG_HOOKS(g_value_dup_object(value));
+        break;
+    case PROP_COVERAGE_PATHS:
+        g_assert(priv->covered_paths == NULL);
+        priv->covered_paths = (char **) g_value_dup_boxed (value);
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+typedef void (*HookRemovalFunc) (GjsDebugHooks *, guint);
+
+static void
+clear_debug_handle(GjsDebugHooks   *hooks,
+                   HookRemovalFunc  remove,
+                   unsigned int    *handle)
+{
+    if (*handle) {
+        remove(hooks, *handle);
+        *handle = 0;
+    }
+}
+
+static void
+gjs_coverage_dispose(GObject *object)
+{
+    GjsCoverage *coverage = GJS_DEBUG_COVERAGE (object);
+    GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
+
+    clear_debug_handle(priv->debug_hooks, gjs_debug_hooks_remove_script_load_hook, 
&priv->new_scripts_connection);
+    clear_debug_handle(priv->debug_hooks, gjs_debug_hooks_remove_singlestep_hook, 
&priv->single_step_connection);
+    clear_debug_handle(priv->debug_hooks, gjs_debug_hooks_remove_frame_step_hook, 
&priv->frame_step_connection);
+
+    g_clear_object(&priv->debug_hooks);
+    g_clear_object(&priv->reflection_context);
+
+    G_OBJECT_CLASS(gjs_coverage_parent_class)->dispose(object);
+}
+
+static void
+gjs_coverage_finalize (GObject *object)
+{
+    GjsCoverage *coverage = GJS_DEBUG_COVERAGE(object);
+    GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
+
+    g_hash_table_unref(priv->file_statistics);
+    g_strfreev(priv->covered_paths);
+
+    G_OBJECT_CLASS(gjs_coverage_parent_class)->finalize(object);
+}
+
+static void
+gjs_coverage_class_init (GjsCoverageClass *klass)
+{
+    GObjectClass *object_class = (GObjectClass *) klass;
+
+    object_class->constructed = gjs_coverage_constructed;
+    object_class->dispose = gjs_coverage_dispose;
+    object_class->finalize = gjs_coverage_finalize;
+    object_class->set_property = gjs_coverage_set_property;
+
+    properties[PROP_DEBUG_HOOKS] = g_param_spec_object("debug-hooks",
+                                                       "Debug Hooks",
+                                                       "Debug Hooks",
+                                                       GJS_TYPE_DEBUG_HOOKS,
+                                                       (GParamFlags) (G_PARAM_CONSTRUCT_ONLY | 
G_PARAM_WRITABLE));
+    properties[PROP_COVERAGE_PATHS] = g_param_spec_boxed("coverage-paths",
+                                                         "Coverage Paths",
+                                                         "Paths (and included subdirectories) of which to 
perform coverage analysis",
+                                                         G_TYPE_STRV,
+                                                         (GParamFlags) (G_PARAM_CONSTRUCT_ONLY | 
G_PARAM_WRITABLE));
+
+    g_object_class_install_properties(object_class,
+                                      PROP_N,
+                                      properties);
+}
+
+/**
+ * gjs_coverage_new:
+ * @debug_hooks: (transfer full): A #GjsDebugHooks to register callbacks on.
+ * @coverage_paths: (transfer none): A null-terminated strv of directories to generate
+ * coverage_data for
+ *
+ * Returns: A #GjsDebugCoverage
+ */
+GjsCoverage *
+gjs_coverage_new (GjsDebugHooks *debug_hooks,
+                  const char    **coverage_paths)
+{
+    GjsCoverage *coverage =
+        GJS_DEBUG_COVERAGE(g_object_new(GJS_TYPE_DEBUG_COVERAGE,
+                                        "debug-hooks", debug_hooks,
+                                        "coverage-paths", coverage_paths,
+                                        NULL));
+
+    return coverage;
+}
diff --git a/gjs/coverage.h b/gjs/coverage.h
new file mode 100644
index 0000000..2c872ea
--- /dev/null
+++ b/gjs/coverage.h
@@ -0,0 +1,86 @@
+/*
+ * Copyright © 2014 Endless Mobile, Inc.
+ *
+ * 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 2 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, write to the Free Software Foundation,
+ * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Authored By: Sam Spilsbury <sam endlessm com>
+ */
+#ifndef _GJS_DEBUG_COVERAGE_H
+#define _GJS_DEBUG_COVERAGE_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GJS_TYPE_DEBUG_COVERAGE gjs_coverage_get_type()
+
+#define GJS_DEBUG_COVERAGE(obj) \
+    (G_TYPE_CHECK_INSTANCE_CAST((obj), \
+     GJS_TYPE_DEBUG_COVERAGE, GjsCoverage))
+
+#define GJS_DEBUG_COVERAGE_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_CAST((klass), \
+     GJS_TYPE_DEBUG_COVERAGE, GjsCoverageClass))
+
+#define GJS_IS_DEBUG_COVERAGE(obj) \
+    (G_TYPE_CHECK_INSTANCE_TYPE((obj), \
+     GJS_TYPE_DEBUG_COVERAGE))
+
+#define GJS_IS_DEBUG_COVERAGE_CLASS(klass) \
+    (G_TYPE_CHECK_CLASS_TYPE ((klass), \
+     GJS_TYPE_DEBUG_COVERAGE))
+
+#define GJS_DEBUG_COVERAGE_GET_CLASS(obj) \
+    (G_TYPE_INSTANCE_GET_CLASS ((obj), \
+     GJS_TYPE_DEBUG_COVERAGE, GjsCoverageClass))
+
+typedef struct _GFile GFile;
+typedef struct _GjsDebugHooks GjsDebugHooks;
+typedef struct _GjsContext GjsContext;
+
+typedef struct _GjsCoverage GjsCoverage;
+typedef struct _GjsCoverageClass GjsCoverageClass;
+typedef struct _GjsCoveragePrivate GjsCoveragePrivate;
+
+struct _GjsCoverage {
+    GObject parent;
+};
+
+struct _GjsCoverageClass {
+    GObjectClass parent_class;
+};
+
+GType gjs_debug_coverage_get_type(void);
+
+/**
+ * gjs_debug_coverage_write_statistics:
+ * @coverage: A #GjsDebugCoverage
+ * @output_file (allow-none): A #GFile to write statistics to. If NULL is provided then coverage data
+ * will be written to files in the form of (filename).info in the same directory as the input file
+ *
+ * This function takes all available statistics and writes them out to either the file provided
+ * or to files of the pattern (filename).info in the same directory as the scanned files. It will
+ * provide coverage data for all files ending with ".js" in the coverage directories, even if they
+ * were never actually executed.
+ */
+void gjs_coverage_write_statistics(GjsCoverage *coverage,
+                                   const char  *output_directory);
+
+GjsCoverage * gjs_coverage_new(GjsDebugHooks *debug_hooks,
+                               const char    **covered_directories);
+
+G_END_DECLS
+
+#endif
diff --git a/test/gjs-test-coverage.cpp b/test/gjs-test-coverage.cpp
new file mode 100644
index 0000000..465ac4e
--- /dev/null
+++ b/test/gjs-test-coverage.cpp
@@ -0,0 +1,1325 @@
+/*
+ * Copyright © 2014 Endless Mobile, Inc.
+ *
+ * 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 2 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, write to the Free Software Foundation,
+ * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Authored By: Sam Spilsbury <sam endlessm com>
+ */
+
+#include <errno.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <sys/types.h>
+#include <fcntl.h>
+#include <ftw.h>
+
+#include <glib.h>
+#include <gio/gio.h>
+#include <gjs/gjs.h>
+#include <gjs/debug-hooks.h>
+#include <gjs/debug-hooks-private.h>
+#include <gjs/reflected-script-private.h>
+#include <gjs/coverage.h>
+
+typedef struct _GjsCoverageFixture {
+    GjsContext    *context;
+    GjsDebugHooks *debug_hooks;
+    GjsCoverage   *coverage;
+    char          *temporary_js_script_directory_name;
+    char          *temporary_js_script_filename;
+    int           temporary_js_script_open_handle;
+} GjsCoverageFixture;
+
+static void
+write_to_file(int        handle,
+              const char *contents)
+{
+    if (write(handle,
+              (gconstpointer) contents,
+              sizeof(char) * strlen(contents)) == -1)
+        g_error("Failed to write %s to file", contents);
+}
+
+static void
+write_to_file_at_beginning(int        handle,
+                           const char *content)
+{
+    if (ftruncate(handle, 0) == -1)
+        g_print("Error deleting contents of test temporary file: %s\n", strerror(errno));
+    lseek(handle, 0, SEEK_SET);
+    write_to_file(handle, content);
+}
+
+static int
+unlink_if_node_is_a_file(const char *path, const struct stat *sb, int typeflag)
+{
+    if (typeflag & FTW_F)
+        unlink(path);
+
+    return 0;
+}
+
+static int
+rmdir_if_node_is_a_dir(const char *path, const struct stat *sb, int typeflag)
+{
+    if (typeflag & FTW_D)
+        rmdir(path);
+
+    return 0;
+}
+
+static void
+recursive_delete_dir_at_path(const char *path)
+{
+    /* We have to recurse twice - once to delete files, and once
+     * to delete directories (because ftw uses preorder traversal) */
+    ftw(path, unlink_if_node_is_a_file, 100);
+    ftw(path, rmdir_if_node_is_a_dir, 100);
+}
+
+static void
+gjs_coverage_fixture_set_up(gpointer      fixture_data,
+                            gconstpointer user_data)
+{
+    GjsCoverageFixture *fixture = (GjsCoverageFixture *) fixture_data;
+    const char         *js_script = "function f () { return 1; }\n";
+
+    fixture->temporary_js_script_directory_name = g_strdup("/tmp/gjs_coverage_tmp.XXXXXX");
+    fixture->temporary_js_script_directory_name =
+        mkdtemp (fixture->temporary_js_script_directory_name);
+
+    if (!fixture->temporary_js_script_directory_name)
+        g_error ("Failed to create temporary directory for test files: %s\n", strerror (errno));
+
+    fixture->temporary_js_script_filename = g_strconcat(fixture->temporary_js_script_directory_name,
+                                                        "/",
+                                                        "gjs_coverage_script_XXXXXX.js",
+                                                        NULL);
+    fixture->temporary_js_script_open_handle =
+        mkstemps(fixture->temporary_js_script_filename, 3);
+
+    /* Allocate a strv that we can pass over to gjs_coverage_new */
+    const char *coverage_paths[] = {
+        fixture->temporary_js_script_filename,
+        NULL
+    };
+
+    const char *search_paths[] = {
+        fixture->temporary_js_script_directory_name,
+        NULL
+    };
+
+    fixture->context = gjs_context_new_with_search_path((char **) search_paths);
+    fixture->debug_hooks = gjs_debug_hooks_new (fixture->context);
+    fixture->coverage = gjs_coverage_new(fixture->debug_hooks,
+                                         coverage_paths);
+
+    write_to_file(fixture->temporary_js_script_open_handle, js_script);
+}
+
+static void
+gjs_coverage_fixture_tear_down(gpointer      fixture_data,
+                               gconstpointer user_data)
+{
+    GjsCoverageFixture *fixture = (GjsCoverageFixture *) fixture_data;
+    unlink(fixture->temporary_js_script_filename);
+    g_free(fixture->temporary_js_script_filename);
+    close(fixture->temporary_js_script_open_handle);
+    recursive_delete_dir_at_path(fixture->temporary_js_script_directory_name);
+    g_free(fixture->temporary_js_script_directory_name);
+
+    g_object_unref(fixture->coverage);
+    g_object_unref(fixture->debug_hooks);
+    g_object_unref(fixture->context);
+}
+
+typedef struct _GjsCoverageToSingleOutputFileFixture {
+    GjsCoverageFixture base_fixture;
+    char         *output_file_directory;
+    char         *output_file_name;
+    unsigned int output_file_handle;
+} GjsCoverageToSingleOutputFileFixture;
+
+static void
+gjs_coverage_to_single_output_file_fixture_set_up (gpointer      fixture_data,
+                                                   gconstpointer user_data)
+{
+    gjs_coverage_fixture_set_up (fixture_data, user_data);
+
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+    fixture->output_file_directory = 
g_build_filename(fixture->base_fixture.temporary_js_script_directory_name,
+                                                      "gjs_coverage_test_coverage.XXXXXX",
+                                                      NULL);
+    fixture->output_file_directory = mkdtemp(fixture->output_file_directory);
+    fixture->output_file_name = g_build_filename(fixture->output_file_directory,
+                                                 "coverage.lcov",
+                                                 NULL);
+    fixture->output_file_handle = open(fixture->output_file_name,
+                                       O_CREAT | O_CLOEXEC | O_RDWR,
+                                       S_IRWXU);
+}
+
+static void
+gjs_coverage_to_single_output_file_fixture_tear_down (gpointer      fixture_data,
+                                                      gconstpointer user_data)
+{
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+    unlink(fixture->output_file_name);
+    close(fixture->output_file_handle);
+    g_free(fixture->output_file_name);
+    recursive_delete_dir_at_path(fixture->output_file_directory);
+    g_free(fixture->output_file_directory);
+
+    gjs_coverage_fixture_tear_down(fixture_data, user_data);
+}
+
+static const char *
+line_starting_with(const char *data,
+                   const char *needle)
+{
+    const gsize needle_length = strlen (needle);
+    const char  *iter = data;
+
+    while (iter) {
+        if (strncmp (iter, needle, needle_length) == 0)
+          return iter;
+
+        iter = strstr (iter, "\n");
+
+        if (iter)
+          iter += 1;
+    }
+
+    return NULL;
+}
+
+static char *
+write_statistics_and_get_coverage_data(GjsCoverage *coverage,
+                                       const char  *filename,
+                                       const char  *output_directory,
+                                       gsize       *coverage_data_length_return)
+{
+    gjs_coverage_write_statistics(coverage, output_directory);
+
+    gsize coverage_data_length;
+    char  *coverage_data_contents;
+
+    char  *output_filename = g_build_filename(output_directory,
+                                              "coverage.lcov",
+                                              NULL);
+
+    g_file_get_contents(output_filename,
+                        &coverage_data_contents,
+                        &coverage_data_length,
+                        NULL);
+
+    g_free(output_filename);
+
+    if (coverage_data_length_return)
+      *coverage_data_length_return = coverage_data_length;
+
+    return coverage_data_contents;
+}
+
+static char *
+eval_script_and_get_coverage_data(GjsContext  *context,
+                                  GjsCoverage *coverage,
+                                  const char  *filename,
+                                  const char  *output_directory,
+                                  gsize       *coverage_data_length_return)
+{
+    gjs_context_eval_file(context,
+                          filename,
+                          NULL,
+                          NULL);
+
+    return write_statistics_and_get_coverage_data(coverage,
+                                                  filename,
+                                                  output_directory,
+                                                  coverage_data_length_return);
+}
+
+static gboolean
+coverage_data_contains_value_for_key(const char *data,
+                                     const char *key,
+                                     const char *value)
+{
+    const char *sf_line = line_starting_with(data, key);
+
+    if (!sf_line)
+        return FALSE;
+
+    return strncmp(&sf_line[strlen (key)],
+                   value,
+                   strlen (value)) == 0;
+}
+
+typedef gboolean (*CoverageDataMatchFunc) (const char *value,
+                                           gpointer    user_data);
+
+static gboolean
+coverage_data_matches_value_for_key_internal(const char            *line,
+                                             const char            *key,
+                                             CoverageDataMatchFunc  match,
+                                             gpointer               user_data)
+{
+    return (*match) (line, user_data);
+}
+
+static gboolean
+coverage_data_matches_value_for_key(const char            *data,
+                                    const char            *key,
+                                    CoverageDataMatchFunc  match,
+                                    gpointer               user_data)
+{
+    const char *line = line_starting_with(data, key);
+
+    if (!line)
+        return FALSE;
+
+    return coverage_data_matches_value_for_key_internal(line, key, match, user_data);
+}
+
+static gboolean
+coverage_data_matches_any_value_for_key(const char            *data,
+                                        const char            *key,
+                                        CoverageDataMatchFunc  match,
+                                        gpointer               user_data)
+{
+    data = line_starting_with(data, key);
+
+    while (data) {
+        if (coverage_data_matches_value_for_key_internal(data, key, match, user_data))
+            return TRUE;
+
+        data = line_starting_with(data + 1, key);
+    }
+
+    return FALSE;
+}
+
+static gboolean
+coverage_data_matches_values_for_key(const char            *data,
+                                     const char            *key,
+                                     gsize                  n,
+                                     CoverageDataMatchFunc  match,
+                                     gpointer               user_data,
+                                     gsize                  data_size)
+{
+    const char *line = line_starting_with (data, key);
+    /* Keep matching. If we fail to match one of them then
+     * bail out */
+    char *data_iterator = (char *) user_data;
+
+    while (line && n > 0) {
+        if (!coverage_data_matches_value_for_key_internal (line, key, match, (gpointer) data_iterator))
+            return FALSE;
+
+        line = line_starting_with (line + 1, key);
+        --n;
+        data_iterator += data_size;
+    }
+
+    /* If n is zero then we've found all available matches */
+    if (n == 0)
+        return TRUE;
+
+    return FALSE;
+}
+
+static void
+test_covered_file_is_duplicated_into_output_if_resource(gpointer      fixture_data,
+                                                        gconstpointer user_data)
+{
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+
+    const char *mock_resource_filename = 
"resource:///org/gnome/gjs/mock/test/gjs-test-coverage/loadedJSFromResource.js";
+    const char *coverage_scripts[] = {
+        mock_resource_filename,
+        NULL
+    };
+
+    g_object_unref(fixture->base_fixture.coverage);
+    fixture->base_fixture.coverage =
+        gjs_coverage_new(fixture->base_fixture.debug_hooks,
+                         coverage_scripts);
+
+    gjs_context_eval_file(fixture->base_fixture.context,
+                          mock_resource_filename,
+                          NULL,
+                          NULL);
+
+    gjs_coverage_write_statistics(fixture->base_fixture.coverage,
+                                  fixture->output_file_directory);
+
+    char *expected_temporary_js_script_file_path =
+        g_build_filename(fixture->output_file_directory,
+                         "org/gnome/gjs/mock/test/gjs-test-coverage/loadedJSFromResource.js",
+                         NULL);
+
+    GFile *file_for_expected_path = g_file_new_for_path(expected_temporary_js_script_file_path);
+
+    g_assert(g_file_query_exists(file_for_expected_path, NULL) == TRUE);
+
+    g_object_unref(file_for_expected_path);
+    g_free(expected_temporary_js_script_file_path);
+}
+
+
+static void
+test_covered_file_is_duplicated_into_output_if_path(gpointer      fixture_data,
+                                                    gconstpointer user_data)
+{
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+
+    gjs_context_eval_file(fixture->base_fixture.context,
+                          fixture->base_fixture.temporary_js_script_filename,
+                          NULL,
+                          NULL);
+
+    gjs_coverage_write_statistics(fixture->base_fixture.coverage,
+                                  fixture->output_file_directory);
+
+    char *temporary_js_script_basename =
+        g_filename_display_basename(fixture->base_fixture.temporary_js_script_filename);
+    char *expected_temporary_js_script_file_path =
+        g_build_filename(fixture->output_file_directory,
+                         temporary_js_script_basename,
+                         NULL);
+
+    GFile *file_for_expected_path = g_file_new_for_path(expected_temporary_js_script_file_path);
+
+    g_assert(g_file_query_exists(file_for_expected_path, NULL) == TRUE);
+
+    g_object_unref(file_for_expected_path);
+    g_free(expected_temporary_js_script_file_path);
+    g_free(temporary_js_script_basename);
+}
+
+static void
+test_previous_contents_preserved(gpointer      fixture_data,
+                                 gconstpointer user_data)
+{
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+    const char *existing_contents = "existing_contents\n";
+    write_to_file(fixture->output_file_handle, existing_contents);
+
+    char *coverage_data_contents =
+        eval_script_and_get_coverage_data(fixture->base_fixture.context,
+                                          fixture->base_fixture.coverage,
+                                          fixture->base_fixture.temporary_js_script_filename,
+                                          fixture->output_file_directory,
+                                          NULL);
+
+    g_assert(strstr(coverage_data_contents, existing_contents) != NULL);
+    g_free(coverage_data_contents);
+}
+
+
+static void
+test_new_contents_written(gpointer      fixture_data,
+                          gconstpointer user_data)
+{
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+    const char *existing_contents = "existing_contents\n";
+    write_to_file(fixture->output_file_handle, existing_contents);
+
+    char *coverage_data_contents =
+        eval_script_and_get_coverage_data(fixture->base_fixture.context,
+                                          fixture->base_fixture.coverage,
+                                          fixture->base_fixture.temporary_js_script_filename,
+                                          fixture->output_file_directory,
+                                          NULL);
+
+    /* We have new content in the coverage data */
+    g_assert(strlen(existing_contents) != strlen(coverage_data_contents));
+    g_free(coverage_data_contents);
+}
+
+static void
+test_expected_source_file_name_written_to_coverage_data(gpointer      fixture_data,
+                                                        gconstpointer user_data)
+{
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+
+    char *coverage_data_contents =
+        eval_script_and_get_coverage_data(fixture->base_fixture.context,
+                                          fixture->base_fixture.coverage,
+                                          fixture->base_fixture.temporary_js_script_filename,
+                                          fixture->output_file_directory,
+                                          NULL);
+
+    char *temporary_js_script_basename =
+        g_filename_display_basename(fixture->base_fixture.temporary_js_script_filename);
+    char *expected_source_filename =
+        g_build_filename(fixture->output_file_directory,
+                         temporary_js_script_basename,
+                         NULL);
+
+    g_assert(coverage_data_contains_value_for_key(coverage_data_contents,
+                                                  "SF:",
+                                                  expected_source_filename));
+
+    g_free(expected_source_filename);
+    g_free(temporary_js_script_basename);
+    g_free(coverage_data_contents);
+}
+
+typedef enum _BranchTaken {
+    NOT_EXECUTED,
+    NOT_TAKEN,
+    TAKEN
+} BranchTaken;
+
+typedef struct _BranchLineData {
+    int         expected_branch_line;
+    int         expected_id;
+    BranchTaken taken;
+} BranchLineData;
+
+static gboolean
+branch_at_line_should_be_taken(const char *line,
+                               gpointer user_data)
+{
+    BranchLineData *branch_data = (BranchLineData *) user_data;
+    int line_no, branch_id, block_no, hit_count_num;
+    char *hit_count = NULL;
+
+    /* Advance past "BRDA:" */
+    line += 5;
+
+    if (sscanf(line, "%i,%i,%i,%as", &line_no, &block_no, &branch_id, &hit_count) != 4)
+        g_error("sscanf: %s", strerror(errno));
+
+    /* Determine the branch hit count. It will be either:
+     * > -1 if the line containing the branch was never executed, or
+     * > N times the branch was taken.
+     *
+     * The value of -1 is represented by a single "-" character, so
+     * we should detect this case and set the value based on that */
+    if (strlen(hit_count) == 1 && *hit_count == '-')
+        hit_count_num = -1;
+    else
+        hit_count_num = atoi(hit_count);
+
+    /* The glibc extension to sscanf dynamically allocates hit_count, so
+     * we need to free it here */
+    free(hit_count);
+
+    const gboolean hit_correct_branch_line =
+        branch_data->expected_branch_line == line_no;
+    const gboolean hit_correct_branch_id =
+        branch_data->expected_id == branch_id;
+    gboolean branch_correctly_taken_or_not_taken;
+
+    switch (branch_data->taken) {
+    case NOT_EXECUTED:
+        branch_correctly_taken_or_not_taken = hit_count_num == -1;
+        break;
+    case NOT_TAKEN:
+        branch_correctly_taken_or_not_taken = hit_count_num == 0;
+        break;
+    case TAKEN:
+        branch_correctly_taken_or_not_taken = hit_count_num > 0;
+        break;
+    default:
+        g_assert_not_reached();
+    };
+
+    return hit_correct_branch_line &&
+           hit_correct_branch_id &&
+           branch_correctly_taken_or_not_taken;
+
+}
+
+static void
+test_single_branch_coverage_written_to_coverage_data(gpointer      fixture_data,
+                                                     gconstpointer user_data)
+{
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+
+    const char *script_with_basic_branch =
+            "let x = 0;\n"
+            "if (x > 0)\n"
+            "    x++;\n"
+            "else\n"
+            "    x++;\n";
+
+    /* We have to seek backwards and overwrite */
+    lseek(fixture->base_fixture.temporary_js_script_open_handle, 0, SEEK_SET);
+
+    if (write(fixture->base_fixture.temporary_js_script_open_handle,
+              (const char *) script_with_basic_branch,
+              sizeof(char) * strlen(script_with_basic_branch)) == 0)
+        g_error("Failed to basic branch script");
+
+    char *coverage_data_contents =
+        eval_script_and_get_coverage_data(fixture->base_fixture.context,
+                                          fixture->base_fixture.coverage,
+                                          fixture->base_fixture.temporary_js_script_filename,
+                                          fixture->output_file_directory,
+                                          NULL);
+
+    const BranchLineData expected_branches[] = {
+        { 2, 0, NOT_TAKEN },
+        { 2, 1, TAKEN }
+    };
+    const gsize expected_branches_len = G_N_ELEMENTS(expected_branches);
+
+    /* There are two possible branches here, the second should be taken
+     * and the first should not have been */
+    g_assert(coverage_data_matches_values_for_key(coverage_data_contents,
+                                                  "BRDA:",
+                                                  expected_branches_len,
+                                                  branch_at_line_should_be_taken,
+                                                  (gpointer) expected_branches,
+                                                  sizeof(BranchLineData)));
+
+    g_assert(coverage_data_contains_value_for_key(coverage_data_contents,
+                                                  "BRF:",
+                                                  "2"));
+    g_assert(coverage_data_contains_value_for_key(coverage_data_contents,
+                                                  "BRH:",
+                                                  "1"));
+    g_free(coverage_data_contents);
+}
+
+static void
+test_multiple_branch_coverage_written_to_coverage_data(gpointer      fixture_data,
+                                                       gconstpointer user_data)
+{
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+
+    const char *script_with_case_statements_branch =
+            "let y;\n"
+            "for (let x = 0; x < 3; x++) {\n"
+            "    switch (x) {\n"
+            "    case 0:\n"
+            "        y = x + 1;\n"
+            "        break;\n"
+            "    case 1:\n"
+            "        y = x + 1;\n"
+            "        break;\n"
+            "    case 2:\n"
+            "        y = x + 1;\n"
+            "        break;\n"
+            "    }\n"
+            "}\n";
+
+    /* We have to seek backwards and overwrite */
+    lseek(fixture->base_fixture.temporary_js_script_open_handle, 0, SEEK_SET);
+
+    if (write(fixture->base_fixture.temporary_js_script_open_handle,
+              (const char *) script_with_case_statements_branch,
+              sizeof(char) * strlen(script_with_case_statements_branch)) == 0)
+        g_error("Failed to write script");
+
+    char *coverage_data_contents =
+        eval_script_and_get_coverage_data(fixture->base_fixture.context,
+                                          fixture->base_fixture.coverage,
+                                          fixture->base_fixture.temporary_js_script_filename,
+                                          fixture->output_file_directory,
+                                          NULL);
+
+    const BranchLineData expected_branches[] = {
+        { 3, 0, TAKEN },
+        { 3, 1, TAKEN },
+        { 3, 2, TAKEN }
+    };
+    const gsize expected_branches_len = G_N_ELEMENTS(expected_branches);
+
+    /* There are two possible branches here, the second should be taken
+     * and the first should not have been */
+    g_assert(coverage_data_matches_values_for_key(coverage_data_contents,
+                                                  "BRDA:",
+                                                  expected_branches_len,
+                                                  branch_at_line_should_be_taken,
+                                                  (gpointer) expected_branches,
+                                                  sizeof(BranchLineData)));
+    g_free(coverage_data_contents);
+}
+
+static void
+test_branches_for_multiple_case_statements_fallthrough(gpointer      fixture_data,
+                                                       gconstpointer user_data)
+{
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+
+    const char *script_with_case_statements_branch =
+            "let y;\n"
+            "for (let x = 0; x < 3; x++) {\n"
+            "    switch (x) {\n"
+            "    case 0:\n"
+            "    case 1:\n"
+            "        y = x + 1;\n"
+            "        break;\n"
+            "    case 2:\n"
+            "        y = x + 1;\n"
+            "        break;\n"
+            "    case 3:\n"
+            "        y = x +1;\n"
+            "        break;\n"
+            "    }\n"
+            "}\n";
+
+    /* We have to seek backwards and overwrite */
+    lseek(fixture->base_fixture.temporary_js_script_open_handle, 0, SEEK_SET);
+
+    if (write(fixture->base_fixture.temporary_js_script_open_handle,
+              (const char *) script_with_case_statements_branch,
+              sizeof(char) * strlen(script_with_case_statements_branch)) == 0)
+        g_error("Failed to write script");
+
+    char *coverage_data_contents =
+        eval_script_and_get_coverage_data(fixture->base_fixture.context,
+                                          fixture->base_fixture.coverage,
+                                          fixture->base_fixture.temporary_js_script_filename,
+                                          fixture->output_file_directory,
+                                          NULL);
+
+    const BranchLineData expected_branches[] = {
+        { 3, 0, TAKEN },
+        { 3, 1, TAKEN },
+        { 3, 2, NOT_TAKEN }
+    };
+    const gsize expected_branches_len = G_N_ELEMENTS(expected_branches);
+
+    /* There are two possible branches here, the second should be taken
+     * and the first should not have been */
+    g_assert(coverage_data_matches_values_for_key(coverage_data_contents,
+                                                  "BRDA:",
+                                                  expected_branches_len,
+                                                  branch_at_line_should_be_taken,
+                                                  (gpointer) expected_branches,
+                                                  sizeof(BranchLineData)));
+    g_free(coverage_data_contents);
+}
+
+static void
+test_branch_not_hit_written_to_coverage_data(gpointer      fixture_data,
+                                             gconstpointer user_data)
+{
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+
+    const char *script_with_never_executed_branch =
+            "let x = 0;\n"
+            "if (x > 0) {\n"
+            "    if (x > 0)\n"
+            "        x++;\n"
+            "} else {\n"
+            "    x++;\n"
+            "}\n";
+
+    write_to_file_at_beginning(fixture->base_fixture.temporary_js_script_open_handle,
+                               script_with_never_executed_branch);
+
+    char *coverage_data_contents =
+        eval_script_and_get_coverage_data(fixture->base_fixture.context,
+                                          fixture->base_fixture.coverage,
+                                          fixture->base_fixture.temporary_js_script_filename,
+                                          fixture->output_file_directory,
+                                          NULL);
+
+    const BranchLineData expected_branch = {
+        3, 0, NOT_EXECUTED
+    };
+
+    g_assert(coverage_data_matches_any_value_for_key(coverage_data_contents,
+                                                     "BRDA:",
+                                                     branch_at_line_should_be_taken,
+                                                     (gpointer) &expected_branch));
+    g_free(coverage_data_contents);
+}
+
+static gboolean
+has_function_name(const char *line,
+                  gpointer    user_data)
+{
+    /* User data is const char ** */
+    const char *expected_function_name = *((const char **) user_data);
+
+    /* Advance past "FN:" */
+    line += 3;
+
+    return strncmp(line,
+                   expected_function_name,
+                   strlen(expected_function_name)) == 0;
+}
+
+static void
+test_function_names_written_to_coverage_data(gpointer      fixture_data,
+                                             gconstpointer user_data)
+{
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+
+    const char *script_with_named_and_unnamed_functions =
+            "function f(){}\n"
+            "let b = function(){}\n";
+
+    write_to_file_at_beginning(fixture->base_fixture.temporary_js_script_open_handle,
+                               script_with_named_and_unnamed_functions);
+
+    char *coverage_data_contents =
+        eval_script_and_get_coverage_data(fixture->base_fixture.context,
+                                          fixture->base_fixture.coverage,
+                                          fixture->base_fixture.temporary_js_script_filename,
+                                          fixture->output_file_directory,
+                                          NULL);
+
+    /* The internal hash table is sorted in alphabetical order
+     * so the function names need to be in this order too */
+    const char * expected_function_names[] = {
+        "(anonymous):2:0",
+        "f:1:0"
+    };
+    const gsize expected_function_names_len = G_N_ELEMENTS(expected_function_names);
+
+    /* There are two possible branches here, the second should be taken
+     * and the first should not have been */
+    g_assert(coverage_data_matches_values_for_key(coverage_data_contents,
+                                                  "FN:",
+                                                  expected_function_names_len,
+                                                  has_function_name,
+                                                  (gpointer) expected_function_names,
+                                                  sizeof(const char *)));
+    g_free(coverage_data_contents);
+}
+
+typedef struct _FunctionHitCountData {
+    const char   *function;
+    unsigned int hit_count_minimum;
+} FunctionHitCountData;
+
+static gboolean
+hit_count_is_more_than_for_function(const char *line,
+                                    gpointer   user_data)
+{
+    FunctionHitCountData *data = (FunctionHitCountData *) user_data;
+    char                 *detected_function = NULL;
+    unsigned int         hit_count;
+
+
+    /* Advance past "FNDA:" */
+    line += 5;
+
+    if (sscanf(line, "%i,%as", &hit_count, &detected_function) != 2)
+        g_error("sscanf: %s", strerror(errno));
+
+    const gboolean function_name_match = g_strcmp0(data->function, detected_function) == 0;
+    const gboolean hit_count_more_than = hit_count >= data->hit_count_minimum;
+
+    /* See above, we must free detected_functon */
+    free(detected_function);
+
+    return function_name_match &&
+           hit_count_more_than;
+}
+
+static void
+test_function_hit_counts_written_to_coverage_data(gpointer      fixture_data,
+                                                  gconstpointer user_data)
+{
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+
+    const char *script_with_executed_functions =
+            "function f(){}\n"
+            "let b = function(){}\n"
+            "f();\n"
+            "b();\n";
+
+    write_to_file_at_beginning(fixture->base_fixture.temporary_js_script_open_handle,
+                               script_with_executed_functions);
+
+    char *coverage_data_contents =
+        eval_script_and_get_coverage_data(fixture->base_fixture.context,
+                                          fixture->base_fixture.coverage,
+                                          fixture->base_fixture.temporary_js_script_filename,
+                                          fixture->output_file_directory,
+                                          NULL);
+
+    /* The internal hash table is sorted in alphabetical order
+     * so the function names need to be in this order too */
+    FunctionHitCountData expected_hit_counts[] = {
+        { "(anonymous):2:0", 1 },
+        { "f:1:0", 1 }
+    };
+
+    const gsize expected_hit_count_len = G_N_ELEMENTS(expected_hit_counts);
+
+    /* There are two possible branches here, the second should be taken
+     * and the first should not have been */
+    g_assert(coverage_data_matches_values_for_key(coverage_data_contents,
+                                                  "FNDA:",
+                                                  expected_hit_count_len,
+                                                  hit_count_is_more_than_for_function,
+                                                  (gpointer) expected_hit_counts,
+                                                  sizeof(FunctionHitCountData)));
+
+    g_free(coverage_data_contents);
+}
+
+static void
+test_total_function_coverage_written_to_coverage_data(gpointer      fixture_data,
+                                                      gconstpointer user_data)
+{
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+
+    const char *script_with_some_executed_functions =
+            "function f(){}\n"
+            "let b = function(){}\n"
+            "f();\n";
+
+    write_to_file_at_beginning(fixture->base_fixture.temporary_js_script_open_handle,
+                               script_with_some_executed_functions);
+
+    char *coverage_data_contents =
+        eval_script_and_get_coverage_data(fixture->base_fixture.context,
+                                          fixture->base_fixture.coverage,
+                                          fixture->base_fixture.temporary_js_script_filename,
+                                          fixture->output_file_directory,
+                                          NULL);
+
+    /* More than one assert per test is bad, but we are testing interlinked concepts */
+    g_assert(coverage_data_contains_value_for_key(coverage_data_contents,
+                                                  "FNF:",
+                                                  "2"));
+    g_assert(coverage_data_contains_value_for_key(coverage_data_contents,
+                                                  "FNH:",
+                                                  "1"));
+    g_free(coverage_data_contents);
+}
+
+typedef struct _LineCountIsMoreThanData {
+    unsigned int expected_lineno;
+    unsigned int expected_to_be_more_than;
+} LineCountIsMoreThanData;
+
+static gboolean
+line_hit_count_is_more_than(const char *line,
+                            gpointer    user_data)
+{
+    LineCountIsMoreThanData *data = (LineCountIsMoreThanData *) user_data;
+
+    const char *coverage_line = &line[3];
+    char *comma_ptr = NULL;
+
+    unsigned int lineno = strtol(coverage_line, &comma_ptr, 10);
+
+    g_assert(comma_ptr[0] == ',');
+
+    char *end_ptr = NULL;
+
+    unsigned int value = strtol(&comma_ptr[1], &end_ptr, 10);
+
+    g_assert(end_ptr[0] == '\0' ||
+             end_ptr[0] == '\n');
+
+    return data->expected_lineno == lineno &&
+           value > data->expected_to_be_more_than;
+}
+
+static void
+test_single_line_hit_written_to_coverage_data(gpointer      fixture_data,
+                                              gconstpointer user_data)
+{
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+
+    char *coverage_data_contents =
+        eval_script_and_get_coverage_data(fixture->base_fixture.context,
+                                          fixture->base_fixture.coverage,
+                                          fixture->base_fixture.temporary_js_script_filename,
+                                          fixture->output_file_directory,
+                                          NULL);
+
+    LineCountIsMoreThanData data = {
+        1,
+        0
+    };
+
+    g_assert(coverage_data_matches_value_for_key(coverage_data_contents,
+                                                 "DA:",
+                                                 line_hit_count_is_more_than,
+                                                 &data));
+    g_free(coverage_data_contents);
+}
+
+static void
+test_full_line_tally_written_to_coverage_data(gpointer      fixture_data,
+                                              gconstpointer user_data)
+{
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+
+    char *coverage_data_contents =
+        eval_script_and_get_coverage_data(fixture->base_fixture.context,
+                                          fixture->base_fixture.coverage,
+                                          fixture->base_fixture.temporary_js_script_filename,
+                                          fixture->output_file_directory,
+                                          NULL);
+
+    /* More than one assert per test is bad, but we are testing interlinked concepts */
+    g_assert(coverage_data_contains_value_for_key(coverage_data_contents,
+                                                  "LF:",
+                                                  "1"));
+    g_assert(coverage_data_contains_value_for_key(coverage_data_contents,
+                                                  "LH:",
+                                                  "1"));
+    g_free(coverage_data_contents);
+}
+
+static void
+test_no_hits_to_coverage_data_for_unexecuted(gpointer      fixture_data,
+                                             gconstpointer user_data)
+{
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+
+    char *coverage_data_contents =
+        write_statistics_and_get_coverage_data(fixture->base_fixture.coverage,
+                                               fixture->base_fixture.temporary_js_script_filename,
+                                               fixture->output_file_directory,
+                                               NULL);
+
+    /* More than one assert per test is bad, but we are testing interlinked concepts */
+    g_assert(coverage_data_contains_value_for_key(coverage_data_contents,
+                                                  "LF:",
+                                                  "1"));
+    g_assert(coverage_data_contains_value_for_key(coverage_data_contents,
+                                                  "LH:",
+                                                  "0"));
+    g_free(coverage_data_contents);
+}
+
+static void
+test_end_of_record_section_written_to_coverage_data(gpointer      fixture_data,
+                                                    gconstpointer user_data)
+{
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+
+    char *coverage_data_contents =
+        eval_script_and_get_coverage_data(fixture->base_fixture.context,
+                                          fixture->base_fixture.coverage,
+                                          fixture->base_fixture.temporary_js_script_filename,
+                                          fixture->output_file_directory,
+                                          NULL);
+
+    g_assert(strstr(coverage_data_contents, "end_of_record") != NULL);
+    g_free(coverage_data_contents);
+}
+
+typedef struct _GjsCoverageMultipleSourcesFixture {
+    GjsCoverageToSingleOutputFileFixture base_fixture;
+    char         *second_js_source_file_name;
+    unsigned int second_gjs_source_file_handle;
+} GjsCoverageMultpleSourcesFixutre;
+
+static void
+gjs_coverage_multiple_source_files_to_single_output_fixture_set_up(gpointer fixture_data,
+                                                                         gconstpointer user_data)
+{
+    gjs_coverage_to_single_output_file_fixture_set_up (fixture_data, user_data);
+
+    GjsCoverageMultpleSourcesFixutre *fixture = (GjsCoverageMultpleSourcesFixutre *) fixture_data;
+    fixture->second_js_source_file_name = 
g_strconcat(fixture->base_fixture.base_fixture.temporary_js_script_directory_name,
+                                                      "/",
+                                                      "gjs_coverage_second_source_file_XXXXXX.js",
+                                                      NULL);
+    fixture->second_gjs_source_file_handle = mkstemps(fixture->second_js_source_file_name, 3);
+
+    /* Because GjsCoverage searches the coverage paths at object-creation time,
+     * we need to destroy the previously constructed one and construct it again */
+    const char *coverage_paths[] = {
+        fixture->base_fixture.base_fixture.temporary_js_script_filename,
+        fixture->second_js_source_file_name,
+        NULL
+    };
+
+    g_object_unref(fixture->base_fixture.base_fixture.coverage);
+    fixture->base_fixture.base_fixture.coverage = 
gjs_coverage_new(fixture->base_fixture.base_fixture.debug_hooks,
+                                                                   coverage_paths);
+
+    char *base_name = g_path_get_basename(fixture->base_fixture.base_fixture.temporary_js_script_filename);
+    char *base_name_without_extension = g_strndup(base_name,
+                                                  strlen(base_name) - 3);
+    char *mock_script = g_strconcat("const FirstScript = imports.",
+                                    base_name_without_extension,
+                                    ";\n",
+                                    "let a = FirstScript.f;\n"
+                                    "\n",
+                                    NULL);
+
+    write_to_file_at_beginning(fixture->second_gjs_source_file_handle, mock_script);
+
+    g_free(mock_script);
+    g_free(base_name_without_extension);
+    g_free(base_name);
+}
+
+static void
+gjs_coverage_multiple_source_files_to_single_output_fixture_tear_down(gpointer      fixture_data,
+                                                                            gconstpointer user_data)
+{
+    GjsCoverageMultpleSourcesFixutre *fixture = (GjsCoverageMultpleSourcesFixutre *) fixture_data;
+    unlink(fixture->second_js_source_file_name);
+    g_free(fixture->second_js_source_file_name);
+    close(fixture->second_gjs_source_file_handle);
+
+    gjs_coverage_to_single_output_file_fixture_tear_down(fixture_data, user_data);
+}
+
+static void
+test_multiple_source_file_records_written_to_coverage_data (gpointer      fixture_data,
+                                                            gconstpointer user_data)
+{
+    GjsCoverageMultpleSourcesFixutre *fixture = (GjsCoverageMultpleSourcesFixutre *) fixture_data;
+
+    char *coverage_data_contents =
+        eval_script_and_get_coverage_data(fixture->base_fixture.base_fixture.context,
+                                          fixture->base_fixture.base_fixture.coverage,
+                                          fixture->second_js_source_file_name,
+                                          fixture->base_fixture.output_file_directory,
+                                          NULL);
+
+    const char *first_sf_record = line_starting_with(coverage_data_contents, "SF:");
+    const char *second_sf_record = line_starting_with(first_sf_record + 1, "SF:");
+
+    g_assert(first_sf_record != NULL);
+    g_assert(second_sf_record != NULL);
+
+    g_free(coverage_data_contents);
+}
+
+typedef struct _ExpectedSourceFileCoverageData {
+    const char              *source_file_path;
+    LineCountIsMoreThanData *more_than;
+    unsigned int            n_more_than_matchers;
+    const char              expected_lines_hit_character;
+    const char              expected_lines_found_character;
+} ExpectedSourceFileCoverageData;
+
+static gboolean
+check_coverage_data_for_source_file(ExpectedSourceFileCoverageData *expected,
+                                    const gsize                     expected_size,
+                                    const char                     *section_start)
+{
+    gsize i;
+    for (i = 0; i < expected_size; ++i) {
+        if (strncmp (&section_start[3],
+                     expected[i].source_file_path,
+                     strlen (expected[i].source_file_path)) == 0) {
+            const gboolean line_hits_match = coverage_data_matches_values_for_key (section_start,
+                                                                                   "DA:",
+                                                                                   
expected[i].n_more_than_matchers,
+                                                                                   
line_hit_count_is_more_than,
+                                                                                   expected[i].more_than,
+                                                                                   sizeof 
(LineCountIsMoreThanData));
+            const char *total_hits_record = line_starting_with (section_start, "LH:");
+            const gboolean total_hits_match = total_hits_record[3] == 
expected[i].expected_lines_hit_character;
+            const char *total_found_record = line_starting_with (section_start, "LF:");
+            const gboolean total_found_match = total_found_record[3] == 
expected[i].expected_lines_found_character;
+
+            return line_hits_match &&
+                   total_hits_match &&
+                   total_found_match;
+        }
+    }
+
+    return FALSE;
+}
+
+static void
+test_correct_line_coverage_data_written_for_both_source_file_sectons(gpointer      fixture_data,
+                                                                     gconstpointer user_data)
+{
+    GjsCoverageMultpleSourcesFixutre *fixture = (GjsCoverageMultpleSourcesFixutre *) fixture_data;
+
+    char *coverage_data_contents =
+        eval_script_and_get_coverage_data(fixture->base_fixture.base_fixture.context,
+                                          fixture->base_fixture.base_fixture.coverage,
+                                          fixture->second_js_source_file_name,
+                                          fixture->base_fixture.output_file_directory,
+                                          NULL);
+
+    LineCountIsMoreThanData first_script_matcher = {
+        1,
+        0
+    };
+
+    LineCountIsMoreThanData second_script_matchers[] = {
+        {
+            1,
+            0
+        },
+        {
+            2,
+            0
+        }
+    };
+
+    char *first_script_basename =
+        g_filename_display_basename(fixture->base_fixture.base_fixture.temporary_js_script_filename);
+    char *second_script_basename =
+        g_filename_display_basename(fixture->second_js_source_file_name);
+
+    char *first_script_output_path =
+        g_build_filename(fixture->base_fixture.output_file_directory,
+                         first_script_basename,
+                         NULL);
+    char *second_script_output_path =
+        g_build_filename(fixture->base_fixture.output_file_directory,
+                         second_script_basename,
+                         NULL);
+
+    ExpectedSourceFileCoverageData expected[] = {
+        {
+            first_script_output_path,
+            &first_script_matcher,
+            1,
+            '1',
+            '1'
+        },
+        {
+            second_script_output_path,
+            second_script_matchers,
+            2,
+            '2',
+            '2'
+        }
+    };
+
+    const gsize expected_len = G_N_ELEMENTS(expected);
+
+    const char *first_sf_record = line_starting_with(coverage_data_contents, "SF:");
+    g_assert(check_coverage_data_for_source_file(expected, expected_len, first_sf_record));
+
+    const char *second_sf_record = line_starting_with(first_sf_record + 3, "SF:");
+    g_assert(check_coverage_data_for_source_file(expected, expected_len, second_sf_record));
+
+    g_free(first_script_basename);
+    g_free(first_script_output_path);
+    g_free(second_script_basename);
+    g_free(second_script_output_path);
+    g_free(coverage_data_contents);
+}
+
+typedef struct _FixturedTest {
+    gsize            fixture_size;
+    GTestFixtureFunc set_up;
+    GTestFixtureFunc tear_down;
+} FixturedTest;
+
+static void
+add_test_for_fixture(const char      *name,
+                     FixturedTest    *fixture,
+                     GTestFixtureFunc test_func,
+                     gconstpointer    user_data)
+{
+    g_test_add_vtable(name,
+                      fixture->fixture_size,
+                      user_data,
+                      fixture->set_up,
+                      test_func,
+                      fixture->tear_down);
+}
+
+void gjs_test_add_tests_for_coverage ()
+{
+    FixturedTest coverage_to_single_output_fixture = {
+        sizeof(GjsCoverageToSingleOutputFileFixture),
+        gjs_coverage_to_single_output_file_fixture_set_up,
+        gjs_coverage_to_single_output_file_fixture_tear_down
+    };
+
+    add_test_for_fixture("/gjs/coverage/file_duplicated_into_output_path",
+                         &coverage_to_single_output_fixture,
+                         test_covered_file_is_duplicated_into_output_if_path,
+                         NULL);
+    add_test_for_fixture("/gjs/coverage/file_duplicated_full_resource_path",
+                         &coverage_to_single_output_fixture,
+                         test_covered_file_is_duplicated_into_output_if_resource,
+                         NULL);
+    add_test_for_fixture("/gjs/coverage/contents_preserved_accumulate_mode",
+                         &coverage_to_single_output_fixture,
+                         test_previous_contents_preserved,
+                         NULL);
+    add_test_for_fixture("/gjs/coverage/new_contents_appended_accumulate_mode",
+                         &coverage_to_single_output_fixture,
+                         test_new_contents_written,
+                         NULL);
+    add_test_for_fixture("/gjs/coverage/expected_source_file_name_written_to_coverage_data",
+                         &coverage_to_single_output_fixture,
+                         test_expected_source_file_name_written_to_coverage_data,
+                         NULL);
+    add_test_for_fixture("/gjs/coverage/single_branch_coverage_written_to_coverage_data",
+                         &coverage_to_single_output_fixture,
+                         test_single_branch_coverage_written_to_coverage_data,
+                         NULL);
+    add_test_for_fixture("/gjs/coverage/multiple_branch_coverage_written_to_coverage_data",
+                         &coverage_to_single_output_fixture,
+                         test_multiple_branch_coverage_written_to_coverage_data,
+                         NULL);
+    add_test_for_fixture("/gjs/coverage/branches_for_multiple_case_statements_fallthrough",
+                         &coverage_to_single_output_fixture,
+                         test_branches_for_multiple_case_statements_fallthrough,
+                         NULL);
+    add_test_for_fixture("/gjs/coverage/not_hit_branch_point_written_to_coverage_data",
+                         &coverage_to_single_output_fixture,
+                         test_branch_not_hit_written_to_coverage_data,
+                         NULL);
+    add_test_for_fixture("/gjs/coverage/function_names_written_to_coverage_data",
+                         &coverage_to_single_output_fixture,
+                         test_function_names_written_to_coverage_data,
+                         NULL);
+    add_test_for_fixture("/gjs/coverage/function_hit_counts_written_to_coverage_data",
+                         &coverage_to_single_output_fixture,
+                         test_function_hit_counts_written_to_coverage_data,
+                         NULL);
+    add_test_for_fixture("/gjs/coverage/total_function_coverage_written_to_coverage_data",
+                         &coverage_to_single_output_fixture,
+                         test_total_function_coverage_written_to_coverage_data,
+                         NULL);
+    add_test_for_fixture("/gjs/coverage/single_line_hit_written_to_coverage_data",
+                         &coverage_to_single_output_fixture,
+                         test_single_line_hit_written_to_coverage_data,
+                         NULL);
+    add_test_for_fixture("/gjs/coverage/full_line_tally_written_to_coverage_data",
+                         &coverage_to_single_output_fixture,
+                         test_full_line_tally_written_to_coverage_data,
+                         NULL);
+    add_test_for_fixture("/gjs/coverage/no_hits_for_unexecuted_file",
+                         &coverage_to_single_output_fixture,
+                         test_no_hits_to_coverage_data_for_unexecuted,
+                         NULL);
+    add_test_for_fixture("/gjs/coverage/end_of_record_section_written_to_coverage_data",
+                         &coverage_to_single_output_fixture,
+                         test_end_of_record_section_written_to_coverage_data,
+                         NULL);
+
+    FixturedTest coverage_for_multiple_files_to_single_output_fixture = {
+        sizeof(GjsCoverageMultpleSourcesFixutre),
+        gjs_coverage_multiple_source_files_to_single_output_fixture_set_up,
+        gjs_coverage_multiple_source_files_to_single_output_fixture_tear_down
+    };
+
+    add_test_for_fixture("/gjs/coverage/multiple_source_file_records_written_to_coverage_data",
+                         &coverage_for_multiple_files_to_single_output_fixture,
+                         test_multiple_source_file_records_written_to_coverage_data,
+                         NULL);
+    add_test_for_fixture("/gjs/coverage/correct_line_coverage_data_written_for_both_sections",
+                         &coverage_for_multiple_files_to_single_output_fixture,
+                         test_correct_line_coverage_data_written_for_both_source_file_sectons,
+                         NULL);
+}
diff --git a/test/gjs-test-coverage/loadedJSFromResource.js b/test/gjs-test-coverage/loadedJSFromResource.js
new file mode 100644
index 0000000..403d3d9
--- /dev/null
+++ b/test/gjs-test-coverage/loadedJSFromResource.js
@@ -0,0 +1 @@
+function mock_function() {}
diff --git a/test/gjs-tests-add-funcs.h b/test/gjs-tests-add-funcs.h
index 5080a68..240b1bb 100644
--- a/test/gjs-tests-add-funcs.h
+++ b/test/gjs-tests-add-funcs.h
@@ -22,5 +22,6 @@
 
 void gjs_test_add_tests_for_reflected_script ();
 void gjs_test_add_tests_for_debug_hooks ();
+void gjs_test_add_tests_for_coverage ();
 
 #endif
diff --git a/test/gjs-tests.cpp b/test/gjs-tests.cpp
index 81219d8..0d4abe1 100644
--- a/test/gjs-tests.cpp
+++ b/test/gjs-tests.cpp
@@ -400,6 +400,7 @@ main(int    argc,
 
     gjs_test_add_tests_for_reflected_script();
     gjs_test_add_tests_for_debug_hooks ();
+    gjs_test_add_tests_for_coverage ();
 
     g_test_run();
 
diff --git a/test/mock-js-resources.gresource.xml b/test/mock-js-resources.gresource.xml
new file mode 100644
index 0000000..196f639
--- /dev/null
+++ b/test/mock-js-resources.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/gjs/mock">
+    <file>test/gjs-test-coverage/loadedJSFromResource.js</file>
+  </gresource>
+</gresources>


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