[gjs/wip/coverage: 1/2] coverage: Add GjsCoverage



commit f307aceb4b741bc0f3b28b2304743f6b0dbd39db
Author: Sam Spilsbury <smspillaz gmail com>
Date:   Fri Jan 17 00:30:15 2014 -0200

    coverage: Add GjsCoverage
    
    GjsCoverage is a wrapper around coverage.js, a new module
    which uses the JS Debugger API and JS Reflect API to collect
    hits counts for lines, branches and functions.
    
    Pass -C or --coverage-path to specify a file that should be
    added to the coverage report. --coverage-output must also
    be passed when using these options and specifies a directory
    to write coverage reports to. This will copy the directory
    structure of covered files into this directory and generate
    an lcov compatible coverage report at coverage.lcov. This file
    will be opened in append-mode and should be deleted when coverage
    reports need to be regenerated.

 .gitignore                                     |    1 +
 Makefile-insttest.am                           |    1 +
 Makefile-test.am                               |   17 +-
 Makefile.am                                    |    2 +
 gjs/console.cpp                                |   20 +
 gjs/coverage.cpp                               | 1316 +++++++++++++++++++++
 gjs/coverage.h                                 |   86 ++
 installed-tests/js/testCoverage.js             |  849 ++++++++++++++
 modules/coverage.js                            |  761 ++++++++++++
 modules/modules.gresource.xml.in               |    1 +
 test/gjs-test-coverage.cpp                     | 1491 ++++++++++++++++++++++++
 test/gjs-test-coverage/loadedJSFromResource.js |    1 +
 test/gjs-tests-add-funcs.h                     |   25 +
 test/gjs-tests.cpp                             |    4 +
 test/mock-js-resources.gresource.xml           |    6 +
 15 files changed, 4578 insertions(+), 3 deletions(-)
---
diff --git a/.gitignore b/.gitignore
index c158681..7d6399e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,3 +45,4 @@ jsunit.test
 testSystemExit.test
 modules-resources.[ch]
 modules.gresource.xml
+mock-js-resources.[ch]
diff --git a/Makefile-insttest.am b/Makefile-insttest.am
index c1ea941..263201c 100644
--- a/Makefile-insttest.am
+++ b/Makefile-insttest.am
@@ -101,6 +101,7 @@ dist_jstests_DATA += \
        installed-tests/js/testself.js                  \
        installed-tests/js/testByteArray.js             \
        installed-tests/js/testClass.js                 \
+       installed-tests/js/testCoverage.js \
        installed-tests/js/testGDBus.js                 \
        installed-tests/js/testEverythingBasic.js               \
        installed-tests/js/testEverythingEncapsulated.js        \
diff --git a/Makefile-test.am b/Makefile-test.am
index 27b2204..52a1235 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 =              \
@@ -22,7 +31,10 @@ gjs_tests_LDADD =            \
        $(GJSTESTS_LIBS)
 
 gjs_tests_SOURCES =            \
-       test/gjs-tests.cpp
+       test/gjs-tests.cpp \
+       test/gjs-tests-add-funcs.h \
+       test/gjs-test-coverage.cpp \
+       mock-js-resources.c
 
 check-local: gjs-tests
        @test -z "${TEST_PROGS}" || ${GTESTER} --verbose ${TEST_PROGS} ${TEST_PROGS_OPTIONS}
@@ -34,8 +46,7 @@ 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)                                     \
+       GI_TYPELIB_PATH=$(builddir):$(GI_TYPELIB_PATH)                                  \
        LD_LIBRARY_PATH="$(LD_LIBRARY_PATH):$(FIREFOX_JS_LIBDIR)"       \
        G_FILENAME_ENCODING=latin1      # ensure filenames are not utf8
diff --git a/Makefile.am b/Makefile.am
index 5059966..321f320 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -29,6 +29,7 @@ nobase_gjs_public_include_HEADERS =   \
 nobase_gjs_module_include_HEADERS =    \
        gjs/gjs-module.h        \
        gjs/compat.h            \
+       gjs/coverage.h \
        gjs/byteArray.h         \
        gjs/importer.h          \
        gjs/jsapi-util.h        \
@@ -106,6 +107,7 @@ libgjs_la_SOURCES =         \
        gjs/importer.cpp                \
        gjs/gi.h                \
        gjs/gi.cpp              \
+       gjs/coverage.cpp \
        gjs/jsapi-private.cpp   \
        gjs/jsapi-util.cpp      \
        gjs/jsapi-dynamic-class.cpp \
diff --git a/gjs/console.cpp b/gjs/console.cpp
index d96bd45..05eafe6 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 }
 };
@@ -57,6 +62,7 @@ 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 +117,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((const gchar **) coverage_paths,
+                                    js_context);
+    }
+
     /* prepare command line arguments */
     if (!gjs_context_define_string_array(js_context, "ARGV",
                                          argc - 1, (const char**)argv + 1,
@@ -131,6 +145,12 @@ main(int argc, char **argv)
     }
 
  out:
+
+    /* Probably doesn't make sense to write statistics on failure */
+    if (coverage && code == 0)
+        gjs_coverage_write_statistics(coverage,
+                                      coverage_output_path);
+ 
     g_object_unref(js_context);
     g_free(script);
     exit(code);
diff --git a/gjs/coverage.cpp b/gjs/coverage.cpp
new file mode 100644
index 0000000..1f98d19
--- /dev/null
+++ b/gjs/coverage.cpp
@@ -0,0 +1,1316 @@
+/*
+ * 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 <sys/stat.h>
+#include <gio/gio.h>
+
+#include "gjs-module.h"
+#include "coverage.h"
+
+#include "util/error.h"
+
+typedef struct _GjsDebugHooks GjsDebugHooks;
+typedef struct _GjsCoverageBranchData GjsCoverageBranchData;
+
+struct _GjsCoveragePrivate {
+    gchar         **covered_paths;
+
+    GjsContext    *context;
+    JSObject      *coverage_statistics;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE(GjsCoverage,
+                           gjs_coverage,
+                           G_TYPE_OBJECT)
+
+enum {
+    PROP_0,
+    PROP_COVERAGE_PATHS,
+    PROP_CONTEXT,
+    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;
+};
+
+typedef struct _GjsCoverageBranchExit {
+    unsigned int line;
+    unsigned int hit_count;
+} GjsCoverageBranchExit;
+
+typedef struct _GjsCoverageBranch {
+    GArray       *exits;
+    unsigned int point;
+    gboolean     hit;
+} GjsCoverageBranch;
+
+typedef struct _GjsCoverageFunction {
+    char         *key;
+    unsigned int hit_count;
+} GjsCoverageFunction;
+
+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,
+                           GArray        *functions,
+                           unsigned int  *n_functions_found,
+                           unsigned int  *n_functions_hit)
+{
+    unsigned int i = 0;
+
+    for (; i < functions->len; ++i) {
+        GjsCoverageFunction *function = &(g_array_index(functions, GjsCoverageFunction, i));
+        write_function_hit_count(stream,
+                                 function->key,
+                                 function->hit_count,
+                                 n_functions_found,
+                                 n_functions_hit);
+    }
+}
+
+static void
+write_function_foreach_func(gpointer value,
+                            gpointer user_data)
+{
+    GOutputStream       *stream = (GOutputStream *) user_data;
+    GjsCoverageFunction *function = (GjsCoverageFunction *) value;
+
+    write_string_into_stream(stream, "FN:");
+    write_string_into_stream(stream, function->key);
+    write_string_into_stream(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);
+}
+
+static void
+write_functions(GOutputStream *data_stream,
+                GArray        *functions)
+{
+    for_each_element_in_array(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");
+}
+
+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_exits_found;
+    unsigned int *n_branch_exits_hit;
+    GOutputStream *output_stream;
+} WriteBranchInfoData;
+
+static void
+write_individual_branch(gpointer branch_ptr,
+                        gpointer user_data)
+{
+    GjsCoverageBranch   *branch = (GjsCoverageBranch *) branch_ptr;
+    WriteBranchInfoData *data = (WriteBranchInfoData *) user_data;
+
+    /* This line is not a branch, don't write anything */
+    if (!branch->point)
+        return;
+
+    unsigned int i = 0;
+    for (; i < branch->exits->len; ++i) {
+        GjsCoverageBranchExit *exit = &(g_array_index(branch->exits, GjsCoverageBranchExit, i));
+        unsigned int alternative_counter = exit->hit_count;
+        unsigned int branch_point = branch->point;
+        char         *hit_count_string = NULL;
+
+        if (!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_exits_found);
+
+        if (alternative_counter > 0)
+            ++(*data->n_branch_exits_hit);
+    }
+}
+
+static void
+write_branch_coverage(GOutputStream *stream,
+                      GArray        *branches,
+                      unsigned int  *n_branch_exits_found,
+                      unsigned int  *n_branch_exits_hit)
+
+{
+    /* Write individual branches and pass-out the totals */
+    WriteBranchInfoData data = {
+        n_branch_exits_found,
+        n_branch_exits_hit,
+        stream
+    };
+
+    for_each_element_in_array(branches,
+                              write_individual_branch,
+                              &data);
+}
+
+static void
+write_branch_totals(GOutputStream *stream,
+                    unsigned int   n_branch_exits_found,
+                    unsigned int   n_branch_exits_hit)
+{
+    write_string_into_stream(stream, "BRF:");
+    write_uint32_into_stream(stream, n_branch_exits_found);
+    write_string_into_stream(stream, "\n");
+
+    write_string_into_stream(stream, "BRH:");
+    write_uint32_into_stream(stream, n_branch_exits_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;
+    JSContext         *context;
+    JSObject          *object;
+} 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;
+}
+
+typedef gboolean (*ConvertAndInsertJSVal) (GArray    *array,
+                                           JSContext *context,
+                                           jsval     *element);
+
+static gboolean
+get_array_from_js_value(JSContext             *context,
+                        jsval                 *value,
+                        size_t                 array_element_size,
+                        GDestroyNotify         element_clear_func,
+                        ConvertAndInsertJSVal  inserter,
+                        GArray                **out_array)
+{
+    g_return_val_if_fail(out_array != NULL, FALSE);
+    g_return_val_if_fail(*out_array == NULL, FALSE);
+
+    JSObject *js_array = JSVAL_TO_OBJECT(*value);
+
+    if (!JS_IsArrayObject(context, js_array)) {
+        g_critical("Returned object from is not an array");
+        return FALSE;
+    }
+
+    /* We're not preallocating any space here at the moment until
+     * we have some profiling data that suggests a good size to
+     * preallocate to. */
+    GArray *c_side_array = g_array_new(TRUE, TRUE, array_element_size);
+    u_int32_t js_array_len;
+
+    if (element_clear_func)
+        g_array_set_clear_func(c_side_array, element_clear_func);
+
+    if (JS_GetArrayLength(context, js_array, &js_array_len)) {
+        u_int32_t i = 0;
+        for (; i < js_array_len; ++i) {
+            jsval element;
+            if (!JS_GetElement(context, js_array, i, &element)) {
+                g_array_unref(c_side_array);
+                gjs_throw(context, "Failed to get function names array element %i", i);
+                return FALSE;
+            }
+
+            if (!(inserter(c_side_array, context, &element))) {
+                g_array_unref(c_side_array);
+                gjs_throw(context, "Failed to convert array element %i", i);
+                return FALSE;
+            }
+        }
+    }
+
+    *out_array = c_side_array;
+
+    return TRUE;
+}
+
+static GArray *
+call_js_function_for_array_return(JSContext             *context,
+                                  JSObject              *object,
+                                  size_t                 array_element_size,
+                                  GDestroyNotify         element_clear_func,
+                                  ConvertAndInsertJSVal  inserter,
+                                  const char            *function_name,
+                                  jsval                 *argument)
+{
+    GArray *array = NULL;
+    jsval rval;
+    if (!JS_CallFunctionName(context, object, function_name, 1, argument, &rval)) {
+        gjs_log_exception(context);
+        return NULL;
+    }
+
+    if (!get_array_from_js_value(context,
+                                 &rval,
+                                 array_element_size,
+                                 element_clear_func,
+                                 inserter,
+                                 &array)) {
+        gjs_log_exception(context);
+        return NULL;
+    }
+
+    return array;
+}
+
+static gboolean
+convert_and_insert_unsigned_int(GArray    *array,
+                                JSContext *context,
+                                jsval     *element)
+{
+    if (!JSVAL_IS_INT(*element) &&
+        !JSVAL_IS_VOID(*element) &&
+        !JSVAL_IS_NULL(*element)) {
+        g_critical("Array element is not an integer or undefined or null");
+        return FALSE;
+    }
+
+    if (JSVAL_IS_INT(*element)) {
+        unsigned int element_integer = JSVAL_TO_INT(*element);
+        g_array_append_val(array, element_integer);
+    } else {
+        int not_executable = -1;
+        g_array_append_val(array, not_executable);
+    }
+
+    return TRUE;
+}
+
+static GArray *
+get_line_hits_for_js_dbg_coverage_object(JSContext *context,
+                                         JSObject  *object,
+                                         jsval     *filename_value)
+{
+    return call_js_function_for_array_return(context,
+                                             object,
+                                             sizeof(unsigned int),
+                                             NULL,
+                                             convert_and_insert_unsigned_int,
+                                             "getExecutedLinesFor",
+                                             filename_value);
+}
+
+static void
+init_covered_function(GjsCoverageFunction *function,
+                      char                *key,
+                      unsigned int        hit_count)
+{
+    function->key = key;
+    function->hit_count = hit_count;
+}
+
+static void
+clear_coverage_function(gpointer info_location)
+{
+    GjsCoverageFunction *info = (GjsCoverageFunction *) info_location;
+    g_free(info->key);
+}
+
+static gboolean
+convert_and_insert_function_decl(GArray    *array,
+                                 JSContext *context,
+                                 jsval     *element)
+{
+    JSObject *object = JSVAL_TO_OBJECT(*element);
+
+    if (!object) {
+        gjs_throw(context, "Converting element to object failed");
+        return FALSE;
+    }
+
+    jsval    function_name_property_value;
+
+    if (!JS_GetProperty(context, object, "name", &function_name_property_value)) {
+        gjs_throw(context, "Failed to get name property for function object");
+        return FALSE;
+    }
+
+    char *utf8_string;
+
+    if (JSVAL_IS_STRING(function_name_property_value)) {
+        if (!gjs_string_to_utf8(context,
+                                function_name_property_value,
+                                &utf8_string)) {
+            gjs_throw(context, "Failed to convert function_name to string");
+            return FALSE;
+        }
+    } else if (JSVAL_IS_NULL(function_name_property_value)) {
+        utf8_string = NULL;
+    } else {
+        gjs_throw(context, "Unexpected type for function_name");
+        return FALSE;
+    }
+
+    jsval hit_count_property_value;
+    if (!JS_GetProperty(context, object, "hitCount", &hit_count_property_value) ||
+        !JSVAL_IS_INT(hit_count_property_value)) {
+        gjs_throw(context, "Failed to get hitCount property for function object");
+        return FALSE;
+    }
+
+    unsigned int line_number = JSVAL_TO_INT(hit_count_property_value);
+
+    GjsCoverageFunction info;
+    init_covered_function(&info,
+                          utf8_string,
+                          line_number);
+
+    g_array_append_val(array, info);
+
+    return TRUE;
+}
+
+static GArray *
+get_function_hits_from_js_dbg_coverage_object(JSContext *context,
+                                              JSObject  *object,
+                                              jsval     *filename_value)
+{
+    return call_js_function_for_array_return(context,
+                                             object,
+                                             sizeof(GjsCoverageFunction),
+                                             clear_coverage_function,
+                                             convert_and_insert_function_decl,
+                                             "getFunctionsFor",
+                                             filename_value);
+}
+
+static void
+init_covered_branch(GjsCoverageBranch *branch,
+                    unsigned int       point,
+                    JSBool             was_hit,
+                    GArray            *exits)
+{
+    branch->point = point;
+    branch->hit = !!was_hit;
+    branch->exits = exits;
+}
+
+static void
+clear_coverage_branch(gpointer branch_location)
+{
+    GjsCoverageBranch *branch = (GjsCoverageBranch *) branch_location;
+    g_array_unref(branch->exits);
+}
+
+static gboolean
+convert_and_insert_branch_exit(GArray    *array,
+                               JSContext *context,
+                               jsval     *element)
+{
+    if (!JSVAL_IS_OBJECT(*element)) {
+        gjs_throw(context, "Branch exit array element is not an object");
+        return FALSE;
+    }
+
+    JSObject *object = JSVAL_TO_OBJECT(*element);
+
+    if (!object) {
+        gjs_throw(context, "Converting element to object failed");
+        return FALSE;
+    }
+
+    jsval   line_value;
+    int32_t line;
+
+    if (!JS_GetProperty(context, object, "line", &line_value) ||
+        !JSVAL_IS_INT(line_value)) {
+        gjs_throw(context, "Failed to get line property from element");
+        return FALSE;
+    }
+
+    line = JSVAL_TO_INT(line_value);
+
+    jsval   hit_count_value;
+    int32_t hit_count;
+
+    if (!JS_GetProperty(context, object, "hitCount", &hit_count_value) ||
+        !JSVAL_IS_INT(hit_count_value)) {
+        gjs_throw(context, "Failed to get hitCount property from element");
+        return FALSE;
+    }
+
+    hit_count = JSVAL_TO_INT(hit_count_value);
+
+    GjsCoverageBranchExit exit = {
+        (unsigned int) line,
+        (unsigned int) hit_count
+    };
+
+    g_array_append_val(array, exit);
+
+    return TRUE;
+}
+
+static gboolean
+convert_and_insert_branch_info(GArray    *array,
+                               JSContext *context,
+                               jsval     *element)
+{
+    if (!JSVAL_IS_OBJECT(*element) &&
+        !JSVAL_IS_VOID(*element)) {
+        gjs_throw(context, "Branch array element is not an object or undefined");
+        return FALSE;
+    }
+
+    if (JSVAL_IS_OBJECT(*element)) {
+        JSObject *object = JSVAL_TO_OBJECT(*element);
+
+        if (!object) {
+            gjs_throw(context, "Converting element to object failed");
+            return FALSE;
+        }
+
+        jsval   branch_point_value;
+        int32_t branch_point;
+
+        if (!JS_GetProperty(context, object, "point", &branch_point_value) ||
+            !JSVAL_IS_INT(branch_point_value)) {
+            gjs_throw(context, "Failed to get point property from element");
+            return FALSE;
+        }
+
+        branch_point = JSVAL_TO_INT(branch_point_value);
+
+        jsval  was_hit_value;
+        JSBool was_hit;
+
+        if (!JS_GetProperty(context, object, "hit", &was_hit_value) ||
+            !JSVAL_IS_BOOLEAN(was_hit_value)) {
+            gjs_throw(context, "Failed to get point property from element");
+            return FALSE;
+        }
+
+        was_hit = JSVAL_TO_BOOLEAN(was_hit_value);
+
+        jsval  branch_exits_value;
+        GArray *branch_exits_array = NULL;
+
+        if (!JS_GetProperty(context, object, "exits", &branch_exits_value) ||
+            !JSVAL_IS_OBJECT(branch_exits_value)) {
+            gjs_throw(context, "Failed to get exits property from element");
+            return FALSE;
+        }
+
+        if (!get_array_from_js_value(context,
+                                     &branch_exits_value,
+                                     sizeof(GjsCoverageBranchExit),
+                                     NULL,
+                                     convert_and_insert_branch_exit,
+                                     &branch_exits_array)) {
+            /* Already logged the exception, no need to do anything here */
+            return FALSE;
+        }
+
+        GjsCoverageBranch branch;
+        init_covered_branch(&branch,
+                            branch_point,
+                            was_hit,
+                            branch_exits_array);
+
+        g_array_append_val(array, branch);
+    }
+
+    return TRUE;
+}
+
+static GArray *
+get_branches_from_js_dbg_coverage_object(JSContext *context,
+                                         JSObject  *object,
+                                         jsval     *filename)
+{
+    return call_js_function_for_array_return(context,
+                                             object,
+                                             sizeof(GjsCoverageBranch),
+                                             clear_coverage_branch,
+                                             convert_and_insert_branch_info,
+                                             "getBranchesFor",
+                                             filename);
+}
+
+static void
+print_statistics_for_file(const char    *filename,
+                          const char    *output_directory,
+                          JSContext     *context,
+                          JSObject      *stats_obj,
+                          GOutputStream *ostream)
+{
+    char *absolute_output_directory = get_absolute_path(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);
+
+    JSAutoCompartment compartment(context, stats_obj);
+
+    JSString *filename_jsstr = JS_NewStringCopyZ(context, filename);
+    jsval    filename_jsval = STRING_TO_JSVAL(filename_jsstr);
+
+    GArray *lines = get_line_hits_for_js_dbg_coverage_object(context, stats_obj,
+                                                             &filename_jsval);
+    GArray *functions = get_function_hits_from_js_dbg_coverage_object(context, stats_obj,
+                                                                      &filename_jsval);
+    GArray *branches = get_branches_from_js_dbg_coverage_object(context, stats_obj,
+                                                                &filename_jsval);
+
+    if (!lines || !functions || !branches)
+        return;
+
+
+    copy_source_file_to_coverage_output(filename, destination_filename);
+
+    write_source_file_header(ostream, (const char *) destination_filename);
+    write_functions(ostream,
+                    functions);
+
+    unsigned int functions_hit_count = 0;
+    unsigned int functions_found_count = 0;
+
+    write_functions_hit_counts(ostream,
+                               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,
+                          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,
+                        lines,
+                        &lines_hit_count,
+                        &executable_lines_count);
+    write_line_totals(ostream,
+                      lines_hit_count,
+                      executable_lines_count);
+    write_end_of_record(ostream);
+
+    g_array_unref(lines);
+    g_array_unref(functions);
+    g_array_unref(branches);
+
+    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;
+
+    /* 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);
+
+    GOutputStream *ostream =
+        G_OUTPUT_STREAM(g_file_append_to(output_file,
+                                         G_FILE_CREATE_NONE,
+                                         NULL,
+                                         &error));
+
+
+    JSContext *context = (JSContext *) gjs_context_get_native_context(priv->context);
+
+    char **file_iter = priv->covered_paths;
+    while (*file_iter) {
+        print_statistics_for_file(*file_iter,
+                                  output_directory,
+                                  context,
+                                  priv->coverage_statistics,
+                                  ostream);
+        ++file_iter;
+    }
+
+    g_object_unref(ostream);
+    g_object_unref(output_file);
+}
+
+static void
+gjs_coverage_init(GjsCoverage *self)
+{
+}
+
+static JSBool
+lazy_get_script_contents(JSContext *context,
+                         unsigned   argc,
+                         jsval     *vp)
+{
+    JSObject *this_object = JS_THIS_OBJECT(context, vp);
+
+    jsval filename_value;
+    if (!JS_GetProperty(context, this_object, "filename", &filename_value) ||
+        !JSVAL_IS_STRING(filename_value)) {
+        gjs_throw(context, "The 'filename' property on this is not set");
+        return JS_FALSE;
+    }
+
+    char *filename = NULL;
+
+    if (!gjs_string_to_utf8(context, filename_value, &filename)) {
+        gjs_throw(context, "Failed to convert filename to a string");
+        return JS_FALSE;
+    }
+
+    GFile *file = g_file_new_for_commandline_arg(filename);
+    GError *error = NULL;
+
+    char *script;
+    gsize script_len;
+
+    if (!g_file_load_contents(file,
+                              NULL,
+                              &script,
+                              &script_len,
+                              NULL,
+                              &error)) {
+        gjs_throw(context, "Failed to load contents for filename %s: %s", filename, error->message);
+        g_object_unref(file);
+        g_free(filename);
+        g_clear_error(&error);
+        return JS_FALSE;
+    }
+
+    g_object_unref(file);
+    g_free(filename);
+
+    JSString *script_jsstr = JS_NewStringCopyN(context, script, script_len);
+    JS_SET_RVAL(context, vp, STRING_TO_JSVAL(script_jsstr));
+
+    g_free(script);
+
+    return JS_TRUE;
+}
+
+
+static GArray *
+strv_to_js_script_info_value_array(JSContext *context, char **strv)
+{
+    GArray *script_filenames = g_array_new(TRUE, TRUE, sizeof(jsval));
+    g_array_set_size(script_filenames, g_strv_length(strv));
+
+    unsigned int i = 0;
+    for (; i < script_filenames->len; ++i) {
+        const char *filename = strv[i];
+        JSString *filename_str = JS_NewStringCopyZ(context, filename);
+        jsval filename_str_value = STRING_TO_JSVAL(filename_str);
+
+        JSObject *object = JS_NewObject(context, NULL, NULL, NULL);
+
+        JS_SetProperty(context, object, "filename", &filename_str_value);
+        JS_DefineFunction(context,
+                          object,
+                          "getContents",
+                          (JSNative) lazy_get_script_contents,
+                          0,
+                          GJS_MODULE_PROP_FLAGS);
+
+        g_array_index(script_filenames, jsval, i) = OBJECT_TO_JSVAL(object);
+    }
+
+    return script_filenames;
+}
+
+static JSClass coverage_global_class = {
+    "GjsCoverageGlobal", JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(GJS_GLOBAL_SLOT_LAST),
+    JS_PropertyStub, JS_DeletePropertyStub, JS_PropertyStub, JS_StrictPropertyStub,
+    JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, NULL,
+    NULL /* checkAccess */, NULL /* call */, NULL /* hasInstance */, NULL /* construct */, NULL,
+    { NULL }
+};
+
+static gboolean
+gjs_context_eval_file_in_compartment(GjsContext *context,
+                                     const char *filename,
+                                     JSObject   *compartment_object,
+                                     GError     **error)
+{
+    char  *script = NULL;
+    gsize script_len = 0;
+
+    GFile *file = g_file_new_for_commandline_arg(filename);
+
+    if (!g_file_load_contents(file,
+                              NULL,
+                              &script,
+                              &script_len,
+                              NULL,
+                              error))
+        return FALSE;
+
+    jsval return_value;
+
+    JSContext *js_context = (JSContext *) gjs_context_get_native_context(context);
+
+    JSAutoCompartment compartment(js_context, compartment_object);
+
+    if (!gjs_eval_with_scope(js_context,
+                             compartment_object,
+                             script, script_len, filename,
+                             &return_value)) {
+        gjs_log_exception(js_context);
+        g_set_error(error, GJS_ERROR, GJS_ERROR_FAILED, "Failed to evaluate %s", filename);
+        return FALSE;
+    }
+
+    g_free(script);
+
+    return TRUE;
+}
+
+static JSBool
+coverage_warning(JSContext *context,
+                 unsigned   argc,
+                 jsval     *vp)
+{
+    jsval *argv = JS_ARGV(context, vp);
+    char *s;
+    JSExceptionState *exc_state;
+    JSString *jstr;
+
+    if (argc != 1) {
+        gjs_throw(context, "Must pass a single argument to Warning()");
+        return JS_FALSE;
+    }
+
+    JS_BeginRequest(context);
+
+    /* JS_ValueToString might throw, in which we will only
+     *log that the value could be converted to string */
+    exc_state = JS_SaveExceptionState(context);
+    jstr = JS_ValueToString(context, argv[0]);
+    if (jstr != NULL)
+        argv[0] = STRING_TO_JSVAL(jstr);    // GC root
+    JS_RestoreExceptionState(context, exc_state);
+
+    if (jstr == NULL) {
+        g_message("JS LOG: <cannot convert value to string>");
+        JS_EndRequest(context);
+        return JS_TRUE;
+    }
+
+    if (!gjs_string_to_utf8(context, STRING_TO_JSVAL(jstr), &s)) {
+        JS_EndRequest(context);
+        return JS_FALSE;
+    }
+
+    g_message("JS COVERAGE WARNING: %s", s);
+    g_free(s);
+
+    JS_EndRequest(context);
+    JS_SET_RVAL(context, vp, JSVAL_VOID);
+    return JS_TRUE;
+}
+
+
+static gboolean
+bootstrap_coverage(GjsCoverage *coverage)
+{
+    static const char  *coverage_script = "resource:///org/gnome/gjs/modules/coverage.js";
+    GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
+    GError             *error = NULL;
+
+    JSContext *context = (JSContext *) gjs_context_get_native_context(priv->context);
+    JSObject *debuggee = JS_GetGlobalObject(context);
+    JS::CompartmentOptions options;
+    options.setVersion(JSVERSION_LATEST);
+    JSObject *debugger_compartment = JS_NewGlobalObject(context, &coverage_global_class, NULL, options);
+
+    {
+        JSAutoCompartment compartment(context, debugger_compartment);
+        JS::RootedObject debuggeeWrapper(context, debuggee);
+        if (!JS_WrapObject(context, debuggeeWrapper.address())) {
+            gjs_throw(context, "Failed to wrap debugeee");
+            return FALSE;
+        }
+
+        JS::RootedValue debuggeeWrapperValue(context, JS::ObjectValue(*debuggeeWrapper));
+        if (!JS_SetProperty(context, debugger_compartment, "debuggee", debuggeeWrapperValue.address())) {
+            gjs_throw(context, "Failed to set debuggee property");
+            return FALSE;
+        }
+
+        if (!JS_InitStandardClasses(context, debugger_compartment)) {
+            gjs_throw(context, "Failed to init standard classes");
+            return FALSE;
+        }
+
+        if (!JS_InitReflect(context, debugger_compartment)) {
+            gjs_throw(context, "Failed to init Reflect");
+            return FALSE;
+        }
+
+        if (!JS_DefineDebuggerObject(context, debugger_compartment)) {
+            gjs_throw(context, "Failed to init Debugger");
+            return FALSE;
+        }
+
+        if (!JS_DefineFunction(context, debugger_compartment,
+                               "Warning",
+                               (JSNative) coverage_warning,
+                               1, GJS_MODULE_PROP_FLAGS))
+            g_error("Failed to define log function");
+
+        if (!gjs_context_eval_file_in_compartment(priv->context,
+                                                  coverage_script,
+                                                  debugger_compartment,
+                                                  &error))
+            g_error("Failed to eval coverage script: %s\n", error->message);
+
+        jsval coverage_statistics_prototype_value;
+        if (!JS_GetProperty(context, debugger_compartment, "CoverageStatistics", 
&coverage_statistics_prototype_value) ||
+            !JSVAL_IS_OBJECT(coverage_statistics_prototype_value)) {
+            gjs_throw(context, "Failed to get CoverageStatistics prototype");
+            return FALSE;
+        }
+
+        JSObject *coverage_statistics_constructor = JSVAL_TO_OBJECT(coverage_statistics_prototype_value);
+
+        /* Now create the array to pass the desired script names over */
+        GArray *filenames = strv_to_js_script_info_value_array(context, priv->covered_paths);
+        JSObject *filenames_js_array = JS_NewArrayObject(context, filenames->len, (jsval *) filenames->data);
+
+        jsval coverage_statistics_constructor_arguments[] = {
+            OBJECT_TO_JSVAL(filenames_js_array)
+        };
+
+        JSObject *coverage_statistics = JS_New(context,
+                                               coverage_statistics_constructor,
+                                               1,
+                                               coverage_statistics_constructor_arguments);
+
+        if (!coverage_statistics) {
+            gjs_throw(context, "Failed to create coverage_statitiscs object");
+            g_array_unref(filenames);
+            return FALSE;
+        }
+
+        priv->coverage_statistics = coverage_statistics;
+
+        g_array_unref(filenames);
+    }
+
+    return TRUE;
+}
+
+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);
+
+    JSContext *context = (JSContext *) gjs_context_get_native_context(priv->context);
+
+    /* Before bootstrapping, turn off the JIT on the context */
+    guint32 options_flags = JS_GetOptions(context) & ~(JSOPTION_ION | JSOPTION_BASELINE | JSOPTION_ASMJS);
+    JS_SetOptions(context, options_flags);
+
+    if (!bootstrap_coverage(coverage)) {
+        JSContext *context = (JSContext *) gjs_context_get_native_context(priv->context);
+        JSAutoCompartment compartment(context, JS_GetGlobalObject(context));
+        gjs_log_exception(context);
+    }
+}
+
+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_COVERAGE_PATHS:
+        g_assert(priv->covered_paths == NULL);
+        priv->covered_paths = (char **) g_value_dup_boxed (value);
+        break;
+    case PROP_CONTEXT:
+        priv->context = GJS_CONTEXT(g_value_dup_object(value));
+        break;
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+        break;
+    }
+}
+
+static void
+gjs_coverage_dispose(GObject *object)
+{
+    GjsCoverage *coverage = GJS_DEBUG_COVERAGE (object);
+    GjsCoveragePrivate *priv = (GjsCoveragePrivate *) gjs_coverage_get_instance_private(coverage);
+
+    g_clear_object(&priv->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_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_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));
+    properties[PROP_CONTEXT] = g_param_spec_object("context",
+                                                   "Context",
+                                                   "A context to gather coverage stats for",
+                                                   GJS_TYPE_CONTEXT,
+                                                   (GParamFlags) (G_PARAM_CONSTRUCT_ONLY | 
G_PARAM_WRITABLE));
+
+    g_object_class_install_properties(object_class,
+                                      PROP_N,
+                                      properties);
+}
+
+/**
+ * gjs_coverage_new:
+ * @coverage_paths: (transfer none): A null-terminated strv of directories to generate
+ * coverage_data for
+ *
+ * Returns: A #GjsDebugCoverage
+ */
+GjsCoverage *
+gjs_coverage_new (const char    **coverage_paths,
+                  GjsContext    *context)
+{
+    GjsCoverage *coverage =
+        GJS_DEBUG_COVERAGE(g_object_new(GJS_TYPE_DEBUG_COVERAGE,
+                                        "coverage-paths", coverage_paths,
+                                        "context", context,
+                                        NULL));
+
+    return coverage;
+}
diff --git a/gjs/coverage.h b/gjs/coverage.h
new file mode 100644
index 0000000..9293183
--- /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(const char    **covered_directories,
+                               GjsContext    *coverage_context);
+
+G_END_DECLS
+
+#endif
diff --git a/installed-tests/js/testCoverage.js b/installed-tests/js/testCoverage.js
new file mode 100644
index 0000000..7d18561
--- /dev/null
+++ b/installed-tests/js/testCoverage.js
@@ -0,0 +1,849 @@
+const JSUnit = imports.jsUnit;
+const Coverage = imports.coverage;
+
+function parseScriptForExpressionLines(script) {
+    const ast = Reflect.parse(script);
+    return Coverage.expressionLinesForAST(ast);
+}
+
+function assertArrayEquals(actual, expected, assertion) {
+    if (actual.length != expected.length)
+        throw "Arrays not equal length. Actual array was " +
+                actual.length + " and Expected array was " +
+                expected.length;
+
+    for (let i = 0; i < actual.length; i++) {
+        assertion(expected[i], actual[i]);
+    }
+}
+
+function testExpressionLinesFoundForAssignmentExpressionSides() {
+    let foundLinesOnBothExpressionSides =
+        parseScriptForExpressionLines("var x;\n" +
+                                      "x = (function() {\n" +
+                                      "    return 10;\n" +
+                                      "})();\n");
+    assertArrayEquals(foundLinesOnBothExpressionSides,
+                      [1, 2, 3],
+                      JSUnit.assertEquals);
+}
+
+
+function testExpressionLinesFoundForLinesInsideFunctions() {
+    let foundLinesInsideNamedFunction =
+        parseScriptForExpressionLines("function f(a, b) {\n" +
+                                      "    let x = a;\n" +
+                                      "    let y = b;\n" +
+                                      "    return x + y;\n" +
+                                      "}\n" +
+                                      "\n" +
+                                      "var z = f(1, 2);\n");
+    assertArrayEquals(foundLinesInsideNamedFunction,
+                      [2, 3, 4, 7],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForLinesInsideAnonymousFunctions() {
+    let foundLinesInsideAnonymousFunction =
+        parseScriptForExpressionLines("var z = (function f(a, b) {\n" +
+                                      "     let x = a;\n" +
+                                      "     let y = b;\n" +
+                                      "     return x + y;\n" +
+                                      " })();\n");
+    assertArrayEquals(foundLinesInsideAnonymousFunction,
+                      [1, 2, 3, 4],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForBodyOfFunctionProperty() {
+    let foundLinesInsideFunctionProperty =
+        parseScriptForExpressionLines("var o = {\n" +
+                                      "    foo: function () {\n" +
+                                      "        let x = a;\n" +
+                                      "    }\n" +
+                                      "};\n");
+    assertArrayEquals(foundLinesInsideFunctionProperty,
+                      [1, 2, 3],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForCallArgsOfFunctionProperty() {
+    let foundLinesInsideCallArgs =
+        parseScriptForExpressionLines("function f(a) {\n" +
+                                      "}\n" +
+                                      "f({\n" +
+                                      "    foo: function() {\n" +
+                                      "        let x = a;\n" +
+                                      "    }\n" +
+                                      "});\n");
+    assertArrayEquals(foundLinesInsideCallArgs,
+                      [1, 3, 4, 5],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForMultilineCallArgs() {
+    let foundLinesInsideMultilineCallArgs =
+        parseScriptForExpressionLines("function f(a, b, c) {\n" +
+                                      "}\n" +
+                                      "f(1,\n" +
+                                      "  2,\n" +
+                                      "  3);\n");
+    assertArrayEquals(foundLinesInsideMultilineCallArgs,
+                      [1, 3, 4, 5],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForNewCallWithObject() {
+    let foundLinesInsideObjectCallArg =
+        parseScriptForExpressionLines("function f(o) {\n" +
+                                      "}\n" +
+                                      "let obj = {\n" +
+                                      "    Name: new f({ a: 1,\n" +
+                                      "                  b: 2,\n" +
+                                      "                  c: 3\n" +
+                                      "                })\n" +
+                                      "}\n");
+    assertArrayEquals(foundLinesInsideObjectCallArg,
+                      [1, 3, 4, 5, 6],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForWhileLoop() {
+    let foundLinesInsideWhileLoop =
+        parseScriptForExpressionLines("var a = 0;\n" +
+                                      "while (a < 1) {\n" +
+                                      "    let x = 0;\n" +
+                                      "    let y = 1;\n" +
+                                      "    a++;" +
+                                      "\n" +
+                                      "}\n");
+    assertArrayEquals(foundLinesInsideWhileLoop,
+                      [1, 2, 3, 4, 5],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForTryCatchFinally() {
+    let foundLinesInsideTryCatchFinally =
+        parseScriptForExpressionLines("var a = 0;\n" +
+                                      "try {\n" +
+                                      "    a++;\n" +
+                                      "} catch (e) {\n" +
+                                      "    a++;\n" +
+                                      "} finally {\n" +
+                                      "    a++;\n" +
+                                      "}\n");
+    assertArrayEquals(foundLinesInsideTryCatchFinally,
+                      [1, 2, 3, 4, 5, 7],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForCaseStatements() {
+    let foundLinesInsideCaseStatements =
+        parseScriptForExpressionLines("var a = 0;\n" +
+                                      "switch (a) {\n" +
+                                      "case 1:\n" +
+                                      "    a++;\n" +
+                                      "    break;\n" +
+                                      "case 2:\n" +
+                                      "    a++;\n" +
+                                      "    break;\n" +
+                                      "}\n");
+    assertArrayEquals(foundLinesInsideCaseStatements,
+                      [1, 2, 4, 5, 7, 8],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForLoop() {
+    let foundLinesInsideLoop =
+        parseScriptForExpressionLines("for (let i = 0; i < 1; i++) {\n" +
+                                      "    let x = 0;\n" +
+                                      "    let y = 1;\n" +
+                                      "\n" +
+                                      "}\n");
+    assertArrayEquals(foundLinesInsideLoop,
+                      [1, 2, 3],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForIfExits() {
+    let foundLinesInsideIfExits =
+        parseScriptForExpressionLines("if (1 > 0) {\n" +
+                                      "    let i = 0;\n" +
+                                      "} else {\n" +
+                                      "    let j = 1;\n" +
+                                      "}\n");
+    assertArrayEquals(foundLinesInsideIfExits,
+                      [1, 2, 4],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForFirstLineOfMultilineIfTests() {
+    let foundLinesInsideMultilineIfTest =
+        parseScriptForExpressionLines("if (1 > 0 &&\n" +
+                                      "    2 > 0 &&\n" +
+                                      "    3 > 0){\n" +
+                                      "    let a = 3;\n" +
+                                      "}\n");
+    assertArrayEquals(foundLinesInsideMultilineIfTest,
+                      [1, 4],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForObjectPropertyLiterals() {
+    let foundLinesInsideObjectPropertyLiterals =
+        parseScriptForExpressionLines("var a = {\n" +
+                                      "    Name: 'foo',\n" +
+                                      "    Ex: 'bar'\n" +
+                                      "};\n");
+    assertArrayEquals(foundLinesInsideObjectPropertyLiterals,
+                      [1, 2, 3],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForObjectPropertyFunction() {
+    let foundLinesInsideObjectPropertyFunction =
+        parseScriptForExpressionLines("var a = {\n" +
+                                      "    Name: function() {},\n" +
+                                      "};\n");
+    assertArrayEquals(foundLinesInsideObjectPropertyFunction,
+                      [1, 2],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForObjectPropertyFunction() {
+    let foundLinesInsideObjectPropertyFunction =
+        parseScriptForExpressionLines("var a = {\n" +
+                                      "    Name: function() {},\n" +
+                                      "};\n");
+    assertArrayEquals(foundLinesInsideObjectPropertyFunction,
+                      [1, 2],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForObjectPropertyObjectExpression() {
+    let foundLinesInsideObjectPropertyObjectExpression =
+        parseScriptForExpressionLines("var a = {\n" +
+                                      "    Name: {},\n" +
+                                      "};\n");
+    assertArrayEquals(foundLinesInsideObjectPropertyObjectExpression,
+                      [1, 2],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForObjectPropertyArrayExpression() {
+    let foundLinesInsideObjectPropertyObjectExpression =
+        parseScriptForExpressionLines("var a = {\n" +
+                                      "    Name: [],\n" +
+                                      "};\n");
+    assertArrayEquals(foundLinesInsideObjectPropertyObjectExpression,
+                      [1, 2],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForObjectArgsToReturn() {
+    let foundLinesInsideObjectArgsToReturn =
+        parseScriptForExpressionLines("var a = {\n" +
+                                      "    Name: {},\n" +
+                                      "};\n");
+    assertArrayEquals(foundLinesInsideObjectArgsToReturn,
+                      [1, 2],
+                      JSUnit.assertEquals);
+}
+
+function testExpressionLinesFoundForObjectArgsToThrow() {
+    let foundLinesInsideObjectArgsToThrow =
+        parseScriptForExpressionLines("function f() {\n" +
+                                      "    throw {\n" +
+                                      "        a: 1,\n" +
+                                      "        b: 2\n" +
+                                      "    }\n" +
+                                      "}\n");
+    assertArrayEquals(foundLinesInsideObjectArgsToThrow,
+                      [2, 3, 4],
+                      JSUnit.assertEquals);
+}
+
+
+function parseScriptForFunctionNames(script) {
+    const ast = Reflect.parse(script);
+    return Coverage.functionsForAST(ast);
+}
+
+function functionDeclarationsEqual(actual, expected) {
+    JSUnit.assertEquals(expected.name, actual.name);
+    JSUnit.assertEquals(expected.line, actual.line);
+    JSUnit.assertEquals(expected.n_params, actual.n_params);
+}
+
+function testFunctionsFoundForDeclarations() {
+    let foundFunctionDeclarations =
+        parseScriptForFunctionNames("function f1() {}\n" +
+                                    "function f2() {}\n" +
+                                    "function f3() {}\n");
+    assertArrayEquals(foundFunctionDeclarations,
+                      [
+                          { name: "f1", line: 1, n_params: 0 },
+                          { name: "f2", line: 2, n_params: 0 },
+                          { name: "f3", line: 3, n_params: 0 }
+                      ],
+                      functionDeclarationsEqual);
+}
+
+function testFunctionsFoundForNestedFunctions() {
+    let foundFunctions =
+        parseScriptForFunctionNames("function f1() {\n" +
+                                    "    let f2 = function() {\n" +
+                                    "        let f3 = function() {\n" +
+                                    "        }\n" +
+                                    "    }\n" +
+                                    "}\n");
+    assertArrayEquals(foundFunctions,
+                      [
+                          { name: "f1", line: 1, n_params: 0 },
+                          { name: null, line: 2, n_params: 0 },
+                          { name: null, line: 3, n_params: 0 }
+                      ],
+                      functionDeclarationsEqual);
+}
+
+function testFunctionsFoundOnSameLineButDifferentiatedOnArgs() {
+    /* Note the lack of newlines. This is all on
+     * one line */
+    let foundFunctionsOnSameLine =
+        parseScriptForFunctionNames("function f1() {" +
+                                    "    return (function (a) {" +
+                                    "        return function (a, b) {}" +
+                                    "    });" +
+                                    "}");
+    assertArrayEquals(foundFunctionsOnSameLine,
+                      [
+                          { name: "f1", line: 1, n_params: 0 },
+                          { name: null, line: 1, n_params: 1 },
+                          { name: null, line: 1, n_params: 2 }
+                      ],
+                      functionDeclarationsEqual);
+}
+
+function parseScriptForBranches(script) {
+    const ast = Reflect.parse(script);
+    return Coverage.branchesForAST(ast);
+}
+
+function branchInfoEqual(actual, expected) {
+    JSUnit.assertEquals(expected.point, actual.point);
+    assertArrayEquals(expected.exits, actual.exits, JSUnit.assertEquals);
+}
+
+function testBothBranchExitsFoundForSimpleBranch() {
+    let foundBranchExitsForSimpleBranch =
+        parseScriptForBranches("if (1) {\n" +
+                               "    let a = 1;\n" +
+                               "} else {\n" +
+                               "    let b = 2;\n" +
+                               "}\n");
+    assertArrayEquals(foundBranchExitsForSimpleBranch,
+                      [
+                          { point: 1, exits: [2, 4] }
+                      ],
+                      branchInfoEqual);
+}
+
+function testSingleExitFoundForBranchWithOneConsequent() {
+    let foundBranchExitsForSingleConsequentBranch =
+        parseScriptForBranches("if (1) {\n" +
+                               "    let a = 1.0;\n" +
+                               "}\n");
+    assertArrayEquals(foundBranchExitsForSingleConsequentBranch,
+                      [
+                          { point: 1, exits: [2] }
+                      ],
+                      branchInfoEqual);
+}
+
+function testMultipleBranchesFoundForNestedIfElseBranches() {
+    let foundBranchesForNestedIfElseBranches =
+        parseScriptForBranches("if (1) {\n" +
+                               "    let a = 1.0;\n" +
+                               "} else if (2) {\n" +
+                               "    let b = 2.0;\n" +
+                               "} else if (3) {\n" +
+                               "    let c = 3.0;\n" +
+                               "} else {\n" +
+                               "    let d = 4.0;\n" +
+                               "}\n");
+    assertArrayEquals(foundBranchesForNestedIfElseBranches,
+                      [
+                          /* the 'else if' line is actually an
+                           * exit for the first branch */
+                          { point: 1, exits: [2, 3] },
+                          { point: 3, exits: [4, 5] },
+                          /* 'else' by itself is not executable,
+                           * it is the block it contains whcih
+                           * is */
+                          { point: 5, exits: [6, 8] }
+                      ],
+                      branchInfoEqual);
+}
+
+
+function testSimpleTwoExitBranchWithoutBlocks() {
+    let foundBranches =
+        parseScriptForBranches("let a, b;\n" +
+                               "if (1)\n" +
+                               "    a = 1.0\n" +
+                               "else\n" +
+                               "    b = 2.0\n" +
+                               "\n");
+    assertArrayEquals(foundBranches,
+                      [
+                          { point: 2, exits: [3, 5] }
+                      ],
+                      branchInfoEqual);
+}
+
+function testNoBranchFoundIfConsequentWasEmpty() {
+    let foundBranches =
+        parseScriptForBranches("let a, b;\n" +
+                               "if (1);\n");
+    assertArrayEquals(foundBranches,
+                      [],
+                      branchInfoEqual);
+}
+
+function testSingleExitFoundIfOnlyAlternateExitDefined() {
+    let foundBranchesForOnlyAlternateDefinition =
+        parseScriptForBranches("let a, b;\n" +
+                               "if (1);\n" +
+                               "else\n" +
+                               "    a++;\n");
+    assertArrayEquals(foundBranchesForOnlyAlternateDefinition,
+                      [
+                          { point: 2, exits: [4] }
+                      ],
+                      branchInfoEqual);
+}
+
+function testImplicitBranchFoundForWhileStatement() {
+    let foundBranchesForWhileStatement =
+        parseScriptForBranches("while (1) {\n" +
+                               "    let a = 1;\n" +
+                               "}\n" +
+                               "let b = 2;");
+    assertArrayEquals(foundBranchesForWhileStatement,
+                      [
+                          { point: 1, exits: [2] }
+                      ],
+                      branchInfoEqual);
+}
+
+function testImplicitBranchFoundForDoWhileStatement() {
+    let foundBranchesForDoWhileStatement =
+        parseScriptForBranches("do {\n" +
+                               "    let a = 1;\n" +
+                               "} while (1)\n" +
+                               "let b = 2;");
+    assertArrayEquals(foundBranchesForDoWhileStatement,
+                      [
+                          { point: 1, exits: [2] }
+                      ],
+                      branchInfoEqual);
+}
+
+function testImplicitBranchFoundForDoWhileStatement() {
+    let foundBranchesForDoWhileStatement =
+        parseScriptForBranches("do {\n" +
+                               "    let a = 1;\n" +
+                               "} while (1)\n" +
+                               "let b = 2;");
+    assertArrayEquals(foundBranchesForDoWhileStatement,
+                      [
+                          /* For do-while loops the branch-point is
+                           * at the 'do' condition and not the
+                           * 'while' */
+                          { point: 1, exits: [2] }
+                      ],
+                      branchInfoEqual);
+}
+
+function testAllExitsFoundForCaseStatements() {
+    let foundExitsInCaseStatement =
+        parseScriptForBranches("let a = 1;\n" +
+                               "switch (1) {\n" +
+                               "case '1':\n" +
+                               "    a++;\n" +
+                               "    break;\n" +
+                               "case '2':\n" +
+                               "    a++\n" +
+                               "    break;\n" +
+                               "default:\n" +
+                               "    a++\n" +
+                               "    break;\n" +
+                               "}\n");
+    assertArrayEquals(foundExitsInCaseStatement,
+                      [
+                          /* There are three potential exits here */
+                          { point: 2, exits: [4, 7, 10] }
+                      ],
+                      branchInfoEqual);
+}
+
+function testAllExitsFoundForFallthroughCaseStatements() {
+    let foundExitsInCaseStatement =
+        parseScriptForBranches("let a = 1;\n" +
+                               "switch (1) {\n" +
+                               "case '1':\n" +
+                               "case 'a':\n" +
+                               "case 'b':\n" +
+                               "    a++;\n" +
+                               "    break;\n" +
+                               "case '2':\n" +
+                               "    a++\n" +
+                               "    break;\n" +
+                               "default:\n" +
+                               "    a++\n" +
+                               "    break;\n" +
+                               "}\n");
+    assertArrayEquals(foundExitsInCaseStatement,
+                      [
+                          /* There are three potential exits here */
+                          { point: 2, exits: [6, 9, 12] }
+                      ],
+                      branchInfoEqual);
+}
+
+function testAllNoExitsFoundForCaseStatementsWithNoopLabels() {
+    let foundExitsInCaseStatement =
+        parseScriptForBranches("let a = 1;\n" +
+                               "switch (1) {\n" +
+                               "case '1':\n" +
+                               "case '2':\n" +
+                               "default:\n" +
+                               "}\n");
+    assertArrayEquals(foundExitsInCaseStatement,
+                      [],
+                      branchInfoEqual);
+}
+
+
+function testGetNumberOfLinesInScript() {
+    let script = "\n\n";
+    let number = Coverage._getNumberOfLinesForScript(script);
+    JSUnit.assertEquals(3, number);
+}
+
+function testZeroExpressionLinesToCounters() {
+    let expressionLines = [];
+    let nLines = 1;
+    let counters = Coverage._expressionLinesToCounters(expressionLines, nLines);
+
+    assertArrayEquals([undefined], counters, JSUnit.assertEquals);
+}
+
+function testSingleExpressionLineToCounters() {
+    let expressionLines = [1, 2];
+    let nLines = 4;
+    let counters = Coverage._expressionLinesToCounters(expressionLines, nLines);
+
+    log(counters);
+    assertArrayEquals([undefined, 0, 0, undefined], counters, JSUnit.assertEquals);
+}
+
+const MockFoundBranches = [
+    {
+        point: 5,
+        exits: [6, 8]
+    },
+    {
+        point: 1,
+        exits: [2, 4]
+    }
+];
+
+const MockNLines = 9;
+
+function testGetsSameNumberOfCountersAsNLines() {
+    let counters = Coverage._branchesToBranchCounters(MockFoundBranches, MockNLines);
+    JSUnit.assertEquals(MockNLines, counters.length);
+}
+
+function testEmptyArrayReturnedForNoBranches() {
+    let counters = Coverage._branchesToBranchCounters([], 1);
+    assertArrayEquals([undefined], counters, JSUnit.assertEquals);
+}
+
+function testBranchesOnLinesForArrayIndicies() {
+    let counters = Coverage._branchesToBranchCounters(MockFoundBranches, MockNLines);
+    JSUnit.assertNotEquals(undefined, counters[1]);
+    JSUnit.assertNotEquals(undefined, counters[5]);
+}
+
+function testExitsSetForBranch() {
+    let counters = Coverage._branchesToBranchCounters(MockFoundBranches, MockNLines);
+    let countersForFirstBranch = counters[1];
+
+    assertArrayEquals(countersForFirstBranch.exits,
+                      [
+                          { line: 2, hitCount: 0 },
+                          { line: 4, hitCount: 0 }
+                      ],
+                      function(expectedExit, actualExit) {
+                          JSUnit.assertEquals(expectedExit.line, actualExit.line);
+                          JSUnit.assertEquals(expectedExit.hitCount, actualExit.hitCount);
+                      });
+}
+
+function testLastExitIsSetToHighestExitStartLine() {
+    let counters = Coverage._branchesToBranchCounters(MockFoundBranches, MockNLines);
+    let countersForFirstBranch = counters[1];
+
+    JSUnit.assertEquals(4, countersForFirstBranch.lastExit);
+}
+
+function testHitIsAlwaysInitiallyFalse() {
+    let counters = Coverage._branchesToBranchCounters(MockFoundBranches, MockNLines);
+    let countersForFirstBranch = counters[1];
+
+    JSUnit.assertEquals(false, countersForFirstBranch.hit);
+}
+
+function testFunctionForKeyFromFunctionWithNameMatchesSchema() {
+    let expectedFunctionKey = 'f:1:2';
+    let functionKeyForFunctionName =
+        Coverage._getFunctionKeyFromReflectedFunction({ name: 'f',
+                                                        line: 1,
+                                                        n_params: 2 });
+
+    JSUnit.assertEquals(expectedFunctionKey, functionKeyForFunctionName);
+}
+
+function testFunctionKeyFromFunctionWithoutNameIsAnonymous() {
+    let expectedFunctionKey = '(anonymous):2:3';
+    let functionKeyForAnonymousFunction =
+        Coverage._getFunctionKeyFromReflectedFunction({ name: null,
+                                                        line: 2,
+                                                        n_params: 3 });
+
+    JSUnit.assertEquals(expectedFunctionKey, functionKeyForAnonymousFunction);
+}
+
+
+
+function testFunctionCounterMapReturnedForFunctionKeys() {
+    let func = {
+        name: 'name',
+        line: 1,
+        n_params: 0
+    };
+
+    let functionKey = Coverage._getFunctionKeyFromReflectedFunction(func);
+    let functionCounters = Coverage._functionsToFunctionCounters([func]);
+
+    JSUnit.assertEquals(0, functionCounters[functionKey].hitCount);
+}
+
+function testKnownFunctionsArrayPopulatedForFunctions() {
+    let functions = [
+        { line: 1 },
+        { line: 2 }
+    ];
+
+    let knownFunctionsArray = Coverage._populateKnownFunctions(functions, 4);
+
+    assertArrayEquals(knownFunctionsArray,
+                      [undefined, true, true, undefined],
+                      JSUnit.assertEquals);
+}
+
+function testIncrementFunctionCountersForFunctionOnSameExecutionStartLine() {
+    let functionKey = 'f:1:0';
+    let functionCounters = {};
+
+    functionCounters[functionKey] = { hitCount: 0 };
+
+    Coverage._incrementFunctionCounters(functionCounters, null, 'f', 1, 0);
+
+    JSUnit.assertEquals(functionCounters[functionKey].hitCount, 1);
+}
+
+function testIncrementFunctionCountersForFunctionOnEarlierStartLine() {
+    let functions = [
+        {
+            name: 'name',
+            line: 1,
+            n_params: 0
+        }
+    ];
+    let functionKey = Coverage._getFunctionKeyFromReflectedFunction(functions[0]);
+    let knownFunctionsArray = Coverage._populateKnownFunctions(functions, 3);
+    let functionCounters = Coverage._functionsToFunctionCounters(functions);
+
+    /* We're entering at line two, but the function definition was actually
+     * at line one */
+    Coverage._incrementFunctionCounters(functionCounters, knownFunctionsArray, 'name', 2, 0);
+
+    JSUnit.assertEquals(functionCounters[functionKey].hitCount, 1);
+}
+
+function testIncrementFunctionCountersThrowsErrorOnUnexpectedFunction() {
+    let functions = [
+        {
+            name: 'name',
+            line: 1,
+            n_params: 0
+        }
+    ];
+    let functionKey = Coverage._getFunctionKeyFromReflectedFunction(functions[0]);
+    let knownFunctionsArray = Coverage._populateKnownFunctions(functions, 3);
+    let functionCounters = Coverage._functionsToFunctionCounters(functions);
+
+    /* We're entering at line two, but the function definition was actually
+     * at line one */
+    JSUnit.assertRaises(function() {
+        Coverage._incrementFunctionCounters(functionCounters,
+                                            knownFunctionsArray,
+                                            'doesnotexist',
+                                            2,
+                                            0);
+    });
+}
+
+function testIncrementExpressionCountersThrowsIfLineOutOfRange() {
+    let expressionCounters = [
+        undefined,
+        0
+    ];
+
+    JSUnit.assertRaises(function() {
+        Coverage._incrementExpressionCounters(expressionCounters, 2);
+    });
+}
+
+function testIncrementExpressionCountersIncrementsIfInRange() {
+    let expressionCounters = [
+        undefined,
+        0
+    ];
+
+    Coverage._incrementExpressionCounters(expressionCounters, 1);
+    JSUnit.assertEquals(1, expressionCounters[1]);
+}
+
+function testWarnsIfWeHitANonExecutableLine() {
+    let expressionCounters = [
+        undefined,
+        0,
+        undefined
+    ];
+
+    let wasCalledContainer = { calledWith: undefined };
+    let reporter = function(cont) {
+        let container = cont;
+        return function(line) {
+            container.calledWith = line;
+        };
+    } (wasCalledContainer);
+
+    Coverage._incrementExpressionCounters(expressionCounters, 2, reporter);
+    JSUnit.assertEquals(wasCalledContainer.calledWith, 2);
+    JSUnit.assertEquals(expressionCounters[2], 1);
+}
+
+function testBranchTrackerSetsBranchToHitOnPointExecution() {
+    let branchCounters = Coverage._branchesToBranchCounters(MockFoundBranches, MockNLines);
+    let branchTracker = new Coverage._BranchTracker(branchCounters);
+
+    branchTracker.incrementBranchCounters(1);
+
+    JSUnit.assertEquals(true, branchCounters[1].hit);
+}
+
+function testBranchTrackerSetsExitToHitOnExecution() {
+    let branchCounters = Coverage._branchesToBranchCounters(MockFoundBranches, MockNLines);
+    let branchTracker = new Coverage._BranchTracker(branchCounters);
+
+    branchTracker.incrementBranchCounters(1);
+    branchTracker.incrementBranchCounters(2);
+
+    JSUnit.assertEquals(1, branchCounters[1].exits[0].hitCount);
+}
+
+function testBranchTrackerFindsNextBranch() {
+    let branchCounters = Coverage._branchesToBranchCounters(MockFoundBranches, MockNLines);
+    let branchTracker = new Coverage._BranchTracker(branchCounters);
+
+    branchTracker.incrementBranchCounters(1);
+    branchTracker.incrementBranchCounters(2);
+    branchTracker.incrementBranchCounters(5);
+
+    JSUnit.assertEquals(true, branchCounters[5].hit);
+}
+
+function testConvertFunctionCountersToArray() {
+    let functionsMap = {};
+
+    functionsMap['(anonymous):2:0'] = { hitCount: 1 };
+    functionsMap['name:1:0'] = { hitCount: 0 };
+
+    let expectedFunctionCountersArray = [
+        { name: '(anonymous):2:0', hitCount: 1 },
+        { name: 'name:1:0', hitCount: 0 }
+    ];
+
+    let convertedFunctionCounters = Coverage._convertFunctionCountersToArray(functionsMap);
+
+    assertArrayEquals(expectedFunctionCountersArray,
+                      convertedFunctionCounters,
+                      function(expected, actual) {
+                          JSUnit.assertEquals(expected.name, actual.name);
+                          JSUnit.assertEquals(expected.hitCount, actual.hitCount);
+                      });
+}
+
+function testConvertFunctionCountersToArrayIsSorted() {
+    let functionsMap = {};
+
+    functionsMap['name:1:0'] = { hitCount: 0 };
+    functionsMap['(anonymous):2:0'] = { hitCount: 1 };
+
+    let expectedFunctionCountersArray = [
+        { name: '(anonymous):2:0', hitCount: 1 },
+        { name: 'name:1:0', hitCount: 0 }
+    ];
+
+    let convertedFunctionCounters = Coverage._convertFunctionCountersToArray(functionsMap);
+
+    assertArrayEquals(expectedFunctionCountersArray,
+                      convertedFunctionCounters,
+                      function(expected, actual) {
+                          JSUnit.assertEquals(expected.name, actual.name);
+                          JSUnit.assertEquals(expected.hitCount, actual.hitCount);
+                      });
+}
+
+const MockFiles = [
+    {
+        filename: 'filename',
+        getContents: function() {
+            return "let f = function() { return 1; };"
+        }
+    }
+];
+
+
+function testCoverageStatisticsContainerFetchesValidStatisticsForFile() {
+    let container = new Coverage.CoverageStatisticsContainer(MockFiles);
+    let statistics = container.fetchStatistics('filename');
+
+    JSUnit.assertNotEquals(undefined, statistics);
+}
+
+function testCoverageStatisticsContainerThrowsForNonExistingFile() {
+    let container = new Coverage.CoverageStatisticsContainer(MockFiles);
+
+    JSUnit.assertRaises(function() {
+        container.fetchStatistics('nonexistent');
+    });
+
+}
+
+JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
diff --git a/modules/coverage.js b/modules/coverage.js
new file mode 100644
index 0000000..bd5a4c5
--- /dev/null
+++ b/modules/coverage.js
@@ -0,0 +1,761 @@
+/*
+ * Copyright (c) 2014 Endless Mobile, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ *
+ * Authored By: Sam Spilsbury <sam endlessm com>
+ */
+
+function getSubNodesForNode(node) {
+    let subNodes = [];
+    switch (node.type) {
+    /* These statements have a single body */
+    case 'LabelledStatement':
+    case 'WithStatement':
+    case 'LetStatement':
+    case 'ForInStatement':
+    case 'ForOfStatement':
+    case 'FunctionDeclaration':
+    case 'FunctionExpression':
+    case 'ArrowExpression':
+    case 'CatchClause':
+        subNodes.push(node.body);
+        break;
+    case 'WhileStatement':
+    case 'DoWhileStatement':
+        subNodes.push(node.body);
+        subNodes.push(node.test);
+        break;
+    case 'ForStatement':
+        if (node.init !== null)
+            subNodes.push(node.init);
+        if (node.test !== null)
+            subNodes.push(node.test);
+        if (node.update !== null)
+            subNodes.push(node.update);
+
+        subNodes.push(node.body);
+        break;
+    case 'BlockStatement':
+        Array.prototype.push.apply(subNodes, node.body);
+        break;
+    case 'ThrowStatement':
+    case 'ReturnStatement':
+        if (node.argument !== null)
+            subNodes.push(node.argument);
+        break;
+    case 'ExpressionStatement':
+        subNodes.push(node.expression);
+        break;
+    case 'AssignmentExpression':
+        subNodes.push(node.left, node.right);
+        break;
+    case 'ObjectExpression':
+        node.properties.forEach(function(prop) {
+            subNodes.push(prop.value);
+        });
+        break;
+    /* It is very possible that there might be something
+     * interesting in the function arguments, so we need to
+     * walk them too */
+    case 'NewExpression':
+    case 'CallExpression':
+        Array.prototype.push.apply(subNodes, node.arguments);
+        subNodes.push(node.callee);
+        break;
+    /* These statements might have multiple different bodies
+     * depending on whether or not they were entered */
+    case 'IfStatement':
+        subNodes = [node.test, node.consequent];
+        if (node.alternate !== null)
+            subNodes.push(node.alternate);
+        break;
+    case 'TryStatement':
+        subNodes = [node.block];
+        if (node.handler !== null)
+            subNodes.push(node.handler);
+        if (node.finalizer !== null)
+            subNodes.push(node.finalizer);
+        break;
+    case 'SwitchStatement':
+        for (let caseClause of node.cases) {
+            caseClause.consequent.forEach(function(expression) {
+                subNodes.push(expression);
+            });
+        }
+
+        break;
+    /* Variable declarations might be initialized to
+     * some expression, so traverse the tree and see if
+     * we can get into the expression */
+    case 'VariableDeclaration':
+        node.declarations.forEach(function (declarator) {
+            if (declarator.init !== null)
+                subNodes.push(declarator.init);
+        });
+
+        break;
+    }
+
+    return subNodes;
+}
+
+function collectForSubNodes(subNodes, collector) {
+    let result = [];
+    if (subNodes !== undefined &&
+        subNodes.length > 0) {
+
+        subNodes.forEach(function(node) {
+            let nodeResult = collector(node);
+            if (nodeResult !== undefined)
+                Array.prototype.push.apply(result, nodeResult);
+
+            let subNodeResults = collectForSubNodes(getSubNodesForNode(node),
+                                                    collector);
+
+            Array.prototype.push.apply(result, subNodeResults);
+        });
+    }
+
+    return result;
+}
+
+/* Unfortunately, the Reflect API doesn't give us enough information to
+ * uniquely identify a function. A function might be anonymous, in which
+ * case the JS engine uses some heurisitics to get a unique string identifier
+ * but that isn't available to us here.
+ *
+ * There's also the edge-case where functions with the same name might be
+ * defined within the same scope, or multiple anonymous functions might
+ * be defined on the same line. In that case, it will look like we entered
+ * the same function multiple times since we can't get column information
+ * from the engine-side.
+ *
+ * For instance:
+ *
+ * 1. function f() {
+ *       function f() {
+ *       }
+ *    }
+ *
+ * 2. let a = function() { function(a, b) {} };
+ *
+ * 3. let a = function() { function () {} }
+ *
+ * We can work-around case 1 by using the line numbers to get a unique identifier.
+ * We can work-around case 2 by using the arguments length to get a unique identifier
+ * We can't work-around case 3. The best thing we can do is warn that coverage
+ * reports might be inaccurate as a result */
+function functionsForNode(node) {
+    let functionNames = [];
+    switch (node.type) {
+    case 'FunctionDeclaration':
+    case 'FunctionExpression':
+        if (node.id !== null) {
+            functionNames.push({ name: node.id.name,
+                                 line: node.loc.start.line,
+                                 n_params: node.params.length });
+        }
+        /* If the function wasn't found, we just push a name
+         * that looks like 'function:lineno' to signify that
+         * this was an anonymous function. If the coverage tool
+         * enters a function with no name (but a line number)
+         * then it can probably use this information to
+         * figure out which function it was */
+        else {
+            functionNames.push({ name: null,
+                                 line: node.loc.start.line,
+                                 n_params: node.params.length });
+        }
+    }
+
+    return functionNames;
+}
+
+function functionsForAST(ast) {
+    return collectForSubNodes(ast.body, functionsForNode);
+}
+
+/* If a branch' consequent is a block statement, there's
+ * a chance that it could start on the same line, although
+ * that's not where execution really starts. If it is
+ * a block statement then handle the case and go
+ * to the first line where execution starts */
+function getBranchExitStartLine(branchBodyNode) {
+    switch (branchBodyNode.type) {
+    case 'BlockStatement':
+        /* Hit a block statement, but nothing inside, can never
+         * be executed, tell the upper level to move on to the next
+         * statement */
+        if (branchBodyNode.body.length === 0)
+            return -1;
+
+        /* Handle the case where we have nested block statements
+         * that never actually get to executable code by handling
+         * all statements within a block */
+        for (let statement of branchBodyNode.body) {
+            let startLine = getBranchExitStartLine(statement);
+            if (startLine !== -1)
+                return startLine;
+        }
+
+        /* Couldn't find an executable line inside this block */
+        return -1;
+
+    case 'SwitchCase':
+        /* Hit a switch, but nothing inside, can never
+         * be executed, tell the upper level to move on to the next
+         * statement */
+        if (branchBodyNode.consequent.length === 0)
+            return -1;
+
+        /* Handle the case where we have nested block statements
+         * that never actually get to executable code by handling
+         * all statements within a block */
+        for (let statement of branchBodyNode.consequent) {
+            let startLine = getBranchExitStartLine(statement);
+            if (startLine !== -1) {
+                return startLine;
+            }
+        }
+
+        /* Couldn't find an executable line inside this block */
+        return -1;
+    /* These types of statements are never executable */
+    case 'EmptyStatement':
+    case 'LabelledStatement':
+        return -1;
+    default:
+        break;
+    }
+
+    return branchBodyNode.loc.start.line;
+}
+
+function branchesForNode(node) {
+    let branches = [];
+
+    let branchExitNodes = [];
+    switch (node.type) {
+    case 'IfStatement':
+        branchExitNodes.push(node.consequent);
+        if (node.alternate !== null)
+            branchExitNodes.push(node.alternate);
+        break;
+    case 'WhileStatement':
+    case 'DoWhileStatement':
+        branchExitNodes.push(node.body);
+        break;
+    case 'SwitchStatement':
+
+        /* The case clauses by themselves are never executable
+         * so find the actual exits */
+        Array.prototype.push.apply(branchExitNodes, node.cases);
+        break;
+    default:
+        break;
+    }
+
+    let branchExitStartLines = branchExitNodes.map(getBranchExitStartLine);
+    branchExitStartLines = branchExitStartLines.filter(function(line) {
+        return line !== -1;
+    });
+
+    /* Branch must have at least one exit */
+    if (branchExitStartLines.length) {
+        branches.push({ point: node.loc.start.line,
+                        exits: branchExitStartLines });
+    }
+
+    return branches;
+}
+
+function branchesForAST(ast) {
+    return collectForSubNodes(ast.body, branchesForNode);
+}
+
+function expressionLinesForNode(statement) {
+    let expressionLines = [];
+
+    let expressionNodeTypes = ['Expression',
+                               'Declaration',
+                               'Statement',
+                               'Clause',
+                               'Literal',
+                               'Identifier'];
+
+    if (expressionNodeTypes.some(function(type) {
+            return statement.type.indexOf(type) !== -1;
+        })) {
+
+        /* These expressions aren't executable on their own */
+        switch (statement.type) {
+        case 'FunctionDeclaration':
+        case 'LiteralExpression':
+            break;
+        /* Perplexingly, an empty block statement is actually executable,
+         * push it if it is */
+        case 'BlockStatement':
+            if (statement.body.length !== 0)
+                break;
+            expressionLines.push(statement.loc.start.line);
+            break;
+        default:
+            expressionLines.push(statement.loc.start.line);
+            break;
+        }
+    }
+
+    return expressionLines;
+}
+
+function deduplicate(list) {
+    return list.filter(function(elem, pos, self) {
+        return self.indexOf(elem) === pos;
+    });
+}
+
+function expressionLinesForAST(ast) {
+    let allExpressions = collectForSubNodes(ast.body, expressionLinesForNode);
+    allExpressions = deduplicate(allExpressions);
+
+    return allExpressions;
+}
+
+function _getNumberOfLinesForScript(scriptContents) {
+    let scriptLines = scriptContents.split("\n");
+    let scriptLineCount = scriptLines.length;
+
+    return scriptLineCount;
+}
+
+/*
+ * 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 undefined 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.
+ *
+ */
+function _expressionLinesToCounters(expressionLines, nLines) {
+    expressionLines.sort(function(left, right) { return left - right; });
+
+    let expressionLinesIndex = 0;
+    let counters = new Array(nLines);
+
+    if (expressionLines.length === 0)
+        return counters;
+
+    for (let i = 1; i < counters.length; i++) {
+        if (expressionLines[expressionLinesIndex] == i) {
+            counters[i] = 0;
+            expressionLinesIndex++;
+        }
+    }
+
+    return counters;
+}
+
+/* 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 */
+function _branchesToBranchCounters(branches, nLines) {
+    branches.sort(function(left, right) {
+        return left.point - right.point;
+    });
+
+    let branchIndex = 0;
+    let counters = new Array(nLines);
+
+    if (branches.length === 0)
+        return counters;
+
+    for (let i = 1; i < counters.length; i++) {
+        let branch = branches[branchIndex];
+        let branchPoint = branch.point;
+
+        if (branchPoint == i) {
+            counters[i] = {
+                point: branchPoint,
+                exits: branch.exits.map(function(exit) {
+                    return {
+                        line: exit,
+                        hitCount: 0
+                    };
+                }),
+                lastExit: (function() {
+                    let lastExitLine = 0;
+                    for (let exit of branch.exits) {
+                        if (lastExitLine < exit)
+                            lastExitLine = exit;
+                    }
+
+                    return lastExitLine;
+                })(),
+                hit: false
+            };
+
+            if (++branchIndex >= branches.length)
+                break;
+        }
+    }
+
+    return counters;
+}
+
+function _getFunctionKeyFromReflectedFunction(func) {
+    let name = func.name !== null ? func.name : '(anonymous)';
+    let line = func.line;
+    let n_params = func.n_params;
+
+    return name + ':' + line + ':' + n_params;
+}
+
+function _functionsToFunctionCounters(functions) {
+    let functionCounters = {};
+
+    functions.forEach(function(func) {
+        let functionKey = _getFunctionKeyFromReflectedFunction(func);
+        functionCounters[functionKey] = {
+            hitCount: 0
+        };
+    });
+
+    return functionCounters;
+}
+
+function _populateKnownFunctions(functions, nLines) {
+    let knownFunctions = new Array(nLines);
+
+    functions.forEach(function(func) {
+        knownFunctions[func.line] = true;
+    });
+
+    return knownFunctions;
+}
+
+/**
+ * _createStatisticsForFoundFilename
+ *
+ * filename: a string describing a filename, potentially in
+ * pendingFiles
+ * pendingFiles: an array of an object with the following properties:
+ * - filename: a string describing its filename
+ * - getContents: a method to get the contents of a file
+ */
+function _createStatisticsForFoundFilename(filename, pendingFiles) {
+    let pendingIndex = -1;
+
+    for (let i = 0; i < pendingFiles.length; i++) {
+        if (filename == pendingFiles[i].filename) {
+            pendingIndex = i;
+            break;
+        }
+    }
+
+    if (pendingIndex !== -1) {
+        let file = (pendingFiles.splice(pendingIndex, 1))[0];
+
+        let contents = file.getContents();
+        let reflection = Reflect.parse(contents);
+        let nLines = _getNumberOfLinesForScript(contents);
+
+        let functions = functionsForAST(reflection);
+
+        return {
+            contents: contents,
+            nLines: nLines,
+            expressionCounters: _expressionLinesToCounters(expressionLinesForAST(reflection), nLines),
+            branchCounters: _branchesToBranchCounters(branchesForAST(reflection), nLines),
+            functionCounters: _functionsToFunctionCounters(functions),
+            linesWithKnownFunctions: _populateKnownFunctions(functions, nLines)
+        }
+    }
+
+    return null;
+}
+
+/**
+ * _incrementFunctionCounters
+ *
+ * functionCounters: An object which is a key-value pair with the following schema:
+ * {
+ *      "key" : { line, hitCount }
+ * }
+ * linesWithKnownFunctions: An array of either "true" or undefined, with true set to
+ * each element corresponding to a line that we know has a function on it.
+ * name: The name of the function or "(anonymous)" if it has no name
+ * line: The line at which execution first started on this function.
+ * nArgs: The number of arguments this function has.
+ */
+function _incrementFunctionCounters(functionCounters,
+                                    linesWithKnownFunctions,
+                                    name,
+                                    line,
+                                    nArgs) {
+    let functionKey = name + ':' + line + ':' + nArgs;
+    let functionCountersForKey = functionCounters[functionKey];
+
+    /* Its possible that the JS Engine might enter a funciton
+     * at an executable line which is a little bit past the
+     * actual definition. Roll backwards until we reach the
+     * last known function definition line which we kept
+     * track of earlier to see if we can find this function first */
+    if (functionCountersForKey === undefined) {
+        do {
+            --line;
+            functionKey = name + ':' + line + ':' + nArgs;
+            functionCountersForKey = functionCounters[functionKey];
+        } while(linesWithKnownFunctions[line] !== true)
+    }
+
+    if (functionCountersForKey !== undefined) {
+        functionCountersForKey.hitCount++;
+    } else {
+        throw new Error("expected Reflect to find function " + functionKey);
+    }
+}
+
+/**
+ * _incrementExpressionCounters
+ *
+ * expressonCounters: An array of either a hit count for a found
+ * executable line or undefined for a known non-executable line.
+ * line: an executed line
+ * reporter: A function a single integer to report back when
+ * we executed lines that we didn't expect
+ */
+function _incrementExpressionCounters(expressionCounters,
+                                      offsetLine,
+                                      reporter) {
+    let expressionCountersLen = expressionCounters.length;
+
+    if (offsetLine >= expressionCountersLen)
+        throw new Error("Executed line " + offsetLine + " which was past the highest-found line " + 
expressionCountersLen);
+
+    /* If this happens it is not a huge problem - though it does
+     * mean that the reflection machinery 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 (expressionCounters[offsetLine] === undefined) {
+        if (reporter !== undefined)
+            reporter(offsetLine);
+
+        expressionCounters[offsetLine] = 0;
+    }
+
+    expressionCounters[offsetLine]++;
+}
+
+function _BranchTracker(branchCounters) {
+    this._branchCounters = branchCounters;
+    this._activeBranch = undefined;
+
+    this.incrementBranchCounters = function(offsetLine) {
+        /* Set branch exits or find a new active branch */
+        let activeBranch = this._activeBranch;
+        if (activeBranch !== undefined) {
+            activeBranch.exits.forEach(function(exit) {
+                if (exit.line === offsetLine) {
+                    exit.hitCount++;
+                }
+            });
+
+            /* Only set the active branch to undefined once we're
+             * completely outside of it, since we might be in a case statement where
+             * we need to check every possible option before jumping to an
+             * exit */
+            if (offsetLine >= activeBranch.lastExit)
+                this._activeBranch = undefined;
+        }
+
+        let nextActiveBranch = branchCounters[offsetLine];
+        if (nextActiveBranch !== undefined) {
+            this._activeBranch = nextActiveBranch;
+            this._activeBranch.hit = true;
+        }
+    }
+}
+
+function _convertFunctionCountersToArray(functionCounters) {
+    let arrayReturn = [];
+    /* functionCounters is an object so convert it to
+     * an array-of-object using the key as a property
+     * of that object */
+    for (let key in functionCounters) {
+        let func = functionCounters[key];
+        arrayReturn.push({ name: key,
+                           hitCount: func.hitCount });
+    }
+
+    arrayReturn.sort(function(left, right) {
+        if (left.name < right.name)
+            return -1;
+        else if (left.name > right.name)
+            return 1;
+        else
+            return 0;
+    });
+    return arrayReturn;
+}
+
+function CoverageStatisticsContainer(files) {
+
+    this._pendingFiles = [];
+    this._coveredFiles = {};
+
+    this._ensureStatisticsFor = function(filename) {
+        let statistics = _createStatisticsForFoundFilename(filename, this._pendingFiles);
+        if (statistics)
+            this._coveredFiles[filename] = statistics;
+    };
+
+    this.fetchStatistics = function(filename) {
+        this._ensureStatisticsFor(filename);
+        let statistics = this._coveredFiles[filename];
+        if (statistics === undefined)
+            throw new Error('Not tracking statistics for ' + filename);
+        return statistics;
+    };
+
+    for (let file of files)
+        this._pendingFiles.push(file);
+
+    return this;
+}
+
+/**
+ * Main class tying together the Debugger object and CoverageStatisticsContainer.
+ *
+ * It isn't poissible to unit test this class because it depends on running
+ * Debugger which in turn depends on objects injected in from another compartment */
+function CoverageStatistics(files) {
+
+    /* 'debuggee' comes from the invocation from
+     * a separate compartment inside of coverage.cpp */
+    this.container = new CoverageStatisticsContainer(files);
+    this.dbg = new Debugger(debuggee);
+    this.dbg.fetchStatistics = function(statisticsContainer) {
+        let container = statisticsContainer;
+        return function (filename) {
+            return container.fetchStatistics(filename);
+        }
+    } (this.container);
+
+    this.getNumberOfLinesFor = function(filename) {
+        return this.container.fetchStatistics(filename).nLines;
+    };
+
+    this.getExecutedLinesFor = function(filename) {
+        return this.container.fetchStatistics(filename).expressionCounters;
+    };
+
+    this.getBranchesFor = function(filename) {
+        return this.container.fetchStatistics(filename).branchCounters;
+    };
+
+    this.getFunctionsFor = function(filename) {
+        let functionCounters = this.container.fetchStatistics(filename).functionCounters;
+
+        return _convertFunctionCountersToArray(functionCounters);
+    };
+
+    this.dbg.onEnterFrame = function(frame) {
+        let statistics;
+
+        try {
+            statistics = this.fetchStatistics(frame.script.url);
+        } catch (e) {
+            /* We don't care about this frame, return */
+            return undefined;
+        }
+
+        /* Log function calls */
+        if (frame.callee !== null && frame.callee.callable) {
+            let name = frame.callee.name ? frame.callee.name : "(anonymous)";
+            let line = frame.script.getOffsetLine(frame.offset);
+            let nArgs = frame.callee.parameterNames.length;
+
+            _incrementFunctionCounters(statistics.functionCounters,
+                                       statistics.linesWithKnownFunctions,
+                                       name,
+                                       line,
+                                       nArgs);
+        }
+
+        /* Upon entering the frame, the active branch is always inactive */
+        frame._branchTracker = new _BranchTracker(statistics.branchCounters);
+
+        /* Set single-step hook */
+        frame.onStep = function() {
+            /* Line counts */
+            let offset = this.offset;
+            let offsetLine = this.script.getOffsetLine(offset);
+
+            _incrementExpressionCounters(statistics.expressionCounters,
+                                         offsetLine,
+                                         function(line) {
+                                             Warning("executed " +
+                                                     frame.script.url +
+                                                     ":" +
+                                                     offsetLine +
+                                                     " which we thought wasn't executable");
+                                         });
+
+            this._branchTracker.incrementBranchCounters(offsetLine);
+        }
+
+        return undefined;
+    };
+
+    return this;
+}
+
diff --git a/modules/modules.gresource.xml.in b/modules/modules.gresource.xml.in
index 8fccdfd..30aab0c 100644
--- a/modules/modules.gresource.xml.in
+++ b/modules/modules.gresource.xml.in
@@ -11,6 +11,7 @@
     @GTK_OVERRIDE@
 
     <file>modules/cairo.js</file>
+    <file>modules/coverage.js</file>
     <file>modules/gettext.js</file>
     <file>modules/lang.js</file>
     <file>modules/mainloop.js</file>
diff --git a/test/gjs-test-coverage.cpp b/test/gjs-test-coverage.cpp
new file mode 100644
index 0000000..30a0726
--- /dev/null
+++ b/test/gjs-test-coverage.cpp
@@ -0,0 +1,1491 @@
+/*
+ * 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/coverage.h>
+
+typedef struct _GjsCoverageFixture {
+    GjsContext    *context;
+    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->coverage = gjs_coverage_new(coverage_paths,
+                                         fixture->context);
+
+    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->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.context);
+    g_object_unref(fixture->base_fixture.coverage);
+    const char *search_paths[] = {
+        fixture->base_fixture.temporary_js_script_directory_name,
+        NULL
+    };
+
+    fixture->base_fixture.context = gjs_context_new_with_search_path((char **) search_paths);
+    fixture->base_fixture.coverage =
+        gjs_coverage_new(coverage_scripts,
+                         fixture->base_fixture.context);
+
+    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);
+}
+
+static void
+silence_log_func(const gchar    *domain,
+                 GLogLevelFlags  log_level,
+                 const gchar    *message,
+                 gpointer        user_data)
+{
+}
+
+static void
+test_expected_entry_not_written_for_nonexistent_file(gpointer      fixture_data,
+                                                        gconstpointer user_data)
+{
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+
+    const char *coverage_paths[] = {
+        "doesnotexist",
+        NULL
+    };
+
+    g_object_unref(fixture->base_fixture.coverage);
+    fixture->base_fixture.coverage = gjs_coverage_new(coverage_paths,
+                                                      fixture->base_fixture.context);
+
+    /* Temporarily disable fatal mask and silence warnings */
+    GLogLevelFlags old_flags = g_log_set_always_fatal((GLogLevelFlags) G_LOG_LEVEL_ERROR);
+    GLogFunc old_log_func = g_log_set_default_handler(silence_log_func, NULL);
+
+    char *coverage_data_contents =
+        eval_script_and_get_coverage_data(fixture->base_fixture.context,
+                                          fixture->base_fixture.coverage,
+                                          "doesnotexist",
+                                          fixture->output_file_directory,
+                                          NULL);
+
+    g_log_set_always_fatal(old_flags);
+    g_log_set_default_handler(old_log_func, NULL);
+
+    char *temporary_js_script_basename =
+        g_filename_display_basename("doesnotexist");
+
+    g_assert(!(coverage_data_contains_value_for_key(coverage_data_contents,
+                                                    "SF:",
+                                                    temporary_js_script_basename)));
+
+    g_free(temporary_js_script_basename);
+}
+
+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;
+}
+
+/* For functions with whitespace between their definition and
+ * first executable line, its possible that the JS engine might
+ * enter their frame a little later in the script than where their
+ * definition starts. We need to handle that case */
+static void
+test_function_hit_counts_for_big_functions_written_to_coverage_data(gpointer      fixture_data,
+                                                                    gconstpointer user_data)
+{
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+
+    const char *script_with_executed_functions =
+            "function f(){\n"
+            "\n"
+            "\n"
+            "var x = 1;\n"
+            "}\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):6: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);
+}
+
+/* For functions which start executing at a function declaration
+ * we also need to make sure that we roll back to the real function, */
+static void
+test_function_hit_counts_for_little_functions_written_to_coverage_data(gpointer      fixture_data,
+                                                                       gconstpointer user_data)
+{
+    GjsCoverageToSingleOutputFileFixture *fixture = (GjsCoverageToSingleOutputFileFixture *) fixture_data;
+
+    const char *script_with_executed_functions =
+            "function f(){\n"
+            "var x = function(){};\n"
+            "}\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", 0 },
+        { "(anonymous):4: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_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.context);
+    g_object_unref(fixture->base_fixture.base_fixture.coverage);
+    const char *search_paths[] = {
+        fixture->base_fixture.base_fixture.temporary_js_script_directory_name,
+        NULL
+    };
+
+    fixture->base_fixture.base_fixture.context = gjs_context_new_with_search_path((char **) search_paths);
+    fixture->base_fixture.base_fixture.coverage = gjs_coverage_new(coverage_paths,
+                                                                   
fixture->base_fixture.base_fixture.context);
+
+    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/entry_not_written_for_nonexistent_file",
+                         &coverage_to_single_output_fixture,
+                         test_expected_entry_not_written_for_nonexistent_file,
+                         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/big_function_hit_counts_written_to_coverage_data",
+                         &coverage_to_single_output_fixture,
+                         test_function_hit_counts_for_big_functions_written_to_coverage_data,
+                         NULL);
+    add_test_for_fixture("/gjs/coverage/little_function_hit_counts_written_to_coverage_data",
+                         &coverage_to_single_output_fixture,
+                         test_function_hit_counts_for_little_functions_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
new file mode 100644
index 0000000..a7414a6
--- /dev/null
+++ b/test/gjs-tests-add-funcs.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright © 2013 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_TESTS_ADD_FUNCS_H
+#define GJS_TESTS_ADD_FUNCS_H
+
+void gjs_test_add_tests_for_coverage ();
+
+#endif
diff --git a/test/gjs-tests.cpp b/test/gjs-tests.cpp
index 45799c5..b79b616 100644
--- a/test/gjs-tests.cpp
+++ b/test/gjs-tests.cpp
@@ -28,6 +28,8 @@
 #include <util/glib.h>
 #include <util/crash.h>
 
+#include "gjs-tests-add-funcs.h"
+
 typedef struct _GjsUnitTestFixture GjsUnitTestFixture;
 
 struct _GjsUnitTestFixture {
@@ -350,6 +352,8 @@ main(int    argc,
     g_test_add_func("/util/glib/strv/concat/null", gjstest_test_func_util_glib_strv_concat_null);
     g_test_add_func("/util/glib/strv/concat/pointers", gjstest_test_func_util_glib_strv_concat_pointers);
 
+    gjs_test_add_tests_for_coverage ();
+
     g_test_run();
 
     return 0;
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]